diff --git a/Package.swift b/Package.swift index a4f24390e4..2a245038b4 100644 --- a/Package.swift +++ b/Package.swift @@ -4,10 +4,16 @@ import PackageDescription let package = Package( name: "AudioKit", - platforms: [.macOS(.v11), .iOS(.v13), .tvOS(.v13)], + platforms: [.macOS(.v12), .iOS(.v15), .tvOS(.v13)], products: [.library(name: "AudioKit", targets: ["AudioKit"])], + dependencies: [.package(url: "https://github.com/apple/swift-atomics", from: .init(1, 0, 3))], targets: [ - .target(name: "AudioKit"), + .target(name: "AudioKit", + dependencies: ["Utilities", .product(name: "Atomics", package: "swift-atomics")], + swiftSettings: [ + .unsafeFlags(["-experimental-performance-annotations"]) + ]), + .target(name: "Utilities"), .testTarget(name: "AudioKitTests", dependencies: ["AudioKit"], resources: [.copy("TestResources/")]), ] ) diff --git a/Sources/AudioKit/AudioKit.swift b/Sources/AudioKit/AudioKit.swift new file mode 100644 index 0000000000..0d9f984229 --- /dev/null +++ b/Sources/AudioKit/AudioKit.swift @@ -0,0 +1,3 @@ +// Copyright AudioKit. All Rights Reserved. Revision History at http://github.com/AudioKit/AudioKit/ + +@_exported import Utilities diff --git a/Sources/AudioKit/Internals/Audio Unit/AVAudioUnit+Helpers.swift b/Sources/AudioKit/Internals/Audio Unit/AVAudioUnit+Helpers.swift deleted file mode 100644 index f06584a3f5..0000000000 --- a/Sources/AudioKit/Internals/Audio Unit/AVAudioUnit+Helpers.swift +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright AudioKit. All Rights Reserved. Revision History at http://github.com/AudioKit/AudioKit/ - -import AVFAudio - -func instantiate(componentDescription: AudioComponentDescription) -> AVAudioUnit { - let semaphore = DispatchSemaphore(value: 0) - var result: AVAudioUnit! - AVAudioUnit.instantiate(with: componentDescription) { avAudioUnit, _ in - guard let au = avAudioUnit else { fatalError("Unable to instantiate AVAudioUnit") } - result = au - semaphore.signal() - } - _ = semaphore.wait(wallTimeout: .distantFuture) - return result -} diff --git a/Sources/AudioKit/Internals/Engine/AVAudioEngine+Extensions.swift b/Sources/AudioKit/Internals/AudioEngine/AVAudioEngine+Extensions.swift similarity index 100% rename from Sources/AudioKit/Internals/Engine/AVAudioEngine+Extensions.swift rename to Sources/AudioKit/Internals/AudioEngine/AVAudioEngine+Extensions.swift diff --git a/Sources/AudioKit/Internals/Engine/AudioEngine+connectionTreeDescription.swift b/Sources/AudioKit/Internals/AudioEngine/AudioEngine+connectionTreeDescription.swift similarity index 100% rename from Sources/AudioKit/Internals/Engine/AudioEngine+connectionTreeDescription.swift rename to Sources/AudioKit/Internals/AudioEngine/AudioEngine+connectionTreeDescription.swift diff --git a/Sources/AudioKit/Internals/Engine/AudioEngine.swift b/Sources/AudioKit/Internals/AudioEngine/AudioEngine.swift similarity index 100% rename from Sources/AudioKit/Internals/Engine/AudioEngine.swift rename to Sources/AudioKit/Internals/AudioEngine/AudioEngine.swift diff --git a/Sources/AudioKit/Internals/Engine/AudioProgram.swift b/Sources/AudioKit/Internals/Engine/AudioProgram.swift new file mode 100644 index 0000000000..fee2b25737 --- /dev/null +++ b/Sources/AudioKit/Internals/Engine/AudioProgram.swift @@ -0,0 +1,93 @@ +// Copyright AudioKit. All Rights Reserved. Revision History at http://github.com/AudioKit/AudioKit/ + +import Foundation +import AudioUnit +import AVFoundation +import AudioToolbox +import Atomics + +/// Information about what the engine needs to run on the audio thread. +final class AudioProgram { + + /// List of information about AudioUnits we're executing. + private let jobs: Vec + + /// Nodes that we start processing first. + let generatorIndices: UnsafeBufferPointer + + private var finished: Vec> + + private var remaining = ManagedAtomic(0) + + init(jobs: [RenderJob], generatorIndices: [Int]) { + self.jobs = Vec(jobs) + self.finished = Vec>(count: jobs.count, { _ in .init(0) }) + + let ptr = UnsafeMutableBufferPointer.allocate(capacity: generatorIndices.count) + for i in generatorIndices.indices { + ptr[i] = generatorIndices[i] + } + self.generatorIndices = .init(ptr) + } + + deinit { + generatorIndices.deallocate() + } + + func reset() { + for i in 0.., + timeStamp: UnsafePointer, + frameCount: AUAudioFrameCount, + outputBufferList: UnsafeMutablePointer, + workerIndex: Int, + runQueues: Vec>) { + + let exec = { index in + let info = self.jobs[index] + + info.render(actionFlags: actionFlags, + timeStamp: timeStamp, + frameCount: frameCount, + outputBufferList: (index == self.jobs.count-1) ? outputBufferList : nil) + + // Increment outputs. + for outputIndex in self.jobs[index].outputIndices { + if self.finished[outputIndex].wrappingIncrementThenLoad(ordering: .relaxed) == self.jobs[outputIndex].inputCount { + runQueues[workerIndex].push(outputIndex) + } + } + + self.remaining.wrappingDecrement(ordering: .relaxed) + } + + while remaining.load(ordering: .relaxed) > 0 { + + // Pop an index off our queue. + if let index = runQueues[workerIndex].pop() { + exec(index) + } else { + + // Try to steal an index. Start with the next worker and wrap around, + // but don't steal from ourselves. + for i in 0.. AVAudioPCMBuffer { + let samples = Int(duration * sampleRate) + + do { + avEngine.reset() + try avEngine.enableManualRenderingMode(.offline, + format: Settings.audioFormat, + maximumFrameCount: maximumFrameCount) + try start() + } catch let err { + Log("🛑 Start Test Error: \(err)") + } + + return AVAudioPCMBuffer( + pcmFormat: avEngine.manualRenderingFormat, + frameCapacity: AVAudioFrameCount(samples) + )! + } + + /// Render audio for a specific duration + /// - Parameter duration: Length of time to render for + /// - Returns: Buffer of rendered audio + public func render(duration: Double, sampleRate: Double = 44100) -> AVAudioPCMBuffer { + let sampleCount = Int(duration * sampleRate) + let startSampleCount = Int(avEngine.manualRenderingSampleTime) + + let buffer = AVAudioPCMBuffer( + pcmFormat: avEngine.manualRenderingFormat, + frameCapacity: AVAudioFrameCount(sampleCount) + )! + + let tempBuffer = AVAudioPCMBuffer( + pcmFormat: avEngine.manualRenderingFormat, + frameCapacity: AVAudioFrameCount(maximumFrameCount) + )! + + do { + while avEngine.manualRenderingSampleTime < sampleCount + startSampleCount { + let currentSampleCount = Int(avEngine.manualRenderingSampleTime) + let framesToRender = min(UInt32(sampleCount + startSampleCount - currentSampleCount), maximumFrameCount) + try avEngine.renderOffline(AVAudioFrameCount(framesToRender), to: tempBuffer) + buffer.append(tempBuffer) + } + } catch let err { + Log("🛑 Could not render offline \(err)") + } + return buffer + } + +} diff --git a/Sources/AudioKit/Internals/Engine/EngineAudioUnit.swift b/Sources/AudioKit/Internals/Engine/EngineAudioUnit.swift new file mode 100644 index 0000000000..a69266e927 --- /dev/null +++ b/Sources/AudioKit/Internals/Engine/EngineAudioUnit.swift @@ -0,0 +1,401 @@ +// Copyright AudioKit. All Rights Reserved. Revision History at http://github.com/AudioKit/AudioKit/ + +import Foundation +import AudioUnit +import AVFoundation +import AudioToolbox +import Atomics + +public typealias AKAURenderContextObserver = (UnsafePointer?) -> Void + +/// Our single audio unit which will evaluate all audio units. +public class EngineAudioUnit: AUAudioUnit { + + private var inputBusArray: AUAudioUnitBusArray! + private var outputBusArray: AUAudioUnitBusArray! + + let inputChannelCount: NSNumber = 2 + let outputChannelCount: NSNumber = 2 + + var cachedMIDIBlock: AUScheduleMIDIEventBlock? + + override public var channelCapabilities: [NSNumber]? { + return [inputChannelCount, outputChannelCount] + } + + /// Initialize with component description and options + /// - Parameters: + /// - componentDescription: Audio Component Description + /// - options: Audio Component Instantiation Options + /// - Throws: error + override public init(componentDescription: AudioComponentDescription, + options: AudioComponentInstantiationOptions = []) throws { + + try super.init(componentDescription: componentDescription, options: options) + + let format = AVAudioFormat(standardFormatWithSampleRate: 44100, channels: 2)! + inputBusArray = AUAudioUnitBusArray(audioUnit: self, + busType: .input, + busses: [try AUAudioUnitBus(format: format)]) + outputBusArray = AUAudioUnitBusArray(audioUnit: self, + busType: .output, + busses: [try AUAudioUnitBus(format: format)]) + + parameterTree = AUParameterTree.createTree(withChildren: []) + + let oldSelector = Selector(("renderContextObserver")) + + guard let method = class_getInstanceMethod(EngineAudioUnit.self, #selector(EngineAudioUnit.akRenderContextObserver)) else { + fatalError() + } + + let newType = method_getTypeEncoding(method)! + + let imp = method_getImplementation(method) + + class_replaceMethod(EngineAudioUnit.self, oldSelector, imp, newType) + + } + + @objc dynamic public func akRenderContextObserver() -> AKAURenderContextObserver { + print("setting up render context observer") + return { [pool] workgroupPtr in + print("actually in render context observer") + + if let workgroupPtr = workgroupPtr { + print("joining workgroup") + pool.join(workgroup: workgroupPtr.pointee) + } else { + print("leaving workgroup") + pool.join(workgroup: nil) + } + } + } + + override public var inputBusses: AUAudioUnitBusArray { + inputBusArray + } + + override public var outputBusses: AUAudioUnitBusArray { + outputBusArray + } + + static func avRenderBlock(block: @escaping AVAudioEngineManualRenderingBlock) -> AURenderBlock { + { + (actionFlags: UnsafeMutablePointer, + timeStamp: UnsafePointer, + frameCount: AUAudioFrameCount, + outputBusNumber: Int, + outputBufferList: UnsafeMutablePointer, + inputBlock: AURenderPullInputBlock?) in + + var status = noErr + _ = block(frameCount, outputBufferList, &status) + + return status + } + } + + /// Returns a function which provides input from a buffer list. + /// + /// Typically, AUs are evaluated recursively. This is less than ideal for various reasons: + /// - Harder to parallelize. + /// - Stack trackes are too deep. + /// - Profiler results are hard to read. + /// + /// So instead we use a dummy input block that just copies over an ABL. + static func basicInputBlock(inputBufferLists: [SynchronizedAudioBufferList]) -> AURenderPullInputBlock { + { + (flags: UnsafeMutablePointer, + timestamp: UnsafePointer, + frames: AUAudioFrameCount, + bus: Int, + outputBuffer: UnsafeMutablePointer) in + + // We'd like to avoid actually copying samples, so just copy the ABL. + let inputBuffer = inputBufferLists[bus] + + assert(inputBuffer.abl.pointee.mNumberBuffers == outputBuffer.pointee.mNumberBuffers) + + // Note that we already have one buffer in the AudioBufferList type, hence the -1 + let ablSize = MemoryLayout.size + Int(inputBuffer.abl.pointee.mNumberBuffers-1) * MemoryLayout.size + memcpy(outputBuffer, inputBuffer.abl, ablSize) + + return noErr + } + } + + /// Returns an input block which mixes buffer lists. + static func mixerInputBlock(inputBufferLists: [SynchronizedAudioBufferList]) -> AURenderPullInputBlock { + { + (flags: UnsafeMutablePointer, + timestamp: UnsafePointer, + frameCount: AUAudioFrameCount, + bus: Int, + outputBuffer: UnsafeMutablePointer) in + + let ablPointer = UnsafeMutableAudioBufferListPointer(outputBuffer) + + for channel in 0..(ablPointer[channel]) + for frame in 0..(inputPointer[channel]) + + for frame in 0.. [ObjectIdentifier: SynchronizedAudioBufferList] { + + var buffers: [ObjectIdentifier: SynchronizedAudioBufferList] = [:] + + for node in nodes { + let length = maximumFramesToRender + let buf = AVAudioPCMBuffer(pcmFormat: format, frameCapacity: length)! + buf.frameLength = length + buffers[ObjectIdentifier(node)] = SynchronizedAudioBufferList(buf) + } + + return buffers + } + + func getOutputs(nodes: [Node]) -> [ObjectIdentifier: [Int]] { + + var nodeOutputs: [ObjectIdentifier: [Int]] = [:] + + for (index, node) in nodes.enumerated() { + for input in node.connections { + let inputId = ObjectIdentifier(input) + var outputs = nodeOutputs[inputId] ?? [] + outputs.append(index) + nodeOutputs[inputId] = outputs + } + } + + return nodeOutputs + } + + /// Recompiles our DAG of nodes into a list of render functions to be called on the audio thread. + func compile() { + // Traverse the node graph to schedule + // audio units. + + if cachedMIDIBlock == nil { + cachedMIDIBlock = scheduleMIDIEventBlock + } + + if let output = output { + + // Generate a new schedule of AUs. + var scheduled = Set() + var list: [Node] = [] + + schedule(node: output, scheduled: &scheduled, list: &list) + + // So we can look up indices of outputs. + let outputs = getOutputs(nodes: list) + + // Generate output buffers for each AU. + let buffers = makeBuffers(nodes: list) + + // Pass the schedule to the engineAU + var renderList: [RenderJob] = [] + + for node in list { + + // Activate input busses. + for busIndex in 0..(AudioProgram(jobs: [], generatorIndices: [])) + + /// Get just the signal generating nodes. + func generatorIndices(nodes: [Node]) -> [Int] { + nodes.enumerated().compactMap { (index, node) in + node.connections.isEmpty ? index : nil + } + } + + /// Recursively build a schedule of audio units to run. + func schedule(node: Node, + scheduled: inout Set, + list: inout [Node]) { + + let id = ObjectIdentifier(node) + if scheduled.contains(id) { return } + + scheduled.insert(id) + + for input in node.connections { + schedule(node: input, scheduled: &scheduled, list: &list) + } + + list.append(node) + } + + override public func allocateRenderResources() throws { + try super.allocateRenderResources() + + compile() + } + + override public func deallocateRenderResources() { + + super.deallocateRenderResources() + } + + // Worker threads. + let pool = ThreadPool() + + override public var internalRenderBlock: AUInternalRenderBlock { + + return { [pool] (actionFlags: UnsafeMutablePointer, + timeStamp: UnsafePointer, + frameCount: AUAudioFrameCount, + outputBusNumber: Int, + outputBufferList: UnsafeMutablePointer, + renderEvents: UnsafePointer?, + inputBlock: AURenderPullInputBlock?) in + + let dspList = self.program.load(ordering: .relaxed) + +// process(events: renderEvents, sysex: { pointer in +// var program: Unmanaged? +// decodeSysex(pointer, &program) +// dspList = program?.takeRetainedValue() +// }) + + // Clear output. + let outputBufferListPointer = UnsafeMutableAudioBufferListPointer(outputBufferList) + for channel in 0 ..< outputBufferListPointer.count { + outputBufferListPointer[channel].clear() + } + + // Distribute the starting indices among workers. + for (index, generatorIndex) in dspList.generatorIndices.enumerated() { + + // XXX: This could fail under very heavy load. + _ = pool.workers[index % pool.workers.count].inputQueue.push(generatorIndex) + } + + // Reset counters. + dspList.reset() + + // Setup worker threads. + for worker in pool.workers { + worker.program = dspList + worker.actionFlags = actionFlags + worker.timeStamp = timeStamp + worker.frameCount = frameCount + worker.outputBufferList = outputBufferList + } + + // Wake workers. + pool.start() + +// dspList.run(actionFlags: actionFlags, +// timeStamp: timeStamp, +// frameCount: frameCount, +// outputBufferList: outputBufferList, +// runQueue: runQueue, +// finishedInputs: finishedInputs) + + // Wait for workers to finish. + pool.wait() + + return noErr + } + } + +} diff --git a/Sources/AudioKit/Internals/Engine/README.md b/Sources/AudioKit/Internals/Engine/README.md new file mode 100644 index 0000000000..1931ce9c6f --- /dev/null +++ b/Sources/AudioKit/Internals/Engine/README.md @@ -0,0 +1,26 @@ + +# AudioKit v6 Engine + +After years of fighting with AVAudioEngine, we've decided to mostly eliminate it, relegating its use to just managing I/O. + +(Rationale is here https://github.com/AudioKit/AudioKit/issues/2804) + +## Approach + +- Instead of the recursive pull-based style of typical AUs, sort the graph and call the AUs in sequence with dummy input blocks (this makes for simpler stack traces and easier profiling). +- Write everything in Swift (AK needs to run in Playgrounds. Make use of @noAllocations and @nolocks) +- Host AUs (actually the same AUs as before!) +- Don't bother trying to reuse buffers and update in-place (this seems to be of marginal benefit on modern hardware) +- Preserve almost exactly the same API + +## Parallel Audio Rendering + +We decided to be ambitious and do parallel audio rendering using a work-stealing approach. So far we've gotten nearly a 4x speedup over the old AVAudioEngine based graph. + +We create a few worker threads which are woken by the audio thread. Those threads process RenderJobs and push the indices of subsequent jobs into their work queues. Each RenderJob renders an AudioUnit. + +## References + +[Meet Audio Workgroups](https://developer.apple.com/videos/play/wwdc2020/10224/) + +[Lock-Free Work Stealing](https://blog.molecular-matters.com/2015/08/24/job-system-2-0-lock-free-work-stealing-part-1-basics/) diff --git a/Sources/AudioKit/Internals/Engine/RenderEvents.swift b/Sources/AudioKit/Internals/Engine/RenderEvents.swift new file mode 100644 index 0000000000..d4ea8ae054 --- /dev/null +++ b/Sources/AudioKit/Internals/Engine/RenderEvents.swift @@ -0,0 +1,39 @@ +// Copyright AudioKit. All Rights Reserved. Revision History at http://github.com/AudioKit/AudioKit/ + +import Foundation +import AudioToolbox + +/// Handles the ickyness of accessing AURenderEvents without reading off the end of the struct. +/// +/// - Parameters: +/// - events: render event list +/// - midi: callback for midi events +/// - sysex: callback for sysex events +/// - param: callback for param events +func process(events: UnsafePointer?, + midi: (UnsafePointer) -> () = { _ in }, + sysex: (UnsafePointer) -> () = { _ in }, + param: (UnsafePointer) -> () = { _ in }) { + + var events = events + while let event = events { + + event.withMemoryRebound(to: AURenderEventHeader.self, capacity: 1) { pointer in + + switch pointer.pointee.eventType { + case .MIDI: + event.withMemoryRebound(to: AUMIDIEvent.self, capacity: 1, midi) + case .midiSysEx: + event.withMemoryRebound(to: AUMIDIEvent.self, capacity: 1, sysex) + case .parameter: + event.withMemoryRebound(to: AUParameterEvent.self, capacity: 1, param) + default: + break + } + + events = .init(pointer.pointee.next) + + } + + } +} diff --git a/Sources/AudioKit/Internals/Engine/RenderJob.swift b/Sources/AudioKit/Internals/Engine/RenderJob.swift new file mode 100644 index 0000000000..7acbe6b142 --- /dev/null +++ b/Sources/AudioKit/Internals/Engine/RenderJob.swift @@ -0,0 +1,87 @@ +// Copyright AudioKit. All Rights Reserved. Revision History at http://github.com/AudioKit/AudioKit/ + +import Foundation +import AudioUnit +import AVFoundation +import AudioToolbox +import Atomics + +typealias RenderJobIndex = Int + +/// Information to render a single AudioUnit +final class RenderJob { + + /// Buffer we're writing to, unless overridden by buffer passed to render. + private let outputBuffer: SynchronizedAudioBufferList + + /// Block called to render. + private let renderBlock: AURenderBlock + + /// Input block passed to the renderBlock. We don't chain AUs recursively. + private let inputBlock: AURenderPullInputBlock + + /// Number of inputs feeding this AU. + let inputCount: Int32 + + /// Indices of AUs that this one feeds. + let outputIndices: Vec + + public init(outputBuffer: SynchronizedAudioBufferList, + renderBlock: @escaping AURenderBlock, + inputBlock: @escaping AURenderPullInputBlock, + inputCount: Int32, + outputIndices: [Int]) { + self.outputBuffer = outputBuffer + self.renderBlock = renderBlock + self.inputBlock = inputBlock + self.inputCount = inputCount + self.outputIndices = Vec(outputIndices) + } + + func render(actionFlags: UnsafeMutablePointer, + timeStamp: UnsafePointer, + frameCount: AUAudioFrameCount, + outputBufferList: UnsafeMutablePointer?) { + + let out = outputBufferList ?? outputBuffer.abl + let outputBufferListPointer = UnsafeMutableAudioBufferListPointer(out) + + // AUs may change the output size, so reset it. + outputBufferListPointer[0].mDataByteSize = frameCount * UInt32(MemoryLayout.size) + outputBufferListPointer[1].mDataByteSize = frameCount * UInt32(MemoryLayout.size) + + let data0Before = outputBufferListPointer[0].mData + let data1Before = outputBufferListPointer[1].mData + + // Do the actual DSP. + let status = renderBlock(actionFlags, + timeStamp, + frameCount, + 0, + out, + inputBlock) + + // Make sure the AU doesn't change the buffer pointers! + assert(outputBufferListPointer[0].mData == data0Before) + assert(outputBufferListPointer[1].mData == data1Before) + + // Propagate errors. + if status != noErr { + switch status { + case kAudioUnitErr_NoConnection: + print("got kAudioUnitErr_NoConnection") + case kAudioUnitErr_TooManyFramesToProcess: + print("got kAudioUnitErr_TooManyFramesToProcess") + case AVAudioEngineManualRenderingError.notRunning.rawValue: + print("got AVAudioEngineManualRenderingErrorNotRunning") + case kAudio_ParamError: + print("got kAudio_ParamError") + default: + print("unknown rendering error \(status)") + } + } + + // Indicate that we're done writing to the output. + outputBuffer.endWriting() + } +} diff --git a/Sources/AudioKit/Internals/Engine/RingBuffer.swift b/Sources/AudioKit/Internals/Engine/RingBuffer.swift new file mode 100644 index 0000000000..4b1914c26e --- /dev/null +++ b/Sources/AudioKit/Internals/Engine/RingBuffer.swift @@ -0,0 +1,76 @@ +// Copyright AudioKit. All Rights Reserved. Revision History at http://github.com/AudioKit/AudioKit/ + +import Foundation +import Atomics + +/// Lock-free FIFO based on TPCircularBuffer without the fancy VM mirroring stuff. +public class RingBuffer { + + var head: Int32 = 0 + var tail: Int32 = 0 + var fillCount = ManagedAtomic(0) + var buffer: UnsafeMutableBufferPointer + + public init() { + buffer = .allocate(capacity: 1024) + } + + deinit { + buffer.deallocate() + } + + /// Push a single element + /// - Parameter value: value to be pushed + /// - Returns: whether the value could be pushed (or not enough space) + public func push(_ value: T) -> Bool { + if Int32(buffer.count) - fillCount.load(ordering: .relaxed) > 0 { + buffer[Int(head)] = value + head = (head + 1) % Int32(buffer.count) + fillCount.wrappingIncrement(ordering: .relaxed) + return true + } + return false + } + + /// Push elements from a buffer. + /// - Parameter ptr: Buffer from which to read elements. + /// - Returns: whether the elements could be pushed + public func push(from ptr: UnsafeBufferPointer) -> Bool { + if Int32(buffer.count) - fillCount.load(ordering: .relaxed) >= ptr.count { + for i in 0.. T? { + if fillCount.load(ordering: .relaxed) > 0 { + let index = Int32(tail) + tail = (tail + 1) % Int32(buffer.count) + fillCount.wrappingDecrement(ordering: .relaxed) + return buffer[Int(index)] + } + return nil + } + + /// Pop elements into a buffer. + /// - Parameter ptr: Buffer to store elements. + /// - Returns: whether the elements could be popped + public func pop(to ptr: UnsafeMutableBufferPointer) -> Bool { + if fillCount.load(ordering: .relaxed) >= ptr.count { + for i in 0.. + + /// For syncrhonization. + private var atomic = ManagedAtomic(0) + + public init(_ pcmBuffer: AVAudioPCMBuffer) { + self.pcmBuffer = pcmBuffer + self.abl = pcmBuffer.mutableAudioBufferList + } + + /// Indicate that we're done writing to the buffer. + func endWriting() { + atomic.wrappingIncrement(ordering: .releasing) + } + + /// Indicate that we're ready to read from the buffer. + func beginReading() { + atomic.wrappingIncrement(ordering: .acquiring) + } +} diff --git a/Sources/AudioKit/Internals/Engine/ThreadPool.swift b/Sources/AudioKit/Internals/Engine/ThreadPool.swift new file mode 100644 index 0000000000..153ceca1ec --- /dev/null +++ b/Sources/AudioKit/Internals/Engine/ThreadPool.swift @@ -0,0 +1,76 @@ +// Copyright AudioKit. All Rights Reserved. Revision History at http://github.com/AudioKit/AudioKit/ + +import Foundation + +/// Pool of worker threads. +/// +/// The CLAP host example uses two semaphores. See https://github.com/free-audio/clap-host/blob/56e5d267ac24593788ac1874e3643f670112cdaf/host/plugin-host.hh#L229 +final class ThreadPool { + + /// For waking up the threads. + private var prod = DispatchSemaphore(value: 0) + + /// For waiting for the workers to finish. + private var done = DispatchSemaphore(value: 0) + + /// Worker threads. + var workers: Vec + + /// Initial guess for the number of worker threads. + let workerCount = 4 // XXX: Need to query for actual worker count. + + /// Queues for each worker. + var runQueues: Vec> + + init() { + runQueues = .init(count: workerCount, { _ in .init() }) + workers = .init(count: workerCount, { [prod, done, runQueues] index in WorkerThread(index: index, runQueues: runQueues, prod: prod, done: done) }) + for worker in workers { + worker.start() + } + } + + /// Wake the threads. + func start() { + for _ in 0.. { + + private var storage: UnsafeMutableBufferPointer + + init(count: Int, _ f: (Int) -> T) { + storage = UnsafeMutableBufferPointer.allocate(capacity: count) + _ = storage.initialize(from: (0...allocate(capacity: array.count) + _ = storage.initialize(from: array) + } + + deinit { + storage.baseAddress?.deinitialize(count: count) + storage.deallocate() + } + + var count: Int { storage.count } + + subscript(index:Int) -> T { + get { + return storage[index] + } + set(newElm) { + storage[index] = newElm + } + } +} + +extension Vec : Sequence { + + func makeIterator() -> UnsafeMutableBufferPointer.Iterator { + return storage.makeIterator() + } +} diff --git a/Sources/AudioKit/Internals/Engine/WorkStealingQueue.swift b/Sources/AudioKit/Internals/Engine/WorkStealingQueue.swift new file mode 100644 index 0000000000..438e934136 --- /dev/null +++ b/Sources/AudioKit/Internals/Engine/WorkStealingQueue.swift @@ -0,0 +1,170 @@ + + +import Foundation +import Atomics + +public protocol DefaultInit { + init() +} + +/// Lock-free unbounded single-producer multiple-consumer queue. +/// +/// This class implements the work stealing queue described in the paper, +/// "Correct and Efficient Work-Stealing for Weak Memory Models," +/// available at https://www.di.ens.fr/~zappa/readings/ppopp13.pdf. +/// +/// Only the queue owner can perform pop and push operations, +/// while others can steal data from the queue. +/// Ported to swift from C++: https://github.com/taskflow/work-stealing-queue +public class WorkStealingQueue where T: AtomicValue, T: DefaultInit { + + final class QueueArray: AtomicReference { + + var C: Int + var M: Int + + var S: Vec> + + init(_ c: Int) { + C = c + M = c-1 + S = Vec(count: c, { _ in ManagedAtomic(T()) }) + } + + var capacity: Int { C } + + func push(_ i: Int, _ o: T) { + S[i & M].store(o, ordering: .relaxed) + } + + func pop(_ i: Int) -> T { + S[i & M].load(ordering: .relaxed) + } + + func resize(_ b: Int, _ t: Int) -> QueueArray { + let new = QueueArray(2*C) + for i in t ..< b { + new.push(i, pop(i)) + } + return new + } + + } + + var _top: ManagedAtomic = .init(0) + var _bottom: ManagedAtomic = .init(0) + var _array: ManagedAtomic + var _garbage: [QueueArray] = [] + + /// constructs the queue with a given capacity + /// + /// capacity the capacity of the queue (must be power of 2) + public init(capacity c: Int = 1024) { + // assert(c && (!(c & (c-1)))) + _array = .init(QueueArray(c)) + _garbage.reserveCapacity(32) + } + + /// queries if the queue is empty at the time of this call + public var isEmpty: Bool { + let b = _bottom.load(ordering: .relaxed) + let t = _top.load(ordering: .relaxed) + return b <= t + } + + /// queries the number of items at the time of this call + public var count: Int { + let b = _bottom.load(ordering: .relaxed) + let t = _top.load(ordering: .relaxed) + return b >= t ? b - t : 0 + } + + /// queries the capacity of the queue + public var capacity: Int { + _array.load(ordering: .relaxed).capacity + } + + /// inserts an item to the queue + /// + /// Only the owner thread can insert an item to the queue. + /// The operation can trigger the queue to resize its capacity + /// if more space is required. + public func push(_ o: T) { + let b = _bottom.load(ordering: .relaxed) + let t = _top.load(ordering: .acquiring) + var a = _array.load(ordering: .relaxed) + + // queue is full + if a.capacity - 1 < (b - t) { + var tmp = a.resize(b, t) + _garbage.append(a) + swap(&a, &tmp) + _array.store(a, ordering: .relaxed) + } + + a.push(b, o) + atomicMemoryFence(ordering: .releasing) + _bottom.store(b + 1, ordering: .relaxed) + } + + /// pops out an item from the queue + /// + /// Only the owner thread can pop out an item from the queue. + /// The return can be a @std_nullopt if this operation failed (empty queue). + public func pop() -> T? { + let b = _bottom.load(ordering: .relaxed) - 1 + let a = _array.load(ordering: .relaxed) + _bottom.store(b, ordering: .relaxed) + atomicMemoryFence(ordering: .sequentiallyConsistent) + let t = _top.load(ordering: .relaxed) + + var item: T? + + if t <= b { + item = a.pop(b) + if t == b { + // the last item just got stolen + let (exchanged, _) = _top.compareExchange(expected: t, + desired: t+1, + successOrdering: .sequentiallyConsistent, + failureOrdering: .relaxed) + if !exchanged { + item = nil + } + _bottom.store(b + 1, ordering: .relaxed) + } + } else { + _bottom.store(b + 1, ordering: .relaxed) + } + + return item + } + + /// steals an item from the queue + /// + /// Any threads can try to steal an item from the queue. + /// The return can be nil if this operation failed (not necessary empty). + public func steal() -> T? { + let t = _top.load(ordering: .acquiring) + atomicMemoryFence(ordering: .sequentiallyConsistent) + let b = _bottom.load(ordering: .acquiring) + + var item: T? + + if(t < b) { + let a = _array.load(ordering: .acquiring) + item = a.pop(t) + + let (exchanged, _) = _top.compareExchange(expected: t, + desired: t+1, + successOrdering: .sequentiallyConsistent, + failureOrdering: .relaxed) + + if !exchanged { + return nil + } + } + + return item + } +} diff --git a/Sources/AudioKit/Internals/Engine/WorkerThread.swift b/Sources/AudioKit/Internals/Engine/WorkerThread.swift new file mode 100644 index 0000000000..d1a499c985 --- /dev/null +++ b/Sources/AudioKit/Internals/Engine/WorkerThread.swift @@ -0,0 +1,121 @@ +// Copyright AudioKit. All Rights Reserved. Revision History at http://github.com/AudioKit/AudioKit/ + +import Foundation +import AudioUnit +import AVFoundation +import AudioToolbox + +extension Int: DefaultInit { + public init() { self = 0 } +} + +final class WorkerThread: Thread { + + /// Used to exit the worker thread. + private var run = true + + /// Used to wake the worker. + private var prod: DispatchSemaphore + + /// Used to wait for the worker to finish a cycle. + private var done: DispatchSemaphore + + /// Information about rendering jobs. + var program: AudioProgram? + + /// AU stuff. + var actionFlags: UnsafeMutablePointer! + + /// AU stuff. + var timeStamp: UnsafePointer! + + /// Number of audio frames to render. + var frameCount: AUAudioFrameCount = 0 + + /// Our main output buffer. + var outputBufferList: UnsafeMutablePointer? + + /// Queue for submitting jobs to the worker. + /// + /// Once we implement stealing, we could simply have workers steal from a main queue. + var inputQueue = RingBuffer() + + /// Index of this worker. + var workerIndex: Int + + private var runQueues: Vec> + + var workgroup: WorkGroup? + + var joinToken: WorkGroup.JoinToken? + + init(index: Int, + runQueues: Vec>, + prod: DispatchSemaphore, + done: DispatchSemaphore, + workgroup: WorkGroup? = nil) { + self.workerIndex = index + self.runQueues = runQueues + self.prod = prod + self.done = done + self.workgroup = workgroup + } + + override func main() { + + if let workgroup = workgroup { + var tbinfo = mach_timebase_info_data_t() + mach_timebase_info(&tbinfo) + + let seconds = (Double(tbinfo.denom) / Double(tbinfo.numer)) * 1_000_000_000 + + // Guessing what the parameters would be for 128 frame buffer at 44.1kHz + let period = (128.0/44100.0) * seconds + let constraint = 0.5 * period + let comp = 0.5 * constraint + + if !set_realtime(period: UInt32(period), computation: UInt32(comp), constraint: UInt32(constraint)) { + print("failed to set worker thread to realtime priority") + } + + joinToken = workgroup.join() + } + + while true { + prod.wait() + + if !run { + break + } + + while let index = inputQueue.pop() { + runQueues[workerIndex].push(index) + } + + // print("worker starting") + + if let program = program { + program.run(actionFlags: actionFlags, + timeStamp: timeStamp, + frameCount: frameCount, + outputBufferList: outputBufferList!, + workerIndex: workerIndex, + runQueues: runQueues) + } else { + print("worker has no program!") + } + + // print("worker done") + done.signal() + } + + if let joinToken = joinToken { + workgroup?.leave(token: joinToken) + } + } + + func exit() { + run = false + prod.signal() + } +} diff --git a/Sources/AudioKit/Internals/Engine/realtime.swift b/Sources/AudioKit/Internals/Engine/realtime.swift new file mode 100644 index 0000000000..ef678e09bf --- /dev/null +++ b/Sources/AudioKit/Internals/Engine/realtime.swift @@ -0,0 +1,32 @@ +// Copyright AudioKit. All Rights Reserved. Revision History at http://github.com/AudioKit/AudioKit/ + +import Foundation + +/// Set the current thread to realtime priority. +/// +/// Adapted from [here](https://developer.apple.com/library/archive/documentation/Darwin/Conceptual/KernelProgramming/scheduler/scheduler.html) +func set_realtime(period: UInt32, computation: UInt32, constraint: UInt32) -> Bool { + let TIME_CONSTRAINT_POLICY: UInt32 = 2 + let TIME_CONSTRAINT_POLICY_COUNT = UInt32(MemoryLayout.size / MemoryLayout.size) + let SUCCESS: Int32 = 0 + var policy: thread_time_constraint_policy = .init() + var ret: Int32 + let thread: thread_port_t = pthread_mach_thread_np(pthread_self()) + + policy.period = period + policy.computation = computation + policy.constraint = constraint + policy.preemptible = 1 + + ret = withUnsafeMutablePointer(to: &policy) { + $0.withMemoryRebound(to: integer_t.self, capacity: Int(TIME_CONSTRAINT_POLICY_COUNT)) { + thread_policy_set(thread, TIME_CONSTRAINT_POLICY, $0, TIME_CONSTRAINT_POLICY_COUNT) + } + } + + if ret != SUCCESS { + print(stderr, "set_realtime() failed.\n") + return false + } + return true +} diff --git a/Sources/AudioKit/Internals/Engine/sysex.swift b/Sources/AudioKit/Internals/Engine/sysex.swift new file mode 100644 index 0000000000..ab6d809c69 --- /dev/null +++ b/Sources/AudioKit/Internals/Engine/sysex.swift @@ -0,0 +1,99 @@ +// Copyright AudioKit. All Rights Reserved. Revision History at http://github.com/AudioKit/AudioKit/ + +import Foundation +import AudioUnit + +/// Encode a value in a MIDI sysex message. Value must be plain-old-data. +public func encodeSysex(_ value: T) -> [UInt8] { + + assert(_isPOD(type(of: value))) + + // Start with a sysex header. + var result: [UInt8] = [0xF0, 0x00] + + // Encode the value as a sequence of nibbles. + // There might be some more efficient way to do this, + // but we can't clash with the 0xF7 end-of-message. + // We may not actually need to encode a valid MIDI sysex + // message, but that could be implementation dependent + // and change over time. Best to be safe. + withUnsafeBytes(of: value) { ptr in + for byte in ptr { + result.append( byte >> 4 ) + result.append( byte & 0xF ) + } + } + + result.append(0xF7) + return result +} + +/// Decode a sysex message into a value. Value must be plain-old-data. +/// +/// We can't return a value because we can't assume the value can be +/// default constructed. +/// +/// - Parameters: +/// - bytes: the sysex message +/// - count: number of bytes in message +/// - value: the value we're writing to +/// +public func decodeSysex(_ bytes: UnsafePointer, count: Int, _ value: inout T) { + + assert(_isPOD(type(of: value))) + + // Number of bytes should include sysex header (0xF0, 0x00) and terminator (0xF7). + assert(count == 2*MemoryLayout.size + 3) + + withUnsafeMutableBytes(of: &value) { ptr in + for i in 0.., _ f: (UnsafePointer) -> ()) { + + let type = event.pointee.eventType + assert(type == .midiSysEx || type == .MIDI) + + let length = event.pointee.length + if let offset = MemoryLayout.offset(of: \AUMIDIEvent.data) { + + let raw = UnsafeRawPointer(event)! + offset + + raw.withMemoryRebound(to: UInt8.self, capacity: Int(length)) { pointer in + f(pointer) + } + } +} + +/// Decode a value from a sysex AURenderEvent. +/// +/// We can't return a value because we can't assume the value can be +/// default constructed. +/// +/// - Parameters: +/// - event: pointer to the AURenderEvent +/// - value: where we will store the value +func decodeSysex(_ event: UnsafePointer, _ value: inout T) { + + assert(_isPOD(type(of: value))) + + let type = event.pointee.eventType + assert(type == .midiSysEx) + + let length = event.pointee.length + withMidiData(event) { ptr in + decodeSysex(ptr, count: Int(length), &value) + } +} diff --git a/Sources/AudioKit/Internals/Hardware/DeviceUtils.swift b/Sources/AudioKit/Internals/Hardware/DeviceUtils.swift index 4a7b591ca0..94c08da2e5 100644 --- a/Sources/AudioKit/Internals/Hardware/DeviceUtils.swift +++ b/Sources/AudioKit/Internals/Hardware/DeviceUtils.swift @@ -11,7 +11,7 @@ struct AudioDeviceUtils { var address: AudioObjectPropertyAddress = AudioObjectPropertyAddress( mSelector: AudioObjectPropertySelector(kAudioHardwarePropertyDevices), mScope: AudioObjectPropertyScope(kAudioObjectPropertyScopeGlobal), - mElement: AudioObjectPropertyElement(kAudioObjectPropertyElementMaster)) + mElement: AudioObjectPropertyElement(kAudioObjectPropertyElementMain)) var result = AudioObjectGetPropertyDataSize(AudioObjectID(kAudioObjectSystemObject), &address, @@ -55,7 +55,7 @@ struct AudioDeviceUtils { var address: AudioObjectPropertyAddress = AudioObjectPropertyAddress( mSelector: AudioObjectPropertySelector(kAudioDevicePropertyDeviceNameCFString), mScope: AudioObjectPropertyScope(kAudioObjectPropertyScopeGlobal), - mElement: AudioObjectPropertyElement(kAudioObjectPropertyElementMaster)) + mElement: AudioObjectPropertyElement(kAudioObjectPropertyElementMain)) var name: CFString? var propsize: UInt32 = UInt32(MemoryLayout.size) @@ -129,7 +129,7 @@ struct AudioDeviceUtils { var address: AudioObjectPropertyAddress = AudioObjectPropertyAddress( mSelector: AudioObjectPropertySelector(kAudioDevicePropertyDeviceUID), mScope: AudioObjectPropertyScope(kAudioObjectPropertyScopeGlobal), - mElement: AudioObjectPropertyElement(kAudioObjectPropertyElementMaster)) + mElement: AudioObjectPropertyElement(kAudioObjectPropertyElementMain)) var name: CFString? var propsize: UInt32 = UInt32(MemoryLayout.size) diff --git a/Sources/AudioKit/Internals/Utilities/AVAudioPCMBuffer+audition.swift b/Sources/AudioKit/Internals/Utilities/AVAudioPCMBuffer+audition.swift index 05ba43db50..b8f066c776 100644 --- a/Sources/AudioKit/Internals/Utilities/AVAudioPCMBuffer+audition.swift +++ b/Sources/AudioKit/Internals/Utilities/AVAudioPCMBuffer+audition.swift @@ -6,16 +6,15 @@ public extension AVAudioPCMBuffer { /// Audition the buffer. Especially useful in AudioKit testing func audition() { let engine = AudioEngine() - let player = AudioPlayer() - engine.output = player + let sampler = Sampler() + engine.output = sampler do { try engine.start() } catch let error as NSError { Log(error, type: .error) return } - player.buffer = self - player.play() + sampler.play(self) sleep(frameCapacity / UInt32(format.sampleRate)) engine.stop() } diff --git a/Sources/AudioKit/Nodes/Effects/Reverb.swift b/Sources/AudioKit/Nodes/Effects/Reverb.swift index eff6328b85..395bb1a5c0 100644 --- a/Sources/AudioKit/Nodes/Effects/Reverb.swift +++ b/Sources/AudioKit/Nodes/Effects/Reverb.swift @@ -44,7 +44,7 @@ public class Reverb: Node { } /// Tells whether the node is processing (ie. started, playing, or active) - public internal(set) var isStarted = true + public var isStarted = true /// Initialize the reverb node /// diff --git a/Sources/AudioKit/Nodes/Generators/PlaygroundNoiseGenerator.swift b/Sources/AudioKit/Nodes/Generators/PlaygroundNoiseGenerator.swift index 98ac29c6a8..858ca855dc 100644 --- a/Sources/AudioKit/Nodes/Generators/PlaygroundNoiseGenerator.swift +++ b/Sources/AudioKit/Nodes/Generators/PlaygroundNoiseGenerator.swift @@ -1,15 +1,127 @@ // Copyright AudioKit. All Rights Reserved. Revision History at http://github.com/AudioKit/AudioKit/ +import AudioUnit import AVFoundation -import CoreAudio // for UnsafeMutableAudioBufferListPointer -/// Pure Swift Noise Generator -@available(macOS 10.15, iOS 13.0, tvOS 13.0, *) public class PlaygroundNoiseGenerator: Node { - fileprivate lazy var sourceNode = AVAudioSourceNode { [self] _, _, frameCount, audioBufferList in - let ablPointer = UnsafeMutableAudioBufferListPointer(audioBufferList) + public let connections: [Node] = [] + + public let avAudioNode: AVAudioNode + + let noiseAU: PlaygroundNoiseGeneratorAudioUnit + + /// Output Volume (Default 1), values above 1 will have gain applied + public var amplitude: AUValue = 1.0 { + didSet { + amplitude = max(amplitude, 0) + noiseAU.amplitudeParam.value = amplitude + self.start() + } + } + + /// Initialize the pure Swift NoiseGenerator, suitable for Playgrounds + /// - Parameters: + /// - amplitude: Volume, usually 0-1 + public init(amplitude: AUValue = 1.0) { + + let componentDescription = AudioComponentDescription(instrument: "pgns") + + AUAudioUnit.registerSubclass(PlaygroundNoiseGeneratorAudioUnit.self, + as: componentDescription, + name: "NoiseGenerator AU", + version: .max) + avAudioNode = instantiate(componentDescription: componentDescription) + noiseAU = avAudioNode.auAudioUnit as! PlaygroundNoiseGeneratorAudioUnit + self.noiseAU.amplitudeParam.value = amplitude + self.amplitude = amplitude + self.stop() + } +} + + +/// Renders an NoiseGenerator +class PlaygroundNoiseGeneratorAudioUnit: AUAudioUnit { + + private var inputBusArray: AUAudioUnitBusArray! + private var outputBusArray: AUAudioUnitBusArray! + + let inputChannelCount: NSNumber = 2 + let outputChannelCount: NSNumber = 2 + + override public var channelCapabilities: [NSNumber]? { + return [inputChannelCount, outputChannelCount] + } + + let amplitudeParam = AUParameterTree.createParameter(identifier: "amplitude", name: "amplitude", address: 0, range: 0...10, unit: .generic, flags: []) + + /// Initialize with component description and options + /// - Parameters: + /// - componentDescription: Audio Component Description + /// - options: Audio Component Instantiation Options + /// - Throws: error + override public init(componentDescription: AudioComponentDescription, + options: AudioComponentInstantiationOptions = []) throws { + + try super.init(componentDescription: componentDescription, options: options) + + let format = AVAudioFormat(standardFormatWithSampleRate: 44100, channels: 2)! + inputBusArray = AUAudioUnitBusArray(audioUnit: self, busType: .input, busses: []) + outputBusArray = AUAudioUnitBusArray(audioUnit: self, busType: .output, busses: [try AUAudioUnitBus(format: format)]) + + parameterTree = AUParameterTree.createTree(withChildren: [amplitudeParam]) + + let paramBlock = self.scheduleParameterBlock + + parameterTree?.implementorValueObserver = { parameter, value in + paramBlock(.zero, 0, parameter.address, parameter.value) + } + } + + override var inputBusses: AUAudioUnitBusArray { + inputBusArray + } + + override var outputBusses: AUAudioUnitBusArray { + outputBusArray + } + + override func allocateRenderResources() throws {} + + override func deallocateRenderResources() {} + + + /// Volume usually 0-1 + var amplitude: AUValue = 1 + + func processEvents(events: UnsafePointer?) { + + process(events: events, + param: { event in + + let paramEvent = event.pointee + + switch paramEvent.parameterAddress { + case 0: amplitude = paramEvent.value + default: break + } + + }) + + } + + override var internalRenderBlock: AUInternalRenderBlock { + { (actionFlags: UnsafeMutablePointer, + timeStamp: UnsafePointer, + frameCount: AUAudioFrameCount, + outputBusNumber: Int, + outputBufferList: UnsafeMutablePointer, + renderEvents: UnsafePointer?, + inputBlock: AURenderPullInputBlock?) in + + self.processEvents(events: renderEvents) + + let ablPointer = UnsafeMutableAudioBufferListPointer(outputBufferList) - if self.isStarted { for frame in 0 ..< Int(frameCount) { // Get signal value for this frame at time. let value = self.amplitude * Float.random(in: -1 ... 1) @@ -17,35 +129,17 @@ public class PlaygroundNoiseGenerator: Node { // Set the same value on all channels (due to the inputFormat we have only 1 channel though). for buffer in ablPointer { let buf: UnsafeMutableBufferPointer = UnsafeMutableBufferPointer(buffer) - buf[frame] = value - } - } - } else { - for frame in 0 ..< Int(frameCount) { - for buffer in ablPointer { - let buf: UnsafeMutableBufferPointer = UnsafeMutableBufferPointer(buffer) - buf[frame] = 0 + if self.shouldBypassEffect { + buf[frame] = 0 + } else { + buf[frame] = value + } } } + + return noErr } - return noErr } - /// Connected nodes - public var connections: [Node] { [] } - - /// Underlying AVAudioNode - public var avAudioNode: AVAudioNode { sourceNode } - - /// Volume usually 0-1 - public var amplitude: AUValue = 1 - - /// Initialize the pure Swift noise generator, suitable for Playgrounds - /// - Parameters: - /// - amplitude: Volume, usually 0-1 - public init(amplitude: AUValue = 1) { - self.amplitude = amplitude - - stop() - } } + diff --git a/Sources/AudioKit/Nodes/Generators/PlaygroundOscillator.swift b/Sources/AudioKit/Nodes/Generators/PlaygroundOscillator.swift index 70c19cca0e..46c7b65908 100644 --- a/Sources/AudioKit/Nodes/Generators/PlaygroundOscillator.swift +++ b/Sources/AudioKit/Nodes/Generators/PlaygroundOscillator.swift @@ -1,70 +1,212 @@ // Copyright AudioKit. All Rights Reserved. Revision History at http://github.com/AudioKit/AudioKit/ +import AudioUnit import AVFoundation -import CoreAudio -let twoPi = 2 * Float.pi +enum PlaygroundOscillatorCommand { + case table(UnsafeMutablePointer?) +} -/// Pure Swift oscillator -@available(macOS 10.15, iOS 13.0, tvOS 13.0, *) public class PlaygroundOscillator: Node { - fileprivate lazy var sourceNode = AVAudioSourceNode { [self] _, _, frameCount, audioBufferList in - let ablPointer = UnsafeMutableAudioBufferListPointer(audioBufferList) + public let connections: [Node] = [] - if self.isStarted { - let phaseIncrement = (twoPi / Float(Settings.sampleRate)) * self.frequency - for frame in 0 ..< Int(frameCount) { - // Get signal value for this frame at time. - let index = Int(self.currentPhase / twoPi * Float(self.waveform!.count)) - let value = self.waveform![index] * self.amplitude + public let avAudioNode: AVAudioNode - // Advance the phase for the next frame. - self.currentPhase += phaseIncrement - if self.currentPhase >= twoPi { self.currentPhase -= twoPi } - if self.currentPhase < 0.0 { self.currentPhase += twoPi } - // Set the same value on all channels (due to the inputFormat we have only 1 channel though). - for buffer in ablPointer { - let buf: UnsafeMutableBufferPointer = UnsafeMutableBufferPointer(buffer) - buf[frame] = value - } - } - } else { - for frame in 0 ..< Int(frameCount) { - for buffer in ablPointer { - let buf: UnsafeMutableBufferPointer = UnsafeMutableBufferPointer(buffer) - buf[frame] = 0 - } + let oscAU: PlaygroundOscillatorAudioUnit + + public var waveform: Table? { + didSet { + if let waveform = waveform { + oscAU.setWaveform(waveform) } } - return noErr + } + + /// Output Volume (Default 1), values above 1 will have gain applied + public var amplitude: AUValue = 1.0 { + didSet { + amplitude = max(amplitude, 0) + oscAU.amplitudeParam.value = amplitude + } } - /// Connected nodes - public var connections: [Node] { [] } - - /// Underlying AVAudioNode - public var avAudioNode: AVAudioNode { sourceNode } - - private var currentPhase: Float = 0 - - fileprivate var waveform: Table? - - /// Pitch in Hz - public var frequency: Float = 440 - - /// Volume usually 0-1 - public var amplitude: AUValue = 1 + // Frequency in Hz + public var frequency: AUValue = 440 { + didSet { + frequency = max(frequency, 0) + oscAU.frequencyParam.value = frequency + } + } /// Initialize the pure Swift oscillator, suitable for Playgrounds /// - Parameters: /// - waveform: Shape of the oscillator waveform /// - frequency: Pitch in Hz /// - amplitude: Volume, usually 0-1 - public init(waveform: Table = Table(.sine), frequency: AUValue = 440, amplitude: AUValue = 1) { + public init(waveform: Table = Table(.sine), frequency: AUValue = 440, amplitude: AUValue = 1.0) { + + let componentDescription = AudioComponentDescription(instrument: "pgos") + + AUAudioUnit.registerSubclass(PlaygroundOscillatorAudioUnit.self, + as: componentDescription, + name: "Oscillator AU", + version: .max) + avAudioNode = instantiate(componentDescription: componentDescription) + oscAU = avAudioNode.auAudioUnit as! PlaygroundOscillatorAudioUnit self.waveform = waveform - self.frequency = frequency + self.oscAU.amplitudeParam.value = amplitude self.amplitude = amplitude + self.oscAU.frequencyParam.value = frequency + self.frequency = frequency + self.oscAU.setWaveform(waveform) + self.waveform = waveform + self.stop() + } +} + + +/// Renders an oscillator +class PlaygroundOscillatorAudioUnit: AUAudioUnit { + + private var inputBusArray: AUAudioUnitBusArray! + private var outputBusArray: AUAudioUnitBusArray! - stop() + let inputChannelCount: NSNumber = 2 + let outputChannelCount: NSNumber = 2 + + override public var channelCapabilities: [NSNumber]? { + return [inputChannelCount, outputChannelCount] + } + + var cachedMIDIBlock: AUScheduleMIDIEventBlock? + + let frequencyParam = AUParameterTree.createParameter(identifier: "frequency", name: "frequency", address: 0, range: 0...22050, unit: .hertz, flags: []) + + let amplitudeParam = AUParameterTree.createParameter(identifier: "amplitude", name: "amplitude", address: 1, range: 0...10, unit: .generic, flags: []) + + func setWaveform(_ waveform: Table) { + let holder = UnsafeMutablePointer
.allocate(capacity: 1) + + holder.initialize(to: waveform) + + let command: PlaygroundOscillatorCommand = .table(holder) + let sysex = encodeSysex(command) + + if cachedMIDIBlock == nil { + cachedMIDIBlock = scheduleMIDIEventBlock + assert(cachedMIDIBlock != nil) + } + + if let block = cachedMIDIBlock { + block(.zero, 0, sysex.count, sysex) + } } + + /// Initialize with component description and options + /// - Parameters: + /// - componentDescription: Audio Component Description + /// - options: Audio Component Instantiation Options + /// - Throws: error + override public init(componentDescription: AudioComponentDescription, + options: AudioComponentInstantiationOptions = []) throws { + + try super.init(componentDescription: componentDescription, options: options) + + let format = AVAudioFormat(standardFormatWithSampleRate: 44100, channels: 2)! + inputBusArray = AUAudioUnitBusArray(audioUnit: self, busType: .input, busses: []) + outputBusArray = AUAudioUnitBusArray(audioUnit: self, busType: .output, busses: [try AUAudioUnitBus(format: format)]) + + parameterTree = AUParameterTree.createTree(withChildren: [frequencyParam, amplitudeParam]) + + let paramBlock = self.scheduleParameterBlock + + parameterTree?.implementorValueObserver = { parameter, value in + paramBlock(.zero, 0, parameter.address, parameter.value) + } + } + + override var inputBusses: AUAudioUnitBusArray { + inputBusArray + } + + override var outputBusses: AUAudioUnitBusArray { + outputBusArray + } + + override func allocateRenderResources() throws {} + + override func deallocateRenderResources() {} + + var currentPhase: AUValue = 0.0 + + /// Pitch in Hz + var frequency: AUValue = 440 + + /// Volume usually 0-1 + var amplitude: AUValue = 1 + + private var table = Table() + + func processEvents(events: UnsafePointer?) { + process(events: events, + sysex: { event in + var command: PlaygroundOscillatorCommand = .table(nil) + + decodeSysex(event, &command) + switch command { + case .table(let ptr): + table = ptr?.pointee ?? Table() + } + }, param: { event in + let paramEvent = event.pointee + switch paramEvent.parameterAddress { + case 0: frequency = paramEvent.value + case 1: amplitude = paramEvent.value + default: break + } + } + ) + } + + override var internalRenderBlock: AUInternalRenderBlock { + { (actionFlags: UnsafeMutablePointer, + timeStamp: UnsafePointer, + frameCount: AUAudioFrameCount, + outputBusNumber: Int, + outputBufferList: UnsafeMutablePointer, + renderEvents: UnsafePointer?, + inputBlock: AURenderPullInputBlock?) in + + self.processEvents(events: renderEvents) + + let ablPointer = UnsafeMutableAudioBufferListPointer(outputBufferList) + + let twoPi: AUValue = AUValue(2 * Double.pi) + let phaseIncrement = (twoPi / AUValue(Settings.sampleRate)) * self.frequency + for frame in 0 ..< Int(frameCount) { + // Get signal value for this frame at time. + let index = Int(self.currentPhase / twoPi * Float(self.table.count)) + let value = self.table[index] * self.amplitude + + // Advance the phase for the next frame. + self.currentPhase += phaseIncrement + if self.currentPhase >= twoPi { self.currentPhase -= twoPi } + if self.currentPhase < 0.0 { self.currentPhase += twoPi } + // Set the same value on all channels (due to the inputFormat we have only 1 channel though). + for buffer in ablPointer { + let buf = UnsafeMutableBufferPointer(buffer) + assert(frame < buf.count) + if self.shouldBypassEffect { + buf[frame] = 0 + } else { + buf[frame] = value + } + } + } + + return noErr + } + } + } + diff --git a/Sources/AudioKit/Nodes/Mixing/MatrixMixer.swift b/Sources/AudioKit/Nodes/Mixing/MatrixMixer.swift index 9c096f9308..7a19cecaa1 100644 --- a/Sources/AudioKit/Nodes/Mixing/MatrixMixer.swift +++ b/Sources/AudioKit/Nodes/Mixing/MatrixMixer.swift @@ -25,6 +25,9 @@ /// )! /// ``` +// We're not supporting multichannel audio in the new v6 engine at first. +#if false + import AVFAudio public class MatrixMixer: Node { @@ -193,3 +196,5 @@ public class MatrixMixer: Node { print("Output Volumes - \(last[0..?) { + + process(events: events, param: { event in + let paramEvent = event.pointee + + switch paramEvent.parameterAddress { + case 0: volume = paramEvent.value + case 1: pan = paramEvent.value + default: break + } + }) + } + + override var internalRenderBlock: AUInternalRenderBlock { + { (actionFlags: UnsafeMutablePointer, + timeStamp: UnsafePointer, + frameCount: AUAudioFrameCount, + outputBusNumber: Int, + outputBufferList: UnsafeMutablePointer, + renderEvents: UnsafePointer?, + inputBlock: AURenderPullInputBlock?) in + + self.processEvents(events: renderEvents) + + let ablPointer = UnsafeMutableAudioBufferListPointer(outputBufferList) + + // Better be stereo. + assert(ablPointer.count == 2) + + // Check that buffers are the correct size. + if ablPointer[0].frameCapacity < frameCount { + print("output buffer 1 too small: \(ablPointer[0].frameCapacity), expecting: \(frameCount)") + return kAudio_ParamError + } + + if ablPointer[1].frameCapacity < frameCount { + print("output buffer 2 too small: \(ablPointer[1].frameCapacity), expecting: \(frameCount)") + return kAudio_ParamError + } + + var inputFlags: AudioUnitRenderActionFlags = [] + _ = inputBlock?(&inputFlags, timeStamp, frameCount, 0, outputBufferList) + + let outBufL = UnsafeMutableBufferPointer(ablPointer[0]) + let outBufR = UnsafeMutableBufferPointer(ablPointer[1]) + for frame in 0.. 0 { + outBufL[frame] *= 1.0 - self.pan + } else if self.pan < 0 { + outBufR[frame] *= 1.0 + self.pan + } + outBufL[frame] *= self.volume + outBufR[frame] *= self.volume + + } + return noErr + } + } + +} + diff --git a/Sources/AudioKit/Nodes/Node+Ext.swift b/Sources/AudioKit/Nodes/Node+Ext.swift new file mode 100644 index 0000000000..8b678f6c81 --- /dev/null +++ b/Sources/AudioKit/Nodes/Node+Ext.swift @@ -0,0 +1,171 @@ +// Copyright AudioKit. All Rights Reserved. Revision History at http://github.com/AudioKit/AudioKit/ + +import AVFoundation + +public extension Node { + /// Reset the internal state of the unit + /// Fixes issues such as https://github.com/AudioKit/AudioKit/issues/2046 + func reset() { + if let avAudioUnit = avAudioNode as? AVAudioUnit { + AudioUnitReset(avAudioUnit.audioUnit, kAudioUnitScope_Global, 0) + } + } + +#if !os(tvOS) + /// Schedule an event with an offset + /// + /// - Parameters: + /// - event: MIDI Event to schedule + /// - offset: Time in samples + /// + func scheduleMIDIEvent(event: MIDIEvent, offset: UInt64 = 0) { + if let midiBlock = avAudioNode.auAudioUnit.scheduleMIDIEventBlock { + event.data.withUnsafeBufferPointer { ptr in + guard let ptr = ptr.baseAddress else { return } + midiBlock(AUEventSampleTimeImmediate + AUEventSampleTime(offset), 0, event.data.count, ptr) + } + } + } +#endif + + var isStarted: Bool { !bypassed } + func start() { bypassed = false } + func stop() { bypassed = true } + func play() { bypassed = false } + func bypass() { bypassed = true } + var outputFormat: AVAudioFormat { Settings.audioFormat } + + /// All parameters on the Node + var parameters: [NodeParameter] { + let mirror = Mirror(reflecting: self) + var params: [NodeParameter] = [] + + for child in mirror.children { + if let param = child.value as? ParameterBase { + params.append(param.projectedValue) + } + } + + return params + } + + /// Set up node parameters using reflection + func setupParameters() { + let mirror = Mirror(reflecting: self) + var params: [AUParameter] = [] + + for child in mirror.children { + if let param = child.value as? ParameterBase { + let def = param.projectedValue.def + let auParam = AUParameterTree.createParameter(identifier: def.identifier, + name: def.name, + address: def.address, + range: def.range, + unit: def.unit, + flags: def.flags) + params.append(auParam) + param.projectedValue.associate(with: avAudioNode, parameter: auParam) + } + } + + avAudioNode.auAudioUnit.parameterTree = AUParameterTree.createTree(withChildren: params) + } +} + +extension Node { + + /// The underlying AudioUnit for the node. + /// + /// NOTE: some AVAudioNodes (e.g. AVAudioPlayerNode) can't be used without AVAudioEngine. + /// For those we'll need to use AUAudioUnit directly and override this. + public var au: AUAudioUnit { + avAudioNode.auAudioUnit + } + + func disconnectAV() { + if let engine = avAudioNode.engine { + engine.disconnectNodeInput(avAudioNode) + for (_, connection) in connections.enumerated() { + connection.disconnectAV() + } + } + } + + /// Work-around for an AVAudioEngine bug. + func initLastRenderTime() { + // We don't have a valid lastRenderTime until we query it. + _ = avAudioNode.lastRenderTime + + for connection in connections { + connection.initLastRenderTime() + } + } + + /// Scan for all parameters and associate with the node. + /// - Parameter node: AVAudioNode to associate + func associateParams(with node: AVAudioNode) { + let mirror = Mirror(reflecting: self) + + for child in mirror.children { + if let param = child.value as? ParameterBase { + param.projectedValue.associate(with: node) + } + } + } + + func makeAVConnections() { + if let node = self as? HasInternalConnections { + node.makeInternalConnections() + } + + // Are we attached? + if let engine = avAudioNode.engine { + for (bus, connection) in connections.enumerated() { + if let sourceEngine = connection.avAudioNode.engine { + if sourceEngine != avAudioNode.engine { + Log("🛑 Error: Attempt to connect nodes from different engines.") + return + } + } + + engine.attach(connection.avAudioNode) + + // Mixers will decide which input bus to use. + if let mixer = avAudioNode as? AVAudioMixerNode { + mixer.connectMixer(input: connection.avAudioNode, format: connection.outputFormat) + if let akMixer = self as? Mixer { + mixer.outputVolume = akMixer.volume + } + } else { + avAudioNode.connect(input: connection.avAudioNode, bus: bus, format: connection.outputFormat) + } + + connection.makeAVConnections() + } + } + } + + var bypassed: Bool { + get { avAudioNode.auAudioUnit.shouldBypassEffect } + set { avAudioNode.auAudioUnit.shouldBypassEffect = newValue } + } +} + +public protocol HasInternalConnections: AnyObject { + /// Override point for any connections internal to the node. + func makeInternalConnections() +} + +/// Protocol mostly to support DynamicOscillator in SoundpipeAudioKit, but could be used elsewhere +public protocol DynamicWaveformNode: Node { + /// Sets the wavetable + /// - Parameter waveform: The tablve + func setWaveform(_ waveform: Table) + + /// Gets the floating point values stored in the wavetable + func getWaveformValues() -> [Float] + + /// Set the waveform change handler + /// - Parameter handler: Closure with an array of floats as the argument + func setWaveformUpdateHandler(_ handler: @escaping ([Float]) -> Void) +} diff --git a/Sources/AudioKit/Nodes/Node+Graphviz.swift b/Sources/AudioKit/Nodes/Node+Graphviz.swift index 9424ca339f..abf66bdf75 100644 --- a/Sources/AudioKit/Nodes/Node+Graphviz.swift +++ b/Sources/AudioKit/Nodes/Node+Graphviz.swift @@ -10,7 +10,7 @@ extension ObjectIdentifier { fileprivate var labels: [ObjectIdentifier: String] = [:] -extension Node { +public extension Node { /// A label for to use when printing the dot. var label: String { diff --git a/Sources/AudioKit/Nodes/Node.swift b/Sources/AudioKit/Nodes/Node.swift index 958f06507e..ef4399b24d 100644 --- a/Sources/AudioKit/Nodes/Node.swift +++ b/Sources/AudioKit/Nodes/Node.swift @@ -25,164 +25,7 @@ public protocol Node: AnyObject { /// Audio format to use when connecting this node. /// Defaults to `Settings.audioFormat`. var outputFormat: AVAudioFormat { get } -} - -public extension Node { - /// Reset the internal state of the unit - /// Fixes issues such as https://github.com/AudioKit/AudioKit/issues/2046 - func reset() { - if let avAudioUnit = avAudioNode as? AVAudioUnit { - AudioUnitReset(avAudioUnit.audioUnit, kAudioUnitScope_Global, 0) - } - } - -#if !os(tvOS) - /// Schedule an event with an offset - /// - /// - Parameters: - /// - event: MIDI Event to schedule - /// - offset: Time in samples - /// - func scheduleMIDIEvent(event: MIDIEvent, offset: UInt64 = 0) { - if let midiBlock = avAudioNode.auAudioUnit.scheduleMIDIEventBlock { - event.data.withUnsafeBufferPointer { ptr in - guard let ptr = ptr.baseAddress else { return } - midiBlock(AUEventSampleTimeImmediate + AUEventSampleTime(offset), 0, event.data.count, ptr) - } - } - } -#endif - - var isStarted: Bool { !bypassed } - func start() { bypassed = false } - func stop() { bypassed = true } - func play() { bypassed = false } - func bypass() { bypassed = true } - var outputFormat: AVAudioFormat { Settings.audioFormat } - - /// All parameters on the Node - var parameters: [NodeParameter] { - let mirror = Mirror(reflecting: self) - var params: [NodeParameter] = [] - - for child in mirror.children { - if let param = child.value as? ParameterBase { - params.append(param.projectedValue) - } - } - - return params - } - - /// Set up node parameters using reflection - func setupParameters() { - let mirror = Mirror(reflecting: self) - var params: [AUParameter] = [] - - for child in mirror.children { - if let param = child.value as? ParameterBase { - let def = param.projectedValue.def - let auParam = AUParameterTree.createParameter(identifier: def.identifier, - name: def.name, - address: def.address, - range: def.range, - unit: def.unit, - flags: def.flags) - params.append(auParam) - param.projectedValue.associate(with: avAudioNode, parameter: auParam) - } - } - - avAudioNode.auAudioUnit.parameterTree = AUParameterTree.createTree(withChildren: params) - } -} - -extension Node { - - func disconnectAV() { - if let engine = avAudioNode.engine { - engine.disconnectNodeInput(avAudioNode) - for (_, connection) in connections.enumerated() { - connection.disconnectAV() - } - } - } - - /// Work-around for an AVAudioEngine bug. - func initLastRenderTime() { - // We don't have a valid lastRenderTime until we query it. - _ = avAudioNode.lastRenderTime - - for connection in connections { - connection.initLastRenderTime() - } - } - - /// Scan for all parameters and associate with the node. - /// - Parameter node: AVAudioNode to associate - func associateParams(with node: AVAudioNode) { - let mirror = Mirror(reflecting: self) - - for child in mirror.children { - if let param = child.value as? ParameterBase { - param.projectedValue.associate(with: node) - } - } - } - - func makeAVConnections() { - if let node = self as? HasInternalConnections { - node.makeInternalConnections() - } - - // Are we attached? - if let engine = avAudioNode.engine { - for (bus, connection) in connections.enumerated() { - if let sourceEngine = connection.avAudioNode.engine { - if sourceEngine != avAudioNode.engine { - Log("🛑 Error: Attempt to connect nodes from different engines.") - return - } - } - - engine.attach(connection.avAudioNode) - - // Mixers will decide which input bus to use. - if let mixer = avAudioNode as? AVAudioMixerNode { - mixer.connectMixer(input: connection.avAudioNode, format: connection.outputFormat) - if let akMixer = self as? Mixer { - mixer.outputVolume = akMixer.volume - } - } else { - avAudioNode.connect(input: connection.avAudioNode, bus: bus, format: connection.outputFormat) - } - - connection.makeAVConnections() - } - } - } - - var bypassed: Bool { - get { avAudioNode.auAudioUnit.shouldBypassEffect } - set { avAudioNode.auAudioUnit.shouldBypassEffect = newValue } - } -} - -public protocol HasInternalConnections: AnyObject { - /// Override point for any connections internal to the node. - func makeInternalConnections() -} - -/// Protocol mostly to support DynamicOscillator in SoundpipeAudioKit, but could be used elsewhere -public protocol DynamicWaveformNode: Node { - /// Sets the wavetable - /// - Parameter waveform: The tablve - func setWaveform(_ waveform: Table) - - /// Gets the floating point values stored in the wavetable - func getWaveformValues() -> [Float] - /// Set the waveform change handler - /// - Parameter handler: Closure with an array of floats as the argument - func setWaveformUpdateHandler(_ handler: @escaping ([Float]) -> Void) + /// The underlying audio unit for the new engine. + var au: AUAudioUnit { get } } diff --git a/Sources/AudioKit/Nodes/NodeParameter.swift b/Sources/AudioKit/Nodes/NodeParameter.swift index 1523305cf4..9f83031324 100644 --- a/Sources/AudioKit/Nodes/NodeParameter.swift +++ b/Sources/AudioKit/Nodes/NodeParameter.swift @@ -49,7 +49,14 @@ public struct NodeParameterDef { /// NodeParameter wraps AUParameter in a user-friendly interface and adds some AudioKit-specific functionality. /// New version for use with Parameter property wrapper. public class NodeParameter { - public private(set) var avAudioNode: AVAudioNode! + + /// Due to Apple bugs, we need to set parameters using the V2 API. + /// + /// See https://github.com/AudioKit/AudioKit/issues/2528 + public private(set) var au: AudioUnit? + + /// For automating parameters. + public private(set) var auAudioUnit: AUAudioUnit? /// AU Parameter that this wraps public private(set) var parameter: AUParameter! @@ -63,8 +70,10 @@ public class NodeParameter { public var value: AUValue { get { parameter.value } set { - if let avAudioUnit = avAudioNode as? AVAudioUnit { - AudioUnitSetParameter(avAudioUnit.audioUnit, + if let au = au { + // Due to Apple bugs, we need to set parameters using the V2 API. + // See https://github.com/AudioKit/AudioKit/issues/2528 + AudioUnitSetParameter(au, param: AudioUnitParameterID(def.address), to: newValue.clamped(to: range)) } @@ -115,11 +124,13 @@ public class NodeParameter { return } assert(delaySamples < 4096) - let paramBlock = avAudioNode.auAudioUnit.scheduleParameterBlock - paramBlock(AUEventSampleTimeImmediate + Int64(delaySamples), - AUAudioFrameCount(duration * Float(Settings.sampleRate)), - parameter.address, - value.clamped(to: range)) + if let paramBlock = auAudioUnit?.scheduleParameterBlock { + paramBlock(AUEventSampleTimeImmediate + Int64(delaySamples), + AUAudioFrameCount(duration * Float(Settings.sampleRate)), + parameter.address, + value.clamped(to: range)) + } + } private var parameterObserverToken: AUParameterObserverToken? @@ -154,7 +165,8 @@ public class NodeParameter { /// - Parameters: /// - avAudioNode: AVAudioUnit to associate with public func associate(with avAudioNode: AVAudioNode) { - self.avAudioNode = avAudioNode + self.au = (avAudioNode as? AVAudioUnit)?.audioUnit + self.auAudioUnit = avAudioNode.auAudioUnit guard let tree = avAudioNode.auAudioUnit.parameterTree else { fatalError("No parameter tree.") } @@ -167,7 +179,8 @@ public class NodeParameter { /// - avAudioNode: AVAudioUnit to associate with /// - parameter: Parameter to associate public func associate(with avAudioNode: AVAudioNode, parameter: AUParameter) { - self.avAudioNode = avAudioNode + self.au = (avAudioNode as? AVAudioUnit)?.audioUnit + self.auAudioUnit = avAudioNode.auAudioUnit self.parameter = parameter } diff --git a/Sources/AudioKit/Nodes/Playback/Apple Sampler/AppleSampler.swift b/Sources/AudioKit/Nodes/Playback/Apple Sampler/AppleSampler.swift index 49102846c5..41cdd89cd7 100644 --- a/Sources/AudioKit/Nodes/Playback/Apple Sampler/AppleSampler.swift +++ b/Sources/AudioKit/Nodes/Playback/Apple Sampler/AppleSampler.swift @@ -43,13 +43,13 @@ open class AppleSampler: Node { public var avAudioNode: AVAudioNode { samplerUnit } /// Output Amplitude. Range: -90.0 -> +12 db, Default: 0 db - public var amplitude: AUValue = 0 { didSet { samplerUnit.masterGain = Float(amplitude) } } + public var amplitude: AUValue = 0 { didSet { samplerUnit.overallGain = Float(amplitude) } } /// Normalized Output Volume. Range: 0 -> 1, Default: 1 public var volume: AUValue = 1 { didSet { let newGain = volume.denormalized(to: -90.0 ... 0.0) - samplerUnit.masterGain = Float(newGain) + samplerUnit.overallGain = Float(newGain) } } diff --git a/Sources/AudioKit/Nodes/Playback/Sampler.swift b/Sources/AudioKit/Nodes/Playback/Sampler.swift new file mode 100644 index 0000000000..5946937b98 --- /dev/null +++ b/Sources/AudioKit/Nodes/Playback/Sampler.swift @@ -0,0 +1,290 @@ +// Copyright AudioKit. All Rights Reserved. Revision History at http://github.com/AudioKit/AudioKit/ + +import Foundation +import AudioUnit +import AVFoundation + +extension AudioBuffer { + func clear() { + bzero(mData, Int(mDataByteSize)) + } + + var frameCapacity: AVAudioFrameCount { + mDataByteSize / UInt32(MemoryLayout.size) + } +} + +enum SamplerCommand { + + /// Play a sample immediately + case playSample(UnsafeMutablePointer?) + + /// Assign a sample to a midi note number. + case assignSample(UnsafeMutablePointer?, UInt8) + + /// Stop all playback + case stop +} + +/// Renders contents of a file +class SamplerAudioUnit: AUAudioUnit { + + private var inputBusArray: AUAudioUnitBusArray! + private var outputBusArray: AUAudioUnitBusArray! + + let inputChannelCount: NSNumber = 2 + let outputChannelCount: NSNumber = 2 + + var cachedMIDIBlock: AUScheduleMIDIEventBlock? + + /// Returns an available voice. Audio thread ONLY. + func getVoice() -> Int? { + + // Linear search to find a voice. This could be better + // using a free list but we're lazy. + for index in 0...allocate(capacity: 1) + + holder.initialize(to: SampleHolder(pcmBuffer: sample, + bufferList: .init(sample.mutableAudioBufferList))) + + let command: SamplerCommand = .assignSample(holder, midiNote) + let sysex = encodeSysex(command) + + if cachedMIDIBlock == nil { + cachedMIDIBlock = scheduleMIDIEventBlock + assert(cachedMIDIBlock != nil) + } + + if let block = cachedMIDIBlock { + block(.zero, 0, sysex.count, sysex) + } + + } + + func stop() { + + let command: SamplerCommand = .stop + let sysex = encodeSysex(command) + + if cachedMIDIBlock == nil { + cachedMIDIBlock = scheduleMIDIEventBlock + assert(cachedMIDIBlock != nil) + } + + if let block = cachedMIDIBlock { + block(.zero, 0, sysex.count, sysex) + } + } + + /// Play a sample immediately. + func play(_ sample: AVAudioPCMBuffer) { + + let holder = UnsafeMutablePointer.allocate(capacity: 1) + + holder.initialize(to: SampleHolder(pcmBuffer: sample, + bufferList: .init(sample.mutableAudioBufferList))) + + let command: SamplerCommand = .playSample(holder) + let sysex = encodeSysex(command) + + if cachedMIDIBlock == nil { + cachedMIDIBlock = scheduleMIDIEventBlock + assert(cachedMIDIBlock != nil) + } + + if let block = cachedMIDIBlock { + block(.zero, 0, sysex.count, sysex) + } + + } + + func playMIDINote(_ noteNumber: UInt8) { + if cachedMIDIBlock == nil { + cachedMIDIBlock = scheduleMIDIEventBlock + assert(cachedMIDIBlock != nil) + } + + if let block = cachedMIDIBlock { + block(.zero, 0, 3, [0x90, noteNumber, 127]) + } + } + + /// Free buffers which have been played. + func collect() { +// for index in 0.., + timeStamp: UnsafePointer, + frameCount: AUAudioFrameCount, + outputBusNumber: Int, + outputBufferList: UnsafeMutablePointer, + renderEvents: UnsafePointer?, + inputBlock: AURenderPullInputBlock?) in + + process(events: renderEvents, + midi: { event in + let data = event.pointee.data + let command = data.0 & 0xF0 + let noteNumber = data.1 + if command == noteOnByte { + if let buf = self.samples[Int(noteNumber)] { + self.play(buf) + } + } else if command == noteOffByte { + // XXX: ignore for now + } + }, + sysex: { event in + var command: SamplerCommand = .playSample(nil) + + decodeSysex(event, &command) + + switch command { + case .playSample(let ptr): + if let voiceIndex = self.getVoice() { + self.voices[voiceIndex].sample = ptr + + // XXX: shoudn't be calling frameLength here (ObjC call) + self.voices[voiceIndex].sampleFrames = Int(ptr!.pointee.pcmBuffer.frameLength) + self.voices[voiceIndex].playhead = 0 + } + + case .assignSample(let ptr, let noteNumber): + self.samples[Int(noteNumber)] = ptr!.pointee.pcmBuffer + case .stop: + for index in 0..? + + /// Number of frames in the buffer for sake of convenience. + var sampleFrames: Int = 0 + + /// Current frame we're playing. Could be negative to indicate number of frames to wait before playing. + var playhead: Int = 0 + + // Envelope state, etc. would go here. +} + +extension AudioBuffer { + subscript(index:Int) -> Float { + get { + return mData!.bindMemory(to: Float.self, capacity: Int(mDataByteSize) / MemoryLayout.size)[index] + } + set(newElm) { + mData!.bindMemory(to: Float.self, capacity: Int(mDataByteSize) / MemoryLayout.size)[index] = newElm + } + } +} + +extension SamplerVoice { + mutating func render(to outputPtr: UnsafeMutableAudioBufferListPointer, + frameCount: AVAudioFrameCount) { + if inUse, let sample = self.sample { + for frame in 0..= 0 && playhead < sampleFrames { + + let data = sample.pointee.bufferList + + for channel in 0 ..< data.count where channel < outputPtr.count { + outputPtr[channel][frame] += data[channel][playhead] + } + + } + + // Advance playhead by a frame. + playhead += 1 + + // Are we done playing? + if playhead >= sampleFrames { + inUse = false + sample.pointee.done = true + self.sample = nil + break + } + } + } + } +} diff --git a/Sources/AudioKit/Audio Files/AVAudioPCMBuffer+Utilities.swift b/Sources/Utilities/AVAudioPCMBuffer+Utilities.swift similarity index 100% rename from Sources/AudioKit/Audio Files/AVAudioPCMBuffer+Utilities.swift rename to Sources/Utilities/AVAudioPCMBuffer+Utilities.swift diff --git a/Sources/Utilities/AVAudioUnit+Helpers.swift b/Sources/Utilities/AVAudioUnit+Helpers.swift new file mode 100644 index 0000000000..9dfcdbb98e --- /dev/null +++ b/Sources/Utilities/AVAudioUnit+Helpers.swift @@ -0,0 +1,29 @@ +// Copyright AudioKit. All Rights Reserved. Revision History at http://github.com/AudioKit/AudioKit/ + +import AVFAudio + +/// Instantiate an AVAudioUnit. +public func instantiate(componentDescription: AudioComponentDescription) -> AVAudioUnit { + let semaphore = DispatchSemaphore(value: 0) + var result: AVAudioUnit! + AVAudioUnit.instantiate(with: componentDescription) { avAudioUnit, _ in + guard let au = avAudioUnit else { fatalError("Unable to instantiate AVAudioUnit") } + result = au + semaphore.signal() + } + _ = semaphore.wait(wallTimeout: .distantFuture) + return result +} + +/// Sometimes we don't want an AVAudioUnit. +public func instantiateAU(componentDescription: AudioComponentDescription) -> AUAudioUnit { + let semaphore = DispatchSemaphore(value: 0) + var result: AUAudioUnit! + AUAudioUnit.instantiate(with: componentDescription) { auAudioUnit, _ in + guard let au = auAudioUnit else { fatalError("Unable to instantiate AUAudioUnit") } + result = au + semaphore.signal() + } + _ = semaphore.wait(wallTimeout: .distantFuture) + return result +} diff --git a/Sources/AudioKit/Internals/Audio Unit/AudioComponentDescription+Helpers.swift b/Sources/Utilities/AudioComponentDescription+Helpers.swift similarity index 100% rename from Sources/AudioKit/Internals/Audio Unit/AudioComponentDescription+Helpers.swift rename to Sources/Utilities/AudioComponentDescription+Helpers.swift diff --git a/Sources/AudioKit/Internals/Utilities/AudioKitHelpers.swift b/Sources/Utilities/AudioKitHelpers.swift similarity index 94% rename from Sources/AudioKit/Internals/Utilities/AudioKitHelpers.swift rename to Sources/Utilities/AudioKitHelpers.swift index 537183a830..53d94b5e53 100644 --- a/Sources/AudioKit/Internals/Utilities/AudioKitHelpers.swift +++ b/Sources/Utilities/AudioKitHelpers.swift @@ -143,19 +143,17 @@ extension Sequence where Iterator.Element: Hashable { } } -@inline(__always) -internal func AudioUnitGetParameter(_ unit: AudioUnit, param: AudioUnitParameterID) -> AUValue { +public func AudioUnitGetParameter(_ unit: AudioUnit, param: AudioUnitParameterID) -> AUValue { var val: AudioUnitParameterValue = 0 AudioUnitGetParameter(unit, param, kAudioUnitScope_Global, 0, &val) return val } -@inline(__always) -internal func AudioUnitSetParameter(_ unit: AudioUnit, param: AudioUnitParameterID, to value: AUValue) { +public func AudioUnitSetParameter(_ unit: AudioUnit, param: AudioUnitParameterID, to value: AUValue) { AudioUnitSetParameter(unit, param, kAudioUnitScope_Global, 0, AudioUnitParameterValue(value), 0) } -extension AVAudioNode { +public extension AVAudioNode { var inputCount: Int { numberOfInputs } func inputConnections() -> [AVAudioConnectionPoint] { @@ -417,31 +415,8 @@ public extension DSPSplitComplex { public extension AVAudioTime { /// Returns an AVAudioTime set to sampleTime of zero at the default sample rate - static func sampleTimeZero(sampleRate: Double = Settings.sampleRate) -> AVAudioTime { + static func sampleTimeZero(sampleRate: Double = 44100) -> AVAudioTime { let sampleTime = AVAudioFramePosition(Double(0)) return AVAudioTime(sampleTime: sampleTime, atRate: sampleRate) } } - -// Protocols used in AudioKit demos - -/// Protocol prescribing that something has an audio "player" -public protocol ProcessesPlayerInput: HasAudioEngine { - var player: AudioPlayer { get } -} - -/// Protocol prescribing that something ahs an audio "engine" -public protocol HasAudioEngine { - var engine: AudioEngine { get } -} - -/// Basic start and stop functionality -public extension HasAudioEngine { - func start() { - do { try engine.start() } catch let err { Log(err) } - } - - func stop() { - engine.stop() - } -} diff --git a/Sources/AudioKit/Internals/Audio Unit/AudioUnit+Helpers.swift b/Sources/Utilities/AudioUnit+Helpers.swift similarity index 100% rename from Sources/AudioKit/Internals/Audio Unit/AudioUnit+Helpers.swift rename to Sources/Utilities/AudioUnit+Helpers.swift diff --git a/Sources/AudioKit/Internals/Utilities/Log.swift b/Sources/Utilities/Log.swift similarity index 97% rename from Sources/AudioKit/Internals/Utilities/Log.swift rename to Sources/Utilities/Log.swift index 95469129ec..b5aab9c12b 100644 --- a/Sources/AudioKit/Internals/Utilities/Log.swift +++ b/Sources/Utilities/Log.swift @@ -38,8 +38,6 @@ public func Log(_ items: Any?..., function: String = #function, line: Int = #line) { - guard Settings.enableLogging else { return } - let fileName = (file as NSString).lastPathComponent let content = (items.map { String(describing: $0 ?? "nil") diff --git a/Tests/AudioKitTests/AudioEngineTests.swift b/Tests/AudioKitTests/AudioEngineTests.swift new file mode 100644 index 0000000000..b02b57025e --- /dev/null +++ b/Tests/AudioKitTests/AudioEngineTests.swift @@ -0,0 +1,207 @@ +// Copyright AudioKit. All Rights Reserved. Revision History at http://github.com/AudioKit/AudioKit/ +import AudioKit +import AVFoundation +import XCTest + +class AudioEngineTests: XCTestCase { + // Changing Settings.audioFormat will change subsequent node connections + // from 44_100 which the MD5's were created with so be sure to change it back at the end of a test + + func testEngineSampleRateGraphConsistency() { + let previousFormat = Settings.audioFormat + + let newRate: Double = 48000 + guard let newAudioFormat = AVAudioFormat(standardFormatWithSampleRate: newRate, + channels: 2) else { + XCTFail("Failed to create format at \(newRate)") + return + } + + if newAudioFormat != Settings.audioFormat { + Log("Changing audioFormat to", newAudioFormat) + Settings.audioFormat = newAudioFormat + } + + let engine = AudioEngine() + let input = AudioPlayer(url: URL.testAudio)! + let mixer = Mixer(input) + + // assign input and engine references + engine.output = mixer + + let mixerSampleRate = mixer.avAudioNode.outputFormat(forBus: 0).sampleRate + let mainMixerNodeSampleRate = engine.mainMixerNode?.avAudioNode.outputFormat(forBus: 0).sampleRate + let inputSampleRate = input.avAudioNode.outputFormat(forBus: 0).sampleRate + + XCTAssertTrue(mixerSampleRate == newRate, + "mixerSampleRate is \(mixerSampleRate), requested rate was \(newRate)") + + XCTAssertTrue(mainMixerNodeSampleRate == newRate, + "mainMixerNodeSampleRate is \(mixerSampleRate), requested rate was \(newRate)") + + XCTAssertTrue(inputSampleRate == newRate, + "oscSampleRate is \(inputSampleRate), requested rate was \(newRate)") + + Log(engine.avEngine.description) + + // restore + Settings.audioFormat = previousFormat + } + + func testEngineSampleRateChanged() { + let previousFormat = Settings.audioFormat + + guard let audioFormat441k = AVAudioFormat(standardFormatWithSampleRate: 44100, channels: 2) else { + XCTFail("Failed to create format at 44.1k") + return + } + guard let audioFormat48k = AVAudioFormat(standardFormatWithSampleRate: 48000, channels: 2) else { + XCTFail("Failed to create format at 48k") + return + } + + Settings.audioFormat = audioFormat441k + let engine = AudioEngine() + let node1 = Mixer() + engine.output = node1 + + guard let mainMixerNode1 = engine.mainMixerNode else { + XCTFail("mainMixerNode1 wasn't created") + return + } + let mainMixerNodeSampleRate1 = mainMixerNode1.avAudioNode.outputFormat(forBus: 0).sampleRate + XCTAssertTrue(mainMixerNodeSampleRate1 == audioFormat441k.sampleRate, + "mainMixerNodeSampleRate is \(mainMixerNodeSampleRate1), requested rate was \(audioFormat441k.sampleRate)") + + Log("44100", engine.avEngine.description) + + Settings.audioFormat = audioFormat48k + let node2 = Mixer() + engine.output = node2 + + guard let mainMixerNode2 = engine.mainMixerNode else { + XCTFail("mainMixerNode2 wasn't created") + return + } + let mainMixerNodeSampleRate2 = mainMixerNode2.avAudioNode.outputFormat(forBus: 0).sampleRate + XCTAssertTrue(mainMixerNodeSampleRate2 == audioFormat48k.sampleRate, + "mainMixerNodeSampleRate2 is \(mainMixerNodeSampleRate2), requested rate was \(audioFormat48k.sampleRate)") + + Log("48000", engine.avEngine.description) + + // restore + Log("Restoring global sample rate to", previousFormat.sampleRate) + Settings.audioFormat = previousFormat + } + + func testEngineMainMixerCreated() { + let engine = AudioEngine() + let input = AudioPlayer(url: URL.testAudio)! + engine.output = input + + guard let mainMixerNode = engine.mainMixerNode else { + XCTFail("mainMixerNode wasn't created") + return + } + let isConnected = mainMixerNode.hasInput(input) + + XCTAssertTrue(isConnected, "AudioPlayer isn't in the mainMixerNode's inputs") + } + + /* + func testEngineSwitchOutputWhileRunning() { + let engine = AudioEngine() + let url1 = Bundle.module.url(forResource: "12345", withExtension: "wav", subdirectory: "TestResources")! + let input1 = AudioPlayer(url: url1)! + let url2 = Bundle.module.url(forResource: "drumloop", withExtension: "wav", subdirectory: "TestResources")! + let input2 = AudioPlayer(url: url2)! + engine.output = input1 + + do { + try engine.start() + } catch let error as NSError { + Log(error, type: .error) + XCTFail("Failed to start engine") + } + + XCTAssertTrue(engine.avEngine.isRunning, "engine isn't running") + input1.start() + + // sleep(1) // for simple realtime check + + // change the output - will stop the engine + engine.output = input2 + + // is it started again? + XCTAssertTrue(engine.avEngine.isRunning) + + input2.start() + + // sleep(1) // for simple realtime check + + engine.stop() + } + */ + +// func testConnectionTreeDescriptionForNilMainMixerNode() { +// let engine = AudioEngine() +// XCTAssertEqual(engine.connectionTreeDescription, "\(connectionTreeLinePrefix)mainMixerNode is nil") +// } +// +// func testConnectionTreeDescriptionForSingleNodeAdded() { +// let engine = AudioEngine() +// let input = AudioPlayer(url: URL.testAudio)! +// engine.output = input +// XCTAssertEqual(engine.connectionTreeDescription, +// """ +// \(connectionTreeLinePrefix)↳Mixer("AudioKit Engine Mixer") +// \(connectionTreeLinePrefix) ↳AudioPlayer +// """) +// } +// +// func testConnectionTreeDescriptionForMixerWithName() { +// let engine = AudioEngine() +// let mixerName = "MixerNameFoo" +// let mixerWithName = Mixer(name: mixerName) +// engine.output = mixerWithName +// XCTAssertEqual(engine.connectionTreeDescription, +// """ +// \(connectionTreeLinePrefix)↳Mixer("AudioKit Engine Mixer") +// \(connectionTreeLinePrefix) ↳Mixer("\(mixerName)") +// """) +// } +// +// func testConnectionTreeDescriptionForMixerWithoutName() { +// let engine = AudioEngine() +// let mixerWithoutName = Mixer() +// engine.output = mixerWithoutName +// let addressOfMixerWithoutName = MemoryAddress(of: mixerWithoutName) +// XCTAssertEqual(engine.connectionTreeDescription, +// """ +// \(connectionTreeLinePrefix)↳Mixer("AudioKit Engine Mixer") +// \(connectionTreeLinePrefix) ↳Mixer("\(addressOfMixerWithoutName)") +// """) +// } + + #if os(macOS) + func testAudioDevices() { + XCTAssert(AudioEngine.devices.count > 0) + } + #endif + + func testOutputDevices() { + XCTAssert(AudioEngine.outputDevices.count > 0) + } + + func testInputDevices() { + XCTAssert(AudioEngine.inputDevices.count > 0) + } + + func testFindAudioUnit() { + let engine = AudioEngine() + let delayAVAudioUnit = engine.findAudioUnit(named: "AUDelay") + XCTAssertNotNil(delayAVAudioUnit) + let unknownAVAudioUnit = engine.findAudioUnit(named: "su·per·ca·li·fra·gil·is·tic·ex·pi·a·li·do·cious") + XCTAssertNil(unknownAVAudioUnit) + } +} diff --git a/Tests/AudioKitTests/EngineRealtimeTests.swift b/Tests/AudioKitTests/EngineRealtimeTests.swift new file mode 100644 index 0000000000..ce96b5305e --- /dev/null +++ b/Tests/AudioKitTests/EngineRealtimeTests.swift @@ -0,0 +1,169 @@ +// Copyright AudioKit. All Rights Reserved. Revision History at http://github.com/AudioKit/AudioKit/ +import AudioKit +import AVFoundation +import XCTest + +class EngineRelatimeTests: XCTestCase { + + func testBasicRealtime() throws { + let engine = Engine() + + let osc = TestOscillator() + + engine.output = osc + try! engine.start() + + sleep(1) + } + + + func testEffectRealtime() throws { + + let engine = Engine() + + let osc = TestOscillator() + let fx = AppleDistortion(osc) + + engine.output = fx + try engine.start() + + sleep(1) + } + + func testTwoEffectsRealtime() throws { + + let engine = Engine() + + let osc = TestOscillator() + let dist = AppleDistortion(osc) + let rev = Reverb(dist) + + engine.output = rev + + try engine.start() + + sleep(1) + } + + /// Test changing the output chain on the fly. + func testDynamicChangeRealtime() throws { + + let engine = Engine() + + let osc = TestOscillator() + let dist = AppleDistortion(osc) + + engine.output = osc + try engine.start() + + sleep(1) + + engine.output = dist + + sleep(1) + } + + func testMixerRealtime() throws { + + let engine = Engine() + + let osc1 = TestOscillator() + let osc2 = TestOscillator() + osc2.frequency = 466.16 // dissonance, so we can really hear it + + let mix = Mixer([osc1, osc2]) + + engine.output = mix + + try engine.start() + + sleep(1) + } + + func testMixerDynamicRealtime() throws { + + let engine = Engine() + + let osc1 = TestOscillator() + let osc2 = TestOscillator() + osc2.frequency = 466.16 // dissonance, so we can really hear it + + let mix = Mixer([osc1]) + + engine.output = mix + + try engine.start() + + sleep(1) + + mix.addInput(osc2) + + sleep(1) + } + + func testMultipleChangesRealtime() throws { + + let engine = Engine() + + let osc1 = TestOscillator() + let osc2 = TestOscillator() + + osc1.frequency = 880 + + engine.output = osc1 + + try engine.start() + + for i in 0..<10 { + sleep(1) + engine.output = (i % 2 == 1) ? osc1 : osc2 + } + } + + func testSamplerRealtime() throws { + let engine = Engine() + let url = URL.testAudio + let buffer = try! AVAudioPCMBuffer(url: url)! + let sampler = Sampler() + + engine.output = sampler + try engine.start() + sampler.play() + sleep(1) + sampler.play(buffer) + sleep(2) + } + + func testManyOscillators() throws { + let engine = Engine() + + let mixer = Mixer() + + for _ in 0..<100 { + mixer.addInput(TestOscillator()) + } + + mixer.volume = 0.001 + engine.output = mixer + + try engine.start() + sleep(2) + } + + func testManyOscillatorsOld() throws { + let engine = AudioEngine() + + let mixer = Mixer() + + for _ in 0..<100 { + mixer.addInput(TestOscillator()) + } + + mixer.volume = 0.001 + engine.output = mixer + + try engine.start() + sleep(2) + } + +} diff --git a/Tests/AudioKitTests/EngineTests.swift b/Tests/AudioKitTests/EngineTests.swift index 26400db101..9c42f8e695 100644 --- a/Tests/AudioKitTests/EngineTests.swift +++ b/Tests/AudioKitTests/EngineTests.swift @@ -1,210 +1,337 @@ // Copyright AudioKit. All Rights Reserved. Revision History at http://github.com/AudioKit/AudioKit/ -@testable import AudioKit +import AudioKit import AVFoundation import XCTest class EngineTests: XCTestCase { - // Changing Settings.audioFormat will change subsequent node connections - // from 44_100 which the MD5's were created with so be sure to change it back at the end of a test + + func testBasic() throws { + let engine = Engine() + + let osc = TestOscillator() - func testEngineSampleRateGraphConsistency() { - let previousFormat = Settings.audioFormat + engine.output = osc - let newRate: Double = 48000 - guard let newAudioFormat = AVAudioFormat(standardFormatWithSampleRate: newRate, - channels: 2) else { - XCTFail("Failed to create format at \(newRate)") - return - } + let audio = engine.startTest(totalDuration: 1.0) + audio.append(engine.render(duration: 1.0)) - if newAudioFormat != Settings.audioFormat { - Log("Changing audioFormat to", newAudioFormat) - Settings.audioFormat = newAudioFormat - } + testMD5(audio) + } - let engine = AudioEngine() - let url = Bundle.module.url(forResource: "12345", withExtension: "wav", subdirectory: "TestResources")! - let input = AudioPlayer(url: url)! - let mixer = Mixer(input) + func testEffect() throws { + + let engine = Engine() + + let osc = TestOscillator() + let fx = AppleDistortion(osc) - // assign input and engine references - engine.output = mixer + engine.output = fx + + let audio = engine.startTest(totalDuration: 1.0) + audio.append(engine.render(duration: 1.0)) + + testMD5(audio) + } + + func testTwoEffects() throws { + + let engine = Engine() + + let osc = TestOscillator() + let dist = AppleDistortion(osc) + let rev = Reverb(dist) + + engine.output = rev + + let audio = engine.startTest(totalDuration: 1.0) + audio.append(engine.render(duration: 1.0)) + + testMD5(audio) + } + + /// Test changing the output chain on the fly. + func testDynamicChange() throws { + + let engine = Engine() + + let osc = TestOscillator() + let dist = AppleDistortion(osc) + + engine.output = osc + + let audio = engine.startTest(totalDuration: 2.0) + + audio.append(engine.render(duration: 1.0)) + + engine.output = dist + + audio.append(engine.render(duration: 1.0)) + + testMD5(audio) + } + + func testMixer() throws { + + let engine = Engine() + + let osc1 = TestOscillator() + let osc2 = TestOscillator() + osc2.frequency = 466.16 // dissonance, so we can really hear it + + let mix = Mixer([osc1, osc2]) + + engine.output = mix + + let audio = engine.startTest(totalDuration: 1.0) + audio.append(engine.render(duration: 1.0)) + + testMD5(audio) + } + + func testMixerVolume() throws { + + let engine = Engine() - let mixerSampleRate = mixer.avAudioNode.outputFormat(forBus: 0).sampleRate - let mainMixerNodeSampleRate = engine.mainMixerNode?.avAudioNode.outputFormat(forBus: 0).sampleRate - let inputSampleRate = input.avAudioNode.outputFormat(forBus: 0).sampleRate + let osc1 = TestOscillator() + let osc2 = TestOscillator() + osc2.frequency = 466.16 // dissonance, so we can really hear it - XCTAssertTrue(mixerSampleRate == newRate, - "mixerSampleRate is \(mixerSampleRate), requested rate was \(newRate)") + let mix = Mixer([osc1, osc2]) - XCTAssertTrue(mainMixerNodeSampleRate == newRate, - "mainMixerNodeSampleRate is \(mixerSampleRate), requested rate was \(newRate)") + // XXX: ensure we get the same output using AVAudioEngine + mix.volume = 0.02 - XCTAssertTrue(inputSampleRate == newRate, - "oscSampleRate is \(inputSampleRate), requested rate was \(newRate)") + engine.output = mix - Log(engine.avEngine.description) + let audio = engine.startTest(totalDuration: 1.0) + audio.append(engine.render(duration: 1.0)) - // restore - Settings.audioFormat = previousFormat + testMD5(audio) } - func testEngineSampleRateChanged() { - let previousFormat = Settings.audioFormat + func testMixerDynamic() throws { - guard let audioFormat441k = AVAudioFormat(standardFormatWithSampleRate: 44100, channels: 2) else { - XCTFail("Failed to create format at 44.1k") - return - } - guard let audioFormat48k = AVAudioFormat(standardFormatWithSampleRate: 48000, channels: 2) else { - XCTFail("Failed to create format at 48k") - return - } + let engine = Engine() - Settings.audioFormat = audioFormat441k - let engine = AudioEngine() - let node1 = Mixer() - engine.output = node1 + let osc1 = TestOscillator() + let osc2 = TestOscillator() + osc2.frequency = 466.16 // dissonance, so we can really hear it - guard let mainMixerNode1 = engine.mainMixerNode else { - XCTFail("mainMixerNode1 wasn't created") - return - } - let mainMixerNodeSampleRate1 = mainMixerNode1.avAudioNode.outputFormat(forBus: 0).sampleRate - XCTAssertTrue(mainMixerNodeSampleRate1 == audioFormat441k.sampleRate, - "mainMixerNodeSampleRate is \(mainMixerNodeSampleRate1), requested rate was \(audioFormat441k.sampleRate)") + let mix = Mixer([osc1]) - Log("44100", engine.avEngine.description) + engine.output = mix - Settings.audioFormat = audioFormat48k - let node2 = Mixer() - engine.output = node2 + let audio = engine.startTest(totalDuration: 2.0) - guard let mainMixerNode2 = engine.mainMixerNode else { - XCTFail("mainMixerNode2 wasn't created") - return - } - let mainMixerNodeSampleRate2 = mainMixerNode2.avAudioNode.outputFormat(forBus: 0).sampleRate - XCTAssertTrue(mainMixerNodeSampleRate2 == audioFormat48k.sampleRate, - "mainMixerNodeSampleRate2 is \(mainMixerNodeSampleRate2), requested rate was \(audioFormat48k.sampleRate)") + audio.append(engine.render(duration: 1.0)) - Log("48000", engine.avEngine.description) + mix.addInput(osc2) - // restore - Log("Restoring global sample rate to", previousFormat.sampleRate) - Settings.audioFormat = previousFormat - } + audio.append(engine.render(duration: 1.0)) - func testEngineMainMixerCreated() { - let engine = AudioEngine() - let url = Bundle.module.url(forResource: "12345", withExtension: "wav", subdirectory: "TestResources")! - let input = AudioPlayer(url: url)! - engine.output = input + testMD5(audio) + } - guard let mainMixerNode = engine.mainMixerNode else { - XCTFail("mainMixerNode wasn't created") - return + func testMixerVolume2() throws { + + for volume in [0.0, 0.1, 0.5, 0.8, 1.0, 2.0] { + let audioEngine = AudioEngine() + let osc = TestOscillator() + let mix = Mixer(osc) + mix.volume = AUValue(volume) + audioEngine.output = mix + let audio = audioEngine.startTest(totalDuration: 1.0) + audio.append(audioEngine.render(duration: 1.0)) + + let engine = Engine() + let osc2 = TestOscillator() + let mix2 = Mixer(osc2) + mix2.volume = AUValue(volume) + engine.output = mix2 + let audio2 = engine.startTest(totalDuration: 1.0) + audio2.append(engine.render(duration: 1.0)) + + for i in 0.. 0) + func testPlaygroundOscillator() { + let engine = Engine() + let osc = PlaygroundOscillator() + engine.output = osc + let audio = engine.startTest(totalDuration: 2.0) + osc.play() + audio.append(engine.render(duration: 2.0)) + testMD5(audio) } - #endif - func testOutputDevices() { - XCTAssert(AudioEngine.outputDevices.count > 0) + func testSysexEncoding() { + + let value = 42 + let sysex = encodeSysex(value) + + XCTAssertEqual(sysex.count, 19) + + var decoded = 0 + decodeSysex(sysex, count: 19, &decoded) + + XCTAssertEqual(decoded, 42) } - func testInputDevices() { - XCTAssert(AudioEngine.inputDevices.count > 0) + func testManyOscillatorsPerf() throws { + let engine = Engine() + + let mixer = Mixer() + + for _ in 0..<100 { + mixer.addInput(TestOscillator()) + } + + mixer.volume = 0.001 + engine.output = mixer + + measure { + let audio = engine.startTest(totalDuration: 2.0) + audio.append(engine.render(duration: 2.0)) + } } - func testFindAudioUnit() { + func testManyOscillatorsOldPerf() throws { let engine = AudioEngine() - let delayAVAudioUnit = engine.findAudioUnit(named: "AUDelay") - XCTAssertNotNil(delayAVAudioUnit) - let unknownAVAudioUnit = engine.findAudioUnit(named: "su·per·ca·li·fra·gil·is·tic·ex·pi·a·li·do·cious") - XCTAssertNil(unknownAVAudioUnit) + + let mixer = Mixer() + + for _ in 0..<100 { + mixer.addInput(TestOscillator()) + } + + mixer.volume = 0.001 + engine.output = mixer + + measure { + let audio = engine.startTest(totalDuration: 2.0) + audio.append(engine.render(duration: 2.0)) + } } + } diff --git a/Tests/AudioKitTests/File Tests/FormatConverterTests.swift b/Tests/AudioKitTests/File Tests/FormatConverterTests.swift index 2857780d13..e88c71ff16 100644 --- a/Tests/AudioKitTests/File Tests/FormatConverterTests.swift +++ b/Tests/AudioKitTests/File Tests/FormatConverterTests.swift @@ -12,7 +12,7 @@ class FormatConverterTests: AudioFileTestCase { } var stereoWAVE44k16Bit: URL? { - Bundle.module.url(forResource: "12345", withExtension: "wav", subdirectory: "TestResources") + URL.testAudio } func testbitDepthRule() throws { diff --git a/Tests/AudioKitTests/Node Tests/Effects Tests/BypassTests.swift b/Tests/AudioKitTests/Node Tests/Effects Tests/BypassTests.swift index 691b877501..7061a3823c 100644 --- a/Tests/AudioKitTests/Node Tests/Effects Tests/BypassTests.swift +++ b/Tests/AudioKitTests/Node Tests/Effects Tests/BypassTests.swift @@ -1,10 +1,9 @@ // Copyright AudioKit. All Rights Reserved. Revision History at http://github.com/AudioKit/AudioKit/ import XCTest -@testable import AudioKit +import AudioKit import AVFAudio -@available(macOS 10.15, iOS 13.0, tvOS 13.0, *) class BypassTests: XCTestCase { let duration = 0.1 let source = ConstantGenerator(constant: 1) @@ -37,7 +36,7 @@ class BypassTests: XCTestCase { } func testStopEffectDoesntPerformAnyTransformation() throws { - let engine = AudioEngine() + let engine = Engine() for effect in effects { engine.output = effect @@ -51,7 +50,7 @@ class BypassTests: XCTestCase { } func testStartEffectPerformsTransformation() throws { - let engine = AudioEngine() + let engine = Engine() for effect in effects { engine.output = effect diff --git a/Tests/AudioKitTests/Node Tests/Effects Tests/CompressorTests.swift b/Tests/AudioKitTests/Node Tests/Effects Tests/CompressorTests.swift index e840a5c8da..a49a971cfa 100644 --- a/Tests/AudioKitTests/Node Tests/Effects Tests/CompressorTests.swift +++ b/Tests/AudioKitTests/Node Tests/Effects Tests/CompressorTests.swift @@ -2,64 +2,60 @@ import AudioKit import XCTest +import AVFoundation class CompressorTests: XCTestCase { func testAttackTime() { - let engine = AudioEngine() - let url = Bundle.module.url(forResource: "12345", withExtension: "wav", subdirectory: "TestResources")! - let player = AudioPlayer(url: url)! - engine.output = Compressor(player, attackTime: 0.1) + let engine = Engine() + let sampler = Sampler() + engine.output = Compressor(sampler, attackTime: 0.1) + sampler.play(url: URL.testAudio) let audio = engine.startTest(totalDuration: 1.0) - player.play() audio.append(engine.render(duration: 1.0)) testMD5(audio) } func testDefault() { - let engine = AudioEngine() - let url = Bundle.module.url(forResource: "12345", withExtension: "wav", subdirectory: "TestResources")! - let player = AudioPlayer(url: url)! - engine.output = Compressor(player) + let engine = Engine() + let sampler = Sampler() + engine.output = Compressor(sampler) let audio = engine.startTest(totalDuration: 1.0) - player.play() + sampler.play(url: URL.testAudio) audio.append(engine.render(duration: 1.0)) testMD5(audio) } func testHeadRoom() { - let engine = AudioEngine() - let url = Bundle.module.url(forResource: "12345", withExtension: "wav", subdirectory: "TestResources")! - let player = AudioPlayer(url: url)! - engine.output = Compressor(player, headRoom: 0) + let engine = Engine() + let sampler = Sampler() + engine.output = Compressor(sampler, headRoom: 0) let audio = engine.startTest(totalDuration: 1.0) - player.play() + sampler.play(url: URL.testAudio) audio.append(engine.render(duration: 1.0)) testMD5(audio) } func testMasterGain() { - let engine = AudioEngine() - let url = Bundle.module.url(forResource: "12345", withExtension: "wav", subdirectory: "TestResources")! - let player = AudioPlayer(url: url)! - engine.output = Compressor(player, masterGain: 1) + let engine = Engine() + let sampler = Sampler() + engine.output = Compressor(sampler, masterGain: 1) let audio = engine.startTest(totalDuration: 1.0) - player.play() + sampler.play(url: URL.testAudio) audio.append(engine.render(duration: 1.0)) testMD5(audio) } func testParameters() { - let engine = AudioEngine() - let url = Bundle.module.url(forResource: "12345", withExtension: "wav", subdirectory: "TestResources")! - let player = AudioPlayer(url: url)! - engine.output = Compressor(player, + let engine = Engine() + let sampler = Sampler() + engine.output = Compressor(sampler, threshold: -25, headRoom: 10, attackTime: 0.1, releaseTime: 0.1, masterGain: 1) let audio = engine.startTest(totalDuration: 1.0) - player.play() + sampler.play(url: URL.testAudio) audio.append(engine.render(duration: 1.0)) testMD5(audio) } @@ -67,12 +63,11 @@ class CompressorTests: XCTestCase { // Release time is not currently tested func testThreshold() { - let engine = AudioEngine() - let url = Bundle.module.url(forResource: "12345", withExtension: "wav", subdirectory: "TestResources")! - let player = AudioPlayer(url: url)! - engine.output = Compressor(player, threshold: -25) + let engine = Engine() + let sampler = Sampler() + engine.output = Compressor(sampler, threshold: -25) let audio = engine.startTest(totalDuration: 1.0) - player.play() + sampler.play(url: URL.testAudio) audio.append(engine.render(duration: 1.0)) testMD5(audio) } diff --git a/Tests/AudioKitTests/Node Tests/Effects Tests/DistortionTests.swift b/Tests/AudioKitTests/Node Tests/Effects Tests/DistortionTests.swift index c30b6a47e7..6af504a288 100644 --- a/Tests/AudioKitTests/Node Tests/Effects Tests/DistortionTests.swift +++ b/Tests/AudioKitTests/Node Tests/Effects Tests/DistortionTests.swift @@ -7,12 +7,11 @@ import AVFAudio class DistortionTests: XCTestCase { #if os(iOS) func testDefaultDistortion() { - let engine = AudioEngine() - let url = Bundle.module.url(forResource: "12345", withExtension: "wav", subdirectory: "TestResources")! - let input = AudioPlayer(url: url)! - engine.output = AppleDistortion(input) + let engine = Engine() + let sampler = Sampler() + engine.output = AppleDistortion(sampler) let audio = engine.startTest(totalDuration: 1.0) - input.start() + sampler.play(url: URL.testAudio) audio.append(engine.render(duration: 1.0)) // testMD5(audio) } diff --git a/Tests/AudioKitTests/Node Tests/Effects Tests/DynamicsProcessorTests.swift b/Tests/AudioKitTests/Node Tests/Effects Tests/DynamicsProcessorTests.swift index 41034558fd..548309378a 100644 --- a/Tests/AudioKitTests/Node Tests/Effects Tests/DynamicsProcessorTests.swift +++ b/Tests/AudioKitTests/Node Tests/Effects Tests/DynamicsProcessorTests.swift @@ -6,11 +6,10 @@ import XCTest class DynamicsProcessorTests: XCTestCase { func testDefault() throws { try XCTSkipIf(true, "TODO This test gives different results on local machines from what CI does") - let engine = AudioEngine() - let url = Bundle.module.url(forResource: "12345", withExtension: "wav", subdirectory: "TestResources")! - let input = AudioPlayer(url: url)! - engine.output = DynamicsProcessor(input) - input.start() + let engine = Engine() + let sampler = Sampler() + engine.output = DynamicsProcessor(sampler) + sampler.play(url: URL.testAudio) let audio = engine.startTest(totalDuration: 1.0) audio.append(engine.render(duration: 1.0)) testMD5(audio) diff --git a/Tests/AudioKitTests/Node Tests/Effects Tests/ExpanderTests.swift b/Tests/AudioKitTests/Node Tests/Effects Tests/ExpanderTests.swift index f0bebabca0..ca0d8bbc37 100644 --- a/Tests/AudioKitTests/Node Tests/Effects Tests/ExpanderTests.swift +++ b/Tests/AudioKitTests/Node Tests/Effects Tests/ExpanderTests.swift @@ -6,11 +6,10 @@ import XCTest class ExpanderTests: XCTestCase { func testDefault() throws { try XCTSkipIf(true, "TODO This test gives different results on local machines from what CI does") - let engine = AudioEngine() - let url = Bundle.module.url(forResource: "12345", withExtension: "wav", subdirectory: "TestResources")! - let input = AudioPlayer(url: url)! - engine.output = Expander(input) - input.start() + let engine = Engine() + let sampler = Sampler() + engine.output = Expander(sampler) + sampler.play(url: URL.testAudio) let audio = engine.startTest(totalDuration: 1.0) audio.append(engine.render(duration: 1.0)) testMD5(audio) diff --git a/Tests/AudioKitTests/Node Tests/Effects Tests/PeakLimiterTests.swift b/Tests/AudioKitTests/Node Tests/Effects Tests/PeakLimiterTests.swift index 9129891386..b7b404047b 100644 --- a/Tests/AudioKitTests/Node Tests/Effects Tests/PeakLimiterTests.swift +++ b/Tests/AudioKitTests/Node Tests/Effects Tests/PeakLimiterTests.swift @@ -5,84 +5,79 @@ import XCTest class PeakLimiterTests: XCTestCase { func testAttackTime() { - let engine = AudioEngine() - let url = Bundle.module.url(forResource: "12345", withExtension: "wav", subdirectory: "TestResources")! - let player = AudioPlayer(url: url)! - engine.output = PeakLimiter(player, attackTime: 0.02) + let engine = Engine() + let sampler = Sampler() + engine.output = PeakLimiter(sampler, attackTime: 0.02) let audio = engine.startTest(totalDuration: 1.0) - player.play() + sampler.play(url: URL.testAudio) audio.append(engine.render(duration: 1.0)) testMD5(audio) } func testDecayTime() throws { - let engine = AudioEngine() - let url = Bundle.module.url(forResource: "12345", withExtension: "wav", subdirectory: "TestResources")! - let player = AudioPlayer(url: url)! - player.volume = 5 // Had to be loud to allow for decay time to affected the sound - engine.output = PeakLimiter(player, decayTime: 0.02) + let engine = Engine() + let sampler = Sampler() + let mixer = Mixer(sampler) + mixer.volume = 5 // Had to be loud to allow for decay time to affected the sound + engine.output = PeakLimiter(mixer, decayTime: 0.02) let audio = engine.startTest(totalDuration: 1.0) - player.play() + sampler.play(url: URL.testAudio) audio.append(engine.render(duration: 1.0)) testMD5(audio) } func testDecayTime2() throws { - let engine = AudioEngine() - let url = Bundle.module.url(forResource: "12345", withExtension: "wav", subdirectory: "TestResources")! - let player = AudioPlayer(url: url)! - player.volume = 5 // Had to be loud to allow for decay time to affected the sound - engine.output = PeakLimiter(player, decayTime: 0.03) + let engine = Engine() + let sampler = Sampler() + let mixer = Mixer(sampler) + mixer.volume = 5 // Had to be loud to allow for decay time to affected the sound + engine.output = PeakLimiter(mixer, decayTime: 0.03) let audio = engine.startTest(totalDuration: 1.0) - player.play() + sampler.play(url: URL.testAudio) audio.append(engine.render(duration: 1.0)) testMD5(audio) } func testDefault() { - let engine = AudioEngine() - let url = Bundle.module.url(forResource: "12345", withExtension: "wav", subdirectory: "TestResources")! - let player = AudioPlayer(url: url)! - engine.output = PeakLimiter(player) + let engine = Engine() + let sampler = Sampler() + engine.output = PeakLimiter(sampler) let audio = engine.startTest(totalDuration: 1.0) - player.play() + sampler.play(url: URL.testAudio) audio.append(engine.render(duration: 1.0)) testMD5(audio) } func testParameters() { - let engine = AudioEngine() - let url = Bundle.module.url(forResource: "12345", withExtension: "wav", subdirectory: "TestResources")! - let player = AudioPlayer(url: url)! - engine.output = PeakLimiter(player, attackTime: 0.02, decayTime: 0.03, preGain: 1) + let engine = Engine() + let sampler = Sampler() + engine.output = PeakLimiter(sampler, attackTime: 0.02, decayTime: 0.03, preGain: 1) let audio = engine.startTest(totalDuration: 1.0) - player.play() + sampler.play(url: URL.testAudio) audio.append(engine.render(duration: 1.0)) testMD5(audio) } func testPreGain() { - let engine = AudioEngine() - let url = Bundle.module.url(forResource: "12345", withExtension: "wav", subdirectory: "TestResources")! - let player = AudioPlayer(url: url)! - engine.output = PeakLimiter(player, preGain: 1) + let engine = Engine() + let sampler = Sampler() + engine.output = PeakLimiter(sampler, preGain: 1) let audio = engine.startTest(totalDuration: 1.0) - player.play() + sampler.play(url: URL.testAudio) audio.append(engine.render(duration: 1.0)) testMD5(audio) } func testPreGainChangingAfterEngineStarted() throws { - let engine = AudioEngine() - let url = Bundle.module.url(forResource: "12345", withExtension: "wav", subdirectory: "TestResources")! - let player = AudioPlayer(url: url)! - let effect = PeakLimiter(player, attackTime: 0.02, decayTime: 0.03, preGain: -20) + let engine = Engine() + let sampler = Sampler() + let effect = PeakLimiter(sampler, attackTime: 0.02, decayTime: 0.03, preGain: -20) engine.output = effect let audio = engine.startTest(totalDuration: 2.0) - player.play() + sampler.play(url: URL.testAudio) audio.append(engine.render(duration: 1.0)) - player.stop() - player.play() + sampler.stop() + sampler.play(url: URL.testAudio) effect.preGain = 40 audio.append(engine.render(duration: 1.0)) testMD5(audio) diff --git a/Tests/AudioKitTests/Node Tests/Effects Tests/ReverbTests.swift b/Tests/AudioKitTests/Node Tests/Effects Tests/ReverbTests.swift index 49fedabef1..ec390d6a56 100644 --- a/Tests/AudioKitTests/Node Tests/Effects Tests/ReverbTests.swift +++ b/Tests/AudioKitTests/Node Tests/Effects Tests/ReverbTests.swift @@ -1,6 +1,6 @@ // Copyright AudioKit. All Rights Reserved. Revision History at http://github.com/AudioKit/AudioKit/ -@testable import AudioKit +import AudioKit import XCTest import AVFAudio @@ -9,14 +9,13 @@ class ReverbTests: XCTestCase { #if os(iOS) func testBypass() { - let engine = AudioEngine() - let url = Bundle.module.url(forResource: "12345", withExtension: "wav", subdirectory: "TestResources")! - let input = AudioPlayer(url: url)! + let engine = Engine() + let input = Sampler() let effect = Reverb(input) effect.bypass() engine.output = effect let audio = engine.startTest(totalDuration: 1.0) - input.start() + input.play(url: URL.testAudio) audio.append(engine.render(duration: 1.0)) testMD5(audio) } @@ -46,38 +45,35 @@ class ReverbTests: XCTestCase { } func testCathedral() { - let engine = AudioEngine() - let url = Bundle.module.url(forResource: "12345", withExtension: "wav", subdirectory: "TestResources")! - let input = AudioPlayer(url: url)! + let engine = Engine() + let input = Sampler() let effect = Reverb(input) engine.output = effect effect.loadFactoryPreset(.cathedral) let audio = engine.startTest(totalDuration: 1.0) - input.start() + input.play(url: URL.testAudio) audio.append(engine.render(duration: 1.0)) testMD5(audio) } func testDefault() { - let engine = AudioEngine() - let url = Bundle.module.url(forResource: "12345", withExtension: "wav", subdirectory: "TestResources")! - let input = AudioPlayer(url: url)! + let engine = Engine() + let input = Sampler() engine.output = Reverb(input) let audio = engine.startTest(totalDuration: 1.0) - input.start() + input.play(url: URL.testAudio) audio.append(engine.render(duration: 1.0)) testMD5(audio) } func testSmallRoom() { - let engine = AudioEngine() - let url = Bundle.module.url(forResource: "12345", withExtension: "wav", subdirectory: "TestResources")! - let input = AudioPlayer(url: url)! + let engine = Engine() + let input = Sampler() let effect = Reverb(input) engine.output = effect effect.loadFactoryPreset(.smallRoom) let audio = engine.startTest(totalDuration: 1.0) - input.start() + input.play(url: URL.testAudio) audio.append(engine.render(duration: 1.0)) testMD5(audio) } diff --git a/Tests/AudioKitTests/Node Tests/GenericNodeTests.swift b/Tests/AudioKitTests/Node Tests/GenericNodeTests.swift index 3c80654014..cbfb542249 100644 --- a/Tests/AudioKitTests/Node Tests/GenericNodeTests.swift +++ b/Tests/AudioKitTests/Node Tests/GenericNodeTests.swift @@ -1,6 +1,6 @@ // Copyright AudioKit. All Rights Reserved. Revision History at http://github.com/AudioKit/AudioKit/ -@testable import AudioKit +import AudioKit import AVFoundation import Foundation import GameplayKit @@ -22,7 +22,7 @@ class GenericNodeTests: XCTestCase { let rng = GKMersenneTwisterRandomSource(seed: 0) let duration = 10 - let engine = AudioEngine() + let engine = Engine() var bigBuffer: AVAudioPCMBuffer? for _ in 0 ..< duration { @@ -50,13 +50,13 @@ class GenericNodeTests: XCTestCase { } func nodeParameterTest(md5: String, factory: (Node) -> Node, m1MD5: String = "", audition: Bool = false) { - let url = Bundle.module.url(forResource: "12345", withExtension: "wav", subdirectory: "TestResources")! - let player = AudioPlayer(url: url)! - let node = factory(player) + let sampler = Sampler() + sampler.play(url: URL.testAudio) + let node = factory(sampler) let duration = node.parameters.count + 1 - let engine = AudioEngine() + let engine = Engine() var bigBuffer: AVAudioPCMBuffer? engine.output = node @@ -64,8 +64,6 @@ class GenericNodeTests: XCTestCase { /// Do the default parameters first if bigBuffer == nil { let audio = engine.startTest(totalDuration: 1.0) - player.play() - player.isLooping = true audio.append(engine.render(duration: 1.0)) bigBuffer = AVAudioPCMBuffer(pcmFormat: audio.format, frameCapacity: audio.frameLength * UInt32(duration)) @@ -73,7 +71,7 @@ class GenericNodeTests: XCTestCase { } for i in 0 ..< node.parameters.count { - let node = factory(player) + let node = factory(sampler) engine.output = node let param = node.parameters[i] @@ -101,18 +99,18 @@ class GenericNodeTests: XCTestCase { @available(macOS 10.15, iOS 13.0, tvOS 13.0, *) func testGenerators() { - nodeParameterTest(md5: "0118dbf3e33bc3052f2e375f06793c5f", factory: { _ in let osc = PlaygroundOscillator(waveform: Table(.square)); osc.play(); return osc }) - nodeParameterTest(md5: "789c1e77803a4f9d10063eb60ca03cea", factory: { _ in let osc = PlaygroundOscillator(waveform: Table(.triangle)); osc.play(); return osc }) - nodeParameterTest(md5: "8d1ece9eb2417d9da48f5ae796a33ac2", factory: { _ in let osc = PlaygroundOscillator(waveform: Table(.triangle), amplitude: 0.1); osc.play(); return osc }) + nodeParameterTest(md5: "1395270f613ccd7adc6a14eca3c5267b", factory: { _ in let osc = PlaygroundOscillator(waveform: Table(.square)); osc.play(); return osc }) + nodeParameterTest(md5: "9c1146981e940074bbbf63f1c2dd3896", factory: { _ in let osc = PlaygroundOscillator(waveform: Table(.triangle)); osc.play(); return osc }) + nodeParameterTest(md5: "870d5e9ea9133b43b8bbb91ca460c4ed", factory: { _ in let osc = PlaygroundOscillator(waveform: Table(.triangle), amplitude: 0.1); osc.play(); return osc }) } func testEffects() { - nodeParameterTest(md5: "d15c926f3da74630f986f7325adf044c", factory: { input in Compressor(input) }) - nodeParameterTest(md5: "ddfea2413fac59b7cdc71f1b8ed733a2", factory: { input in Decimator(input) }) - nodeParameterTest(md5: "d12817d8f84dfee6380030c5ddf7916b", factory: { input in Delay(input, time: 0.01) }) - nodeParameterTest(md5: "583791002739d735fba13f6bac48dba6", factory: { input in Distortion(input) }) - nodeParameterTest(md5: "0ae9a6b248486f343c55bf0818c3007d", factory: { input in PeakLimiter(input) }) - nodeParameterTest(md5: "b31ce15bb38716fd95070d1299679d3a", factory: { input in RingModulator(input) }) + nodeParameterTest(md5: "d8f308e27f019492714aad5c6e2d4520", factory: { input in Compressor(input) }) + nodeParameterTest(md5: "627d50441819d00c2fa4037e24807966", factory: { input in Decimator(input) }) + nodeParameterTest(md5: "dec105c6e2e44556608c9f393e205c1e", factory: { input in Delay(input, time: 0.01) }) + nodeParameterTest(md5: "3979c710eff8e12f0c3f535987624fde", factory: { input in Distortion(input) }) + nodeParameterTest(md5: "d65f43bda68342d9a53a5e9eda7ad36d", factory: { input in PeakLimiter(input) }) + nodeParameterTest(md5: "950411a6dcd15bb8bb424a7dc5b93dac", factory: { input in RingModulator(input) }) #if os(iOS) nodeParameterTest(md5: "28d2cb7a5c1e369ca66efa8931d31d4d", factory: { player in Reverb(player) }) @@ -124,9 +122,9 @@ class GenericNodeTests: XCTestCase { } func testFilters() { - nodeParameterTest(md5: "03e7b02e4fceb5fe6a2174740eda7e36", factory: { input in HighPassFilter(input) }) - nodeParameterTest(md5: "af137ecbe57e669340686e9721a2d1f2", factory: { input in HighShelfFilter(input) }) - nodeParameterTest(md5: "a43c821e13efa260d88d522b4d29aa45", factory: { input in LowPassFilter(input) }) - nodeParameterTest(md5: "2007d443458f8536b854d111aae4b51b", factory: { input in LowShelfFilter(input) }) + nodeParameterTest(md5: "befc21e17a65f32169c8b0efb15ea75c", factory: { input in HighPassFilter(input) }) + nodeParameterTest(md5: "69926231aedb80c4bd9ad8c27e2738b8", factory: { input in HighShelfFilter(input) }) + nodeParameterTest(md5: "aa3f867e12cf44b80d8142ebd0dc00a5", factory: { input in LowPassFilter(input) }) + nodeParameterTest(md5: "8bcb9c497515412afae7ae3bd2cc7b62", factory: { input in LowShelfFilter(input) }) } } diff --git a/Tests/AudioKitTests/Node Tests/Mixing Tests/MatrixMixerTests.swift b/Tests/AudioKitTests/Node Tests/Mixing Tests/MatrixMixerTests.swift index aaecc9e5a1..b70e9a14de 100644 --- a/Tests/AudioKitTests/Node Tests/Mixing Tests/MatrixMixerTests.swift +++ b/Tests/AudioKitTests/Node Tests/Mixing Tests/MatrixMixerTests.swift @@ -4,6 +4,9 @@ import XCTest import AudioKit import AVFAudio +// We're not supporting multichannel audio in the new v6 engine at first. +#if false + @available(iOS 13.0, *) class MatrixMixerTests: XCTestCase { let engine = AudioEngine() @@ -81,3 +84,5 @@ class MatrixMixerTests: XCTestCase { XCTAssertTrue(output1.allSatisfy { $0 == 0 }) } } + +#endif diff --git a/Tests/AudioKitTests/Node Tests/Mixing Tests/MixerTests.swift b/Tests/AudioKitTests/Node Tests/Mixing Tests/MixerTests.swift index e410931e36..bb21e4204e 100644 --- a/Tests/AudioKitTests/Node Tests/Mixing Tests/MixerTests.swift +++ b/Tests/AudioKitTests/Node Tests/Mixing Tests/MixerTests.swift @@ -6,25 +6,22 @@ import XCTest class MixerTests: XCTestCase { func testSplitConnection() { - let engine = AudioEngine() - let url = Bundle.module.url(forResource: "12345", withExtension: "wav", subdirectory: "TestResources")! - let player = AudioPlayer(url: url)! - let mixer1 = Mixer(player) + let engine = Engine() + let sampler = Sampler() + let mixer1 = Mixer(sampler) let mixer2 = Mixer() engine.output = Mixer(mixer1, mixer2) let audio = engine.startTest(totalDuration: 1.0) - player.play() + sampler.play(url: URL.testAudio) audio.append(engine.render(duration: 1.0)) - mixer2.addInput(player) - mixer2.removeInput(player) - mixer2.addInput(player) + mixer2.addInput(sampler) + mixer2.removeInput(sampler) + mixer2.addInput(sampler) testMD5(audio) } -} -extension MixerTests { func testWiringAfterEngineStart() { - let engine = AudioEngine() + let engine = Engine() let engineMixer = Mixer() engine.output = engineMixer @@ -33,15 +30,13 @@ extension MixerTests { let subtreeMixer = Mixer() engineMixer.addInput(subtreeMixer) - let url = Bundle.module.url(forResource: "12345", withExtension: "wav", subdirectory: "TestResources")! - let player = AudioPlayer(url: url)! - subtreeMixer.addInput(player) + let sampler = Sampler() + subtreeMixer.addInput(sampler) - print(engine.connectionTreeDescription) - player.play() + sampler.play(url: URL.testAudio) // only for auditioning - // wait(for: player.duration) + // wait(for: 2.0) engine.stop() } @@ -59,14 +54,13 @@ extension MixerTests { let engineMixer = Mixer() engine.output = engineMixer - let url = Bundle.module.url(forResource: "12345", withExtension: "wav", subdirectory: "TestResources")! - let player = AudioPlayer(url: url)! + let sampler = Sampler() let mixerA = Mixer(volume: 0.5, name: "mixerA") - mixerA.addInput(player) + mixerA.addInput(sampler) engineMixer.addInput(mixerA) - let mixerB = Mixer(player, name: "mixerB") + let mixerB = Mixer(sampler, name: "mixerB") mixerB.volume = 0.5 engineMixer.addInput(mixerB) diff --git a/Tests/AudioKitTests/Node Tests/NodeRecorderTests.swift b/Tests/AudioKitTests/Node Tests/NodeRecorderTests.swift index 42df5981f0..fa6e4279e0 100644 --- a/Tests/AudioKitTests/Node Tests/NodeRecorderTests.swift +++ b/Tests/AudioKitTests/Node Tests/NodeRecorderTests.swift @@ -1,5 +1,5 @@ // Copyright AudioKit. All Rights Reserved. Revision History at http://github.com/AudioKit/AudioKit/ -@testable import AudioKit +import AudioKit import AVFoundation import XCTest @@ -7,15 +7,14 @@ class NodeRecorderTests: XCTestCase { func testBasicRecord() throws { return // for now, tests are failing - let engine = AudioEngine() - let url = Bundle.module.url(forResource: "12345", withExtension: "wav", subdirectory: "TestResources")! - let player = AudioPlayer(url: url)! - engine.output = player - let recorder = try NodeRecorder(node: player) + let engine = Engine() + let sampler = Sampler() + engine.output = sampler + let recorder = try NodeRecorder(node: sampler) // record a little audio try engine.start() - player.play() + sampler.play(url: URL.testAudio) try recorder.reset() try recorder.record() sleep(1) @@ -24,23 +23,21 @@ class NodeRecorderTests: XCTestCase { recorder.stop() let audioFileURL = recorder.audioFile!.url engine.stop() - player.stop() - try player.load(url: audioFileURL) + sampler.stop() // test the result let audio = engine.startTest(totalDuration: 1.0) - player.play() + sampler.play(url: audioFileURL) audio.append(engine.render(duration: 1.0)) testMD5(audio) } func testCallback() throws { return // for now, tests are failing - let engine = AudioEngine() - let url = Bundle.module.url(forResource: "12345", withExtension: "wav", subdirectory: "TestResources")! - let player = AudioPlayer(url: url)! - engine.output = player - let recorder = try NodeRecorder(node: player) + let engine = Engine() + let sampler = Sampler() + engine.output = sampler + let recorder = try NodeRecorder(node: sampler) // attach the callback handler var values = [Float]() @@ -50,7 +47,7 @@ class NodeRecorderTests: XCTestCase { // record a little audio try engine.start() - player.play() + sampler.play(url: URL.testAudio) try recorder.reset() try recorder.record() sleep(1) @@ -59,12 +56,11 @@ class NodeRecorderTests: XCTestCase { recorder.stop() let audioFileURL = recorder.audioFile!.url engine.stop() - player.stop() - try player.load(url: audioFileURL) + sampler.stop() // test the result let audio = engine.startTest(totalDuration: 1.0) - player.play() + sampler.play(url: audioFileURL) audio.append(engine.render(duration: 1.0)) XCTAssertEqual(values[5000], -0.027038574) } diff --git a/Tests/AudioKitTests/Node Tests/NodeTests.swift b/Tests/AudioKitTests/Node Tests/NodeTests.swift index 91158a27bb..69048e7454 100644 --- a/Tests/AudioKitTests/Node Tests/NodeTests.swift +++ b/Tests/AudioKitTests/Node Tests/NodeTests.swift @@ -1,29 +1,27 @@ // Copyright AudioKit. All Rights Reserved. Revision History at http://github.com/AudioKit/AudioKit/ -@testable import AudioKit +import AudioKit import AVFoundation import XCTest class NodeTests: XCTestCase { func testNodeBasic() { - let engine = AudioEngine() - let player = AudioPlayer(testFile: "12345") - XCTAssertNil(player.avAudioNode.engine) - engine.output = player - XCTAssertNotNil(player.avAudioNode.engine) + let engine = Engine() + let sampler = Sampler() + engine.output = sampler let audio = engine.startTest(totalDuration: 0.1) - player.play() + sampler.play(url: URL.testAudio) audio.append(engine.render(duration: 0.1)) testMD5(audio) } #if os(macOS) // For some reason failing on iOS and tvOS func testNodeConnection() { - let engine = AudioEngine() - let player = AudioPlayer(testFile: "12345") - let verb = Reverb(player) + let engine = Engine() + let sampler = Sampler() + let verb = Reverb(sampler) engine.output = verb let audio = engine.startTest(totalDuration: 0.1) - player.play() + sampler.play(url: URL.testAudio) audio.append(engine.render(duration: 0.1)) XCTAssertFalse(audio.isSilent) testMD5(audio) @@ -33,8 +31,8 @@ class NodeTests: XCTestCase { func testNodeOutputFormatRespected() { let outputFormat = AVAudioFormat(standardFormatWithSampleRate: 16000, channels: 2)! let engine = AudioEngine() - let player = AudioPlayer(testFile: "12345") - let verb = CustomFormatReverb(player, outputFormat: outputFormat) + let sampler = Sampler() + let verb = CustomFormatReverb(sampler, outputFormat: outputFormat) engine.output = verb XCTAssertEqual(engine.mainMixerNode!.avAudioNode.inputFormat(forBus: 0), outputFormat) @@ -42,7 +40,7 @@ class NodeTests: XCTestCase { } func testRedundantConnection() { - let player = AudioPlayer(testFile: "12345") + let player = Sampler() let mixer = Mixer() mixer.addInput(player) mixer.addInput(player) @@ -50,19 +48,19 @@ class NodeTests: XCTestCase { } func testDynamicOutput() { - let engine = AudioEngine() + let engine = Engine() - let player1 = AudioPlayer(testFile: "12345") - engine.output = player1 + let sampler1 = Sampler() + engine.output = sampler1 let audio = engine.startTest(totalDuration: 2.0) - player1.play() + sampler1.play(url: URL.testAudio) let newAudio = engine.render(duration: 1.0) audio.append(newAudio) - let player2 = AudioPlayer(testFile: "drumloop") - engine.output = player2 - player2.play() + let sampler2 = Sampler() + engine.output = sampler2 + sampler2.play(url: URL.testAudioDrums) let newAudio2 = engine.render(duration: 1.0) audio.append(newAudio2) @@ -102,21 +100,21 @@ class NodeTests: XCTestCase { func testDynamicConnection2() throws { try XCTSkipIf(true, "TODO Skipped test") - let engine = AudioEngine() + let engine = Engine() - let player1 = AudioPlayer(testFile: "12345") - let mixer = Mixer(player1) + let sampler1 = Sampler() + let mixer = Mixer(sampler1) engine.output = mixer let audio = engine.startTest(totalDuration: 2.0) - player1.play() + sampler1.play(url: URL.testAudio) audio.append(engine.render(duration: 1.0)) - let player2 = AudioPlayer(testFile: "drumloop") - let verb = Reverb(player2) - player2.play() + let sampler2 = Sampler() + let verb = Reverb(sampler2) + sampler2.play(url: URL.testAudioDrums) mixer.addInput(verb) audio.append(engine.render(duration: 1.0)) @@ -127,25 +125,25 @@ class NodeTests: XCTestCase { func testDynamicConnection3() throws { try XCTSkipIf(true, "TODO Skipped test") - let engine = AudioEngine() + let engine = Engine() - let player1 = AudioPlayer(testFile: "12345") - let mixer = Mixer(player1) + let sampler1 = Sampler() + let mixer = Mixer(sampler1) engine.output = mixer let audio = engine.startTest(totalDuration: 3.0) - player1.play() + sampler1.play(url: URL.testAudio) audio.append(engine.render(duration: 1.0)) - let player2 = AudioPlayer(testFile: "drumloop") - mixer.addInput(player2) + let sampler2 = Sampler() + mixer.addInput(sampler2) - player2.play() + sampler2.play(url: URL.testAudioDrums) audio.append(engine.render(duration: 1.0)) - mixer.removeInput(player2) + mixer.removeInput(sampler2) audio.append(engine.render(duration: 1.0)) @@ -154,24 +152,24 @@ class NodeTests: XCTestCase { func testDynamicConnection4() throws { try XCTSkipIf(true, "TODO Skipped test") - let engine = AudioEngine() + let engine = Engine() let outputMixer = Mixer() - let player1 = AudioPlayer(testFile: "12345") + let player1 = Sampler() outputMixer.addInput(player1) engine.output = outputMixer let audio = engine.startTest(totalDuration: 2.0) - player1.play() + player1.play(url: URL.testAudio) audio.append(engine.render(duration: 1.0)) - let player2 = AudioPlayer(testFile: "drumloop") + let player2 = Sampler() let localMixer = Mixer() localMixer.addInput(player2) outputMixer.addInput(localMixer) - player2.play() + player2.play(url: URL.testAudioDrums) audio.append(engine.render(duration: 1.0)) testMD5(audio) @@ -179,19 +177,19 @@ class NodeTests: XCTestCase { func testDynamicConnection5() throws { try XCTSkipIf(true, "TODO Skipped test") - let engine = AudioEngine() + let engine = Engine() let outputMixer = Mixer() engine.output = outputMixer let audio = engine.startTest(totalDuration: 1.0) - let player = AudioPlayer(testFile: "12345") + let player = Sampler() let mixer = Mixer() mixer.addInput(player) outputMixer.addInput(mixer) // change mixer to osc and this will play - player.play() + player.play(url: URL.testAudio) audio.append(engine.render(duration: 1.0)) @@ -201,14 +199,14 @@ class NodeTests: XCTestCase { func testDisconnect() { let engine = AudioEngine() - let player = AudioPlayer(testFile: "12345") + let player = Sampler() let mixer = Mixer(player) engine.output = mixer let audio = engine.startTest(totalDuration: 2.0) - player.play() + player.play(url: URL.testAudio) audio.append(engine.render(duration: 1.0)) @@ -219,26 +217,26 @@ class NodeTests: XCTestCase { testMD5(audio) } - func testNodeDetach() { - let engine = AudioEngine() - - let player = AudioPlayer(testFile: "12345") - - let mixer = Mixer(player) - engine.output = mixer - - let audio = engine.startTest(totalDuration: 2.0) - - player.play() - - audio.append(engine.render(duration: 1.0)) - - player.detach() - - audio.append(engine.render(duration: 1.0)) - - testMD5(audio) - } +// func testNodeDetach() { +// let engine = AudioEngine() +// +// let player = Sampler() +// +// let mixer = Mixer(player) +// engine.output = mixer +// +// let audio = engine.startTest(totalDuration: 2.0) +// +// player.play(url: URL.testAudio) +// +// audio.append(engine.render(duration: 1.0)) +// +// player.detach() +// +// audio.append(engine.render(duration: 1.0)) +// +// testMD5(audio) +// } func testNodeStatus() { let url = Bundle.module.url(forResource: "chromaticScale-1", @@ -269,33 +267,33 @@ class NodeTests: XCTestCase { let engine = AudioEngine() let engine2 = AudioEngine() - let player = AudioPlayer(testFile: "12345") + let sampler = Sampler() - engine2.output = player + engine2.output = sampler - let verb = Reverb(player) + let verb = Reverb(sampler) engine.output = verb let audio = engine.startTest(totalDuration: 0.1) - player.play() + sampler.play(url: URL.testAudio) audio.append(engine.render(duration: 0.1)) XCTAssert(audio.isSilent) } - func testManyMixerConnections() { - let engine = AudioEngine() - - var players: [AudioPlayer] = [] - for _ in 0 ..< 16 { - players.append(AudioPlayer()) - } - - let mixer = Mixer(players) - engine.output = mixer - - XCTAssertEqual(mixer.avAudioNode.inputCount, 16) - } +// func testManyMixerConnections() { +// let engine = AudioEngine() +// +// var samplers: [Sampler] = [] +// for _ in 0 ..< 16 { +// samplers.append(Sampler()) +// } +// +// let mixer = Mixer(samplers) +// engine.output = mixer +// +// XCTAssertEqual(mixer.avAudioNode.inputCount, 16) +// } func connectionCount(node: AVAudioNode) -> Int { var count = 0 @@ -311,7 +309,7 @@ class NodeTests: XCTestCase { func testFanout() { let engine = AudioEngine() - let player = AudioPlayer(testFile: "12345") + let player = Sampler() let verb = Reverb(player) let mixer = Mixer(player, verb) @@ -324,7 +322,7 @@ class NodeTests: XCTestCase { func testMixerRedundantUpstreamConnection() { let engine = AudioEngine() - let player = AudioPlayer(testFile: "12345") + let player = Sampler() let mixer1 = Mixer(player) let mixer2 = Mixer(mixer1) @@ -342,11 +340,11 @@ class NodeTests: XCTestCase { try XCTSkipIf(true, "TODO Skipped test") let engine = AudioEngine() - let player = AudioPlayer(testFile: "12345") + let player = Sampler() func exampleStart() { engine.output = player try! engine.start() - player.play() + player.play(url: URL.testAudio) sleep(1) } func exampleStop() { @@ -365,18 +363,16 @@ class NodeTests: XCTestCase { // This provides a baseline for measuring the overhead // of mixers in testMixerPerformance. func testChainPerformance() { - let engine = AudioEngine() - let player = AudioPlayer(testFile: "12345") + let engine = Engine() + let player = Sampler() let rev = Reverb(player) - XCTAssertNil(player.avAudioNode.engine) engine.output = rev - XCTAssertNotNil(player.avAudioNode.engine) measureMetrics([.wallClockTime], automaticallyStartMeasuring: false) { let audio = engine.startTest(totalDuration: 10.0) - player.play() + player.play(url: URL.testAudio) startMeasuring() let buf = engine.render(duration: 10.0) @@ -388,20 +384,18 @@ class NodeTests: XCTestCase { // Measure the overhead of mixers. func testMixerPerformance() { - let engine = AudioEngine() - let player = AudioPlayer(testFile: "12345") + let engine = Engine() + let player = Sampler() let mix1 = Mixer(player) let rev = Reverb(mix1) let mix2 = Mixer(rev) - XCTAssertNil(player.avAudioNode.engine) engine.output = mix2 - XCTAssertNotNil(player.avAudioNode.engine) measureMetrics([.wallClockTime], automaticallyStartMeasuring: false) { let audio = engine.startTest(totalDuration: 10.0) - player.play() + player.play(url: URL.testAudio) startMeasuring() let buf = engine.render(duration: 10.0) @@ -411,50 +405,49 @@ class NodeTests: XCTestCase { } } - func testConnectionTreeDescriptionForStandaloneNode() { - let player = AudioPlayer(testFile: "12345") - XCTAssertEqual(player.connectionTreeDescription, "\(connectionTreeLinePrefix)↳AudioPlayer") - } +// func testConnectionTreeDescriptionForStandaloneNode() { +// let sampler = Sampler() +// XCTAssertEqual(sampler.connectionTreeDescription, "\(connectionTreeLinePrefix)↳Sampler") +// } +// +// func testConnectionTreeDescriptionForConnectedNode() { +// let sampler = Sampler() +// +// let verb = Reverb(sampler) +// let mixer = Mixer(sampler, verb) +// let mixerAddress = MemoryAddress(of: mixer).description +// +// XCTAssertEqual(mixer.connectionTreeDescription, +// """ +// \(connectionTreeLinePrefix)↳Mixer("\(mixerAddress)") +// \(connectionTreeLinePrefix) ↳Sampler +// \(connectionTreeLinePrefix) ↳Reverb +// \(connectionTreeLinePrefix) ↳Sampler +// """) +// } - func testConnectionTreeDescriptionForConnectedNode() { - let player = AudioPlayer(testFile: "12345") - - let verb = Reverb(player) - let mixer = Mixer(player, verb) - let mixerAddress = MemoryAddress(of: mixer).description - - XCTAssertEqual(mixer.connectionTreeDescription, - """ - \(connectionTreeLinePrefix)↳Mixer("\(mixerAddress)") - \(connectionTreeLinePrefix) ↳AudioPlayer - \(connectionTreeLinePrefix) ↳Reverb - \(connectionTreeLinePrefix) ↳AudioPlayer - """) - } - - #if !os(tvOS) - func testConnectionTreeDescriptionForNamedNode() { - let nameString = "Customized Name" - let sampler = MIDISampler(name: nameString) - let compressor = Compressor(sampler) - let mixer = Mixer(compressor) - let mixerAddress = MemoryAddress(of: mixer).description - - XCTAssertEqual(mixer.connectionTreeDescription, - """ - \(connectionTreeLinePrefix)↳Mixer("\(mixerAddress)") - \(connectionTreeLinePrefix) ↳Compressor - \(connectionTreeLinePrefix) ↳MIDISampler("\(nameString)") - """) - } - #endif +// #if !os(tvOS) +// func testConnectionTreeDescriptionForNamedNode() { +// let nameString = "Customized Name" +// let sampler = MIDISampler(name: nameString) +// let compressor = Compressor(sampler) +// let mixer = Mixer(compressor) +// let mixerAddress = MemoryAddress(of: mixer).description +// +// XCTAssertEqual(mixer.connectionTreeDescription, +// """ +// \(connectionTreeLinePrefix)↳Mixer("\(mixerAddress)") +// \(connectionTreeLinePrefix) ↳Compressor +// \(connectionTreeLinePrefix) ↳MIDISampler("\(nameString)") +// """) +// } +// #endif func testGraphviz() { - let player = AudioPlayer(testFile: "12345") - player.label = "MyAwesomePlayer" - - let verb = Reverb(player) - let mixer = Mixer(player, verb) + let sampler = Sampler() + + let verb = Reverb(sampler) + let mixer = Mixer(sampler, verb) let dot = mixer.graphviz @@ -463,24 +456,20 @@ class NodeTests: XCTestCase { } func testAllNodesInChainDeallocatedOnRemove() { - for strategy in [DisconnectStrategy.recursive, .detach] { - let engine = AudioEngine() - var chain: Node? = createChain() - weak var weakPitch = chain?.avAudioNode - weak var weakDelay = chain?.connections.first?.avAudioNode - weak var weakPlayer = chain?.connections.first?.connections.first?.avAudioNode - let mixer = Mixer(chain!, createChain()) - engine.output = mixer - - mixer.removeInput(chain!, strategy: strategy) - chain = nil + let engine = Engine() + var chain: Node? = createChain() + weak var weakPitch = chain?.avAudioNode + weak var weakDelay = chain?.connections.first?.avAudioNode + weak var weakPlayer = chain?.connections.first?.connections.first?.avAudioNode + let mixer = Mixer(chain!, createChain()) + engine.output = mixer - XCTAssertNil(weakPitch) - XCTAssertNil(weakDelay) - XCTAssertNil(weakPlayer) + mixer.removeInput(chain!) + chain = nil - XCTAssertFalse(engine.avEngine.description.contains("other nodes")) - } + XCTAssertNil(weakPitch) + XCTAssertNil(weakDelay) + XCTAssertNil(weakPlayer) } @available(iOS 13.0, *) @@ -585,12 +574,5 @@ class NodeTests: XCTestCase { } private extension NodeTests { - func createChain() -> Node { TimePitch(Delay(AudioPlayer())) } -} - -extension AudioPlayer { - convenience init(testFile: String) { - let url = Bundle.module.url(forResource: testFile, withExtension: "wav", subdirectory: "TestResources")! - self.init(url: url)! - } + func createChain() -> Node { TimePitch(Delay(Sampler())) } } diff --git a/Tests/AudioKitTests/Node Tests/Playback Tests/AppleSamplerTests.swift b/Tests/AudioKitTests/Node Tests/Playback Tests/AppleSamplerTests.swift index d2cdd9b371..f1d3342b06 100644 --- a/Tests/AudioKitTests/Node Tests/Playback Tests/AppleSamplerTests.swift +++ b/Tests/AudioKitTests/Node Tests/Playback Tests/AppleSamplerTests.swift @@ -9,7 +9,7 @@ import XCTest /* class AppleSamplerTests: XCTestCase { let sampler = AppleSampler() - let engine = AudioEngine() + let engine = Engine() override func setUpWithError() throws { let sampleURL = Bundle.module.url(forResource: "TestResources/sinechirp", withExtension: "wav")! diff --git a/Tests/AudioKitTests/Node Tests/RecordingTests.swift b/Tests/AudioKitTests/Node Tests/RecordingTests.swift index b4364b3785..157aab7b20 100644 --- a/Tests/AudioKitTests/Node Tests/RecordingTests.swift +++ b/Tests/AudioKitTests/Node Tests/RecordingTests.swift @@ -1,5 +1,5 @@ // Copyright AudioKit. All Rights Reserved. Revision History at http://github.com/AudioKit/AudioKit/ -@testable import AudioKit +import AudioKit import AVFoundation import XCTest @@ -83,8 +83,7 @@ class RecordingTests: AudioFileTestCase { } func testOpenCloseFile() { - guard let url = Bundle.module.url(forResource: "TestResources/12345", withExtension: "wav"), - let file = try? AVAudioFile(forReading: url) else { + guard let file = try? AVAudioFile(forReading: URL.testAudio) else { XCTFail("Didn't get test file") return } @@ -136,8 +135,7 @@ class RecordingTests: AudioFileTestCase { } func testPauseRecording() { - guard let url = Bundle.module.url(forResource: "TestResources/12345", withExtension: "wav"), - let file = try? AVAudioFile(forReading: url) else { + guard let file = try? AVAudioFile(forReading: URL.testAudio) else { XCTFail("Didn't get test file") return } @@ -196,8 +194,7 @@ class RecordingTests: AudioFileTestCase { } func testReset() { - guard let url = Bundle.module.url(forResource: "TestResources/12345", withExtension: "wav"), - let file = try? AVAudioFile(forReading: url) else { + guard let file = try? AVAudioFile(forReading: URL.testAudio) else { XCTFail("Didn't get test file") return } diff --git a/Tests/AudioKitTests/RingBufferTests.swift b/Tests/AudioKitTests/RingBufferTests.swift new file mode 100644 index 0000000000..c02eb76e40 --- /dev/null +++ b/Tests/AudioKitTests/RingBufferTests.swift @@ -0,0 +1,83 @@ +// Copyright AudioKit. All Rights Reserved. Revision History at http://github.com/AudioKit/AudioKit/ + +import XCTest +import AudioKit + +final class RingBufferTests: XCTestCase { + + func testRingBuffer() { + let buffer = RingBuffer() + + let pushResult = buffer.push(1.666) + + XCTAssertTrue(pushResult) + + let popResult = buffer.pop() + + XCTAssertEqual(popResult, 1.666) + + var floats: [Float] = [1, 2, 3, 4, 5] + + _ = floats.withUnsafeBufferPointer { ptr in + buffer.push(from: ptr) + } + + floats = [0, 0, 0, 0, 0] + + _ = floats.withUnsafeMutableBufferPointer { ptr in + buffer.pop(to: ptr) + } + + XCTAssertEqual(floats, [1, 2, 3, 4, 5]) + + } + + func testProducerConsumer() { + + let buffer = RingBuffer() + + class Producer: Thread { + var buffer: RingBuffer + + init(buffer: RingBuffer) { + self.buffer = buffer + } + + override func main() { + for i in 0 ..< 1000 { + XCTAssertTrue(buffer.push(i)) + } + } + } + + class Consumer: Thread { + var buffer: RingBuffer + + init(buffer: RingBuffer) { + self.buffer = buffer + } + + override func main() { + for i in 0 ..< 1000 { + + while(true) { + if let value = buffer.pop() { + XCTAssertEqual(value, i) + break + } + } + + } + } + } + + let producer = Producer(buffer: buffer) + let consumer = Consumer(buffer: buffer) + + consumer.start() + producer.start() + + sleep(1) + } + +} diff --git a/Tests/AudioKitTests/TableTests.swift b/Tests/AudioKitTests/TableTests.swift index 2419f04475..d6a2f81515 100644 --- a/Tests/AudioKitTests/TableTests.swift +++ b/Tests/AudioKitTests/TableTests.swift @@ -6,10 +6,10 @@ import XCTest class TableTests: XCTestCase { @available(macOS 10.15, iOS 13.0, tvOS 13.0, *) func testReverseSawtooth() { - let engine = AudioEngine() - let input = PlaygroundOscillator(waveform: Table(.reverseSawtooth)) - engine.output = input - input.start() + let engine = Engine() + let osc = PlaygroundOscillator(waveform: Table(.reverseSawtooth)) + engine.output = osc + osc.start() let audio = engine.startTest(totalDuration: 1.0) audio.append(engine.render(duration: 1.0)) testMD5(audio) @@ -17,7 +17,7 @@ class TableTests: XCTestCase { @available(macOS 10.15, iOS 13.0, tvOS 13.0, *) func testSawtooth() { - let engine = AudioEngine() + let engine = Engine() let input = PlaygroundOscillator(waveform: Table(.sawtooth)) engine.output = input input.start() @@ -26,22 +26,18 @@ class TableTests: XCTestCase { testMD5(audio) } - /* Can't test due to sine differences on M1 chip func testSine() { - let engine = AudioEngine() + let engine = Engine() let input = PlaygroundOscillator(waveform: Table(.sine)) engine.output = input - // This is just the usual tested sine wave input.start() let audio = engine.startTest(totalDuration: 1.0) audio.append(engine.render(duration: 1.0)) testMD5(audio) } - */ - @available(macOS 10.15, iOS 13.0, tvOS 13.0, *) func testTriangle() { - let engine = AudioEngine() + let engine = Engine() let input = PlaygroundOscillator(waveform: Table(.triangle)) engine.output = input input.start() @@ -50,9 +46,8 @@ class TableTests: XCTestCase { testMD5(audio) } - /* Can't test due to sine differences on M1 chip func testHarmonicWithPartialAmplitudes() { - let engine = AudioEngine() + let engine = Engine() let partialAmplitudes: [Float] = [0.8, 0.2, 0.3, 0.06, 0.12, 0.0015] let input = PlaygroundOscillator(waveform: Table(.harmonic(partialAmplitudes))) engine.output = input @@ -61,5 +56,4 @@ class TableTests: XCTestCase { audio.append(engine.render(duration: 1.0)) testMD5(audio) } - */ } diff --git a/Tests/AudioKitTests/Tap Tests/AmplitudeTapTests.swift b/Tests/AudioKitTests/Tap Tests/AmplitudeTapTests.swift index 2660c8ae63..b94373a9db 100644 --- a/Tests/AudioKitTests/Tap Tests/AmplitudeTapTests.swift +++ b/Tests/AudioKitTests/Tap Tests/AmplitudeTapTests.swift @@ -7,11 +7,10 @@ import AVFAudio class AmplitudeTapTests: XCTestCase { func testTapDoesntDeadlockOnStop() throws { - let engine = AudioEngine() - let url = Bundle.module.url(forResource: "12345", withExtension: "wav", subdirectory: "TestResources")! - let player = AudioPlayer(url: url)! - engine.output = player - let tap = AmplitudeTap(player) + let engine = Engine() + let sampler = Sampler() + engine.output = sampler + let tap = AmplitudeTap(sampler) _ = engine.startTest(totalDuration: 1) tap.start() @@ -89,7 +88,6 @@ class AmplitudeTapTests: XCTestCase { let noise = PlaygroundNoiseGenerator(amplitude: 0.0) engine.output = noise - noise.start() let expect = expectation(description: "wait for amplitudes") diff --git a/Tests/AudioKitTests/Tap Tests/BaseTapTests.swift b/Tests/AudioKitTests/Tap Tests/BaseTapTests.swift index f675668d51..8b875a64b9 100644 --- a/Tests/AudioKitTests/Tap Tests/BaseTapTests.swift +++ b/Tests/AudioKitTests/Tap Tests/BaseTapTests.swift @@ -6,8 +6,7 @@ import XCTest class BaseTapTests: XCTestCase { func testBaseTapDeallocated() throws { let engine = AudioEngine() - let url = Bundle.module.url(forResource: "12345", withExtension: "wav", subdirectory: "TestResources")! - let player = AudioPlayer(url: url)! + let player = AudioPlayer(url: URL.testAudio)! engine.output = player var tap: BaseTap? = BaseTap(player, bufferSize: 1024) @@ -21,8 +20,7 @@ class BaseTapTests: XCTestCase { func testBufferSizeExceedingFrameCapacity() { let engine = AudioEngine() - let url = Bundle.module.url(forResource: "12345", withExtension: "wav", subdirectory: "TestResources")! - let player = AudioPlayer(url: url)! + let player = AudioPlayer(url: URL.testAudio)! engine.output = player let tap: BaseTap = BaseTap(player, bufferSize: 176400) diff --git a/Tests/AudioKitTests/Tap Tests/RawBufferTapTests.swift b/Tests/AudioKitTests/Tap Tests/RawBufferTapTests.swift index 4b7f4f122e..8c2f4093e0 100644 --- a/Tests/AudioKitTests/Tap Tests/RawBufferTapTests.swift +++ b/Tests/AudioKitTests/Tap Tests/RawBufferTapTests.swift @@ -10,7 +10,9 @@ final class RawBufferTapTests: XCTestCase { let engine = AudioEngine() let osc = PlaygroundOscillator() - engine.output = osc + let mixer = Mixer(osc) + mixer.volume = 0 + engine.output = mixer let dataExpectation = XCTestExpectation(description: "dataExpectation") var allBuffers: [(AVAudioPCMBuffer, AVAudioTime)] = [] diff --git a/Tests/AudioKitTests/Test Helpers/ConstantGenerator.swift b/Tests/AudioKitTests/Test Helpers/ConstantGenerator.swift index c1745ef4f5..cfda7c1eef 100644 --- a/Tests/AudioKitTests/Test Helpers/ConstantGenerator.swift +++ b/Tests/AudioKitTests/Test Helpers/ConstantGenerator.swift @@ -1,23 +1,118 @@ // Copyright AudioKit. All Rights Reserved. Revision History at http://github.com/AudioKit/AudioKit/ +import Foundation +import AudioUnit +import AVFoundation import AudioKit -import AVFAudio -@available(macOS 10.15, iOS 13.0, tvOS 13.0, *) public class ConstantGenerator: Node { - public var connections: [Node] { [] } - public private(set) var avAudioNode: AVAudioNode + public let connections: [Node] = [] - init(constant: Float) { - avAudioNode = AVAudioSourceNode { _, _, frameCount, audioBufferList in - let ablPointer = UnsafeMutableAudioBufferListPointer(audioBufferList) - for frame in 0.., + timeStamp: UnsafePointer, + frameCount: AUAudioFrameCount, + outputBusNumber: Int, + outputBufferList: UnsafeMutablePointer, + renderEvents: UnsafePointer?, + inputBlock: AURenderPullInputBlock?) in + + let ablPointer = UnsafeMutableAudioBufferListPointer(outputBufferList) + for frame in 0 ..< Int(frameCount) { for buffer in ablPointer { - let buf: UnsafeMutableBufferPointer = UnsafeMutableBufferPointer(buffer) - buf[frame] = constant + let buf = UnsafeMutableBufferPointer(buffer) + assert(frame < buf.count) + buf[frame] = self.constant } } + return noErr } } + } + +//@available(macOS 10.15, iOS 13.0, tvOS 13.0, *) +//public class ConstantGenerator: Node { +// public var connections: [Node] { [] } +// public private(set) var avAudioNode: AVAudioNode +// +// init(constant: Float) { +// avAudioNode = AVAudioSourceNode { _, _, frameCount, audioBufferList in +// let ablPointer = UnsafeMutableAudioBufferListPointer(audioBufferList) +// for frame in 0.. = UnsafeMutableBufferPointer(buffer) +// buf[frame] = constant +// } +// } +// return noErr +// } +// } +//} diff --git a/Tests/AudioKitTests/Test Helpers/TestOscillator.swift b/Tests/AudioKitTests/Test Helpers/TestOscillator.swift new file mode 100644 index 0000000000..bed6e96c35 --- /dev/null +++ b/Tests/AudioKitTests/Test Helpers/TestOscillator.swift @@ -0,0 +1,112 @@ +// Copyright AudioKit. All Rights Reserved. Revision History at http://github.com/AudioKit/AudioKit/ + +import Foundation +import AudioUnit +import AVFoundation +import AudioKit + +public class TestOscillator: Node { + public let connections: [Node] = [] + + public let avAudioNode: AVAudioNode + + let oscAU: TestOscillatorAudioUnit + + // XXX: should be using parameters + public var frequency: Float { get { oscAU.frequency } set { oscAU.frequency = newValue }} + + public init() { + + let componentDescription = AudioComponentDescription(generator: "tosc") + + AUAudioUnit.registerSubclass(TestOscillatorAudioUnit.self, + as: componentDescription, + name: "osc AU", + version: .max) + avAudioNode = instantiate(componentDescription: componentDescription) + oscAU = avAudioNode.auAudioUnit as! TestOscillatorAudioUnit + } +} + + +/// Renders a sine wave. +class TestOscillatorAudioUnit: AUAudioUnit { + + private var inputBusArray: AUAudioUnitBusArray! + private var outputBusArray: AUAudioUnitBusArray! + + let inputChannelCount: NSNumber = 2 + let outputChannelCount: NSNumber = 2 + + override public var channelCapabilities: [NSNumber]? { + return [inputChannelCount, outputChannelCount] + } + + /// Initialize with component description and options + /// - Parameters: + /// - componentDescription: Audio Component Description + /// - options: Audio Component Instantiation Options + /// - Throws: error + override public init(componentDescription: AudioComponentDescription, + options: AudioComponentInstantiationOptions = []) throws { + + try super.init(componentDescription: componentDescription, options: options) + + let format = AVAudioFormat(standardFormatWithSampleRate: 44100, channels: 2)! + inputBusArray = AUAudioUnitBusArray(audioUnit: self, busType: .input, busses: []) + outputBusArray = AUAudioUnitBusArray(audioUnit: self, busType: .output, busses: [try AUAudioUnitBus(format: format)]) + + parameterTree = AUParameterTree.createTree(withChildren: []) + } + + override var inputBusses: AUAudioUnitBusArray { + inputBusArray + } + + override var outputBusses: AUAudioUnitBusArray { + outputBusArray + } + + override func allocateRenderResources() throws {} + + override func deallocateRenderResources() {} + + var currentPhase: Double = 0.0 + var frequency: Float = 440.0 + var amplitude: Float = 1.0 + + override var internalRenderBlock: AUInternalRenderBlock { + { (actionFlags: UnsafeMutablePointer, + timeStamp: UnsafePointer, + frameCount: AUAudioFrameCount, + outputBusNumber: Int, + outputBufferList: UnsafeMutablePointer, + renderEvents: UnsafePointer?, + inputBlock: AURenderPullInputBlock?) in + + let ablPointer = UnsafeMutableAudioBufferListPointer(outputBufferList) + + let twoPi = 2 * Double.pi + let phaseIncrement = (twoPi / Double(Settings.sampleRate)) * Double(self.frequency) + for frame in 0 ..< Int(frameCount) { + // Get signal value for this frame at time. + let value = sin(Float(self.currentPhase)) * self.amplitude + + // Advance the phase for the next frame. + self.currentPhase += phaseIncrement + if self.currentPhase >= twoPi { self.currentPhase -= twoPi } + if self.currentPhase < 0.0 { self.currentPhase += twoPi } + // Set the same value on all channels (due to the inputFormat we have only 1 channel though). + for buffer in ablPointer { + let buf = UnsafeMutableBufferPointer(buffer) + assert(frame < buf.count) + buf[frame] = value + } + } + + return noErr + } + } + +} + diff --git a/Tests/AudioKitTests/ValidatedMD5s.swift b/Tests/AudioKitTests/ValidatedMD5s.swift index 9c24a093af..89b77160c1 100644 --- a/Tests/AudioKitTests/ValidatedMD5s.swift +++ b/Tests/AudioKitTests/ValidatedMD5s.swift @@ -1,78 +1,104 @@ import AVFoundation import XCTest +extension URL { + static var testAudio: URL { + return Bundle.module.url(forResource: "12345", withExtension: "wav", subdirectory: "TestResources")! + } + + static var testAudioDrums: URL { + return Bundle.module.url(forResource: "drumloop", withExtension: "wav", subdirectory: "TestResources")! + } + +} + extension XCTestCase { func testMD5(_ buffer: AVAudioPCMBuffer) { let localMD5 = buffer.md5 let name = description XCTAssertFalse(buffer.isSilent) - XCTAssert(validatedMD5s[name] == buffer.md5, "\nFAILEDMD5 \"\(name)\": \"\(localMD5)\",") + if let validMD5s = validatedMD5s[name] { + XCTAssert(validMD5s.contains(localMD5), "\nFAILEDMD5 \"\(name)\": \"\(localMD5)\",") + } } } -let validatedMD5s: [String: String] = [ - "-[AppleSamplerTests testAmplitude]": "d0526514c48f769f48e237974a21a2e5", - "-[AppleSamplerTests testPan]": "6802732a1a3d132485509187fe476f9a", - "-[AppleSamplerTests testSamplePlayback]": "7e38e34c8d052d9730b24cddd160d328", - "-[AppleSamplerTests testStop]": "b42b86f6a7ff3a6fc85eb1760226cba0", - "-[AppleSamplerTests testVolume]": "0b71c337205812fb30c536a014af7765", - "-[AudioPlayerTests testBasic]": "feb1367cee8917a890088b8967b8d422", - "-[AudioPlayerTests testEngineRestart]": "b0dd4297f40fd11a2b648f6cb3aad13f", - "-[AudioPlayerTests testCurrentTime]": "af7c73c8c8c6f43a811401246c10cba4", - "-[AudioPlayerTests testToggleEditTime]": "ff165ef8695946c41d3bbb8b68e5d295", - "-[AudioPlayerTests testLoop]": "4288a0ae8722e446750e1e0b3b96068a", - "-[AudioPlayerTests testPlayAfterPause]": "ff480a484c1995e69022d470d09e6747", - "-[AudioPlayerTests testScheduleFile]": "ba487f42fa93379f0b24c7930d51fdd3", - "-[AudioPlayerTests testSeek]": "3bba42419e6583797e166b7a6d4bb45d", - "-[AudioPlayerTests testVolume]": "ba487f42fa93379f0b24c7930d51fdd3", - "-[AudioPlayerTests testSwitchFilesDuringPlayback]": "5bd0d50c56837bfdac4d9881734d0f8e", - "-[AudioPlayerTests testCanStopPausedPlayback]": "7076f63dc5c70f6bd006a7d4ff891aa3", - "-[AudioPlayerTests testCurrentPosition]": "8c5c55d9f59f471ca1abb53672e3ffbf", - "-[AudioPlayerTests testSeekAfterPause]": "271add78c1dc38d54b261d240dab100f", - "-[AudioPlayerTests testSeekAfterStop]": "90a31285a6ce11a3609a2c52f0b3ec66", - "-[AudioPlayerTests testSeekForwardsAndBackwards]": "31d6c565efa462738ac32e9438ccfed0", - "-[AudioPlayerTests testSeekWillStop]": "84b026cbdf45d9c5f5659f1106fdee6a", - "-[AudioPlayerTests testSeekWillContinueLooping]": "5becbd9530850f217f95ee1142a8db30", - "-[AudioPlayerTests testPlaybackWillStopWhenSettingLoopingForBuffer]": "5becbd9530850f217f95ee1142a8db30", - "-[CompressorTests testAttackTime]": "f2da585c3e9838c1a41f1a5f34c467d0", - "-[CompressorTests testDefault]": "3064ef82b30c512b2f426562a2ef3448", - "-[CompressorTests testHeadRoom]": "98ac5f20a433ba5a858c461aa090d81f", - "-[CompressorTests testMasterGain]": "b8ff41f64341a786bd6533670d238560", - "-[CompressorTests testParameters]": "6b99deb194dd53e8ceb6428924d6666b", - "-[CompressorTests testThreshold]": "e1133fc525a256a72db31453d293c47c", - "-[MixerTests testSplitConnection]": "6b2d34e86130813c7e7d9f1cf7a2a87c", - "-[MultiSegmentPlayerTests testAttemptToPlayZeroFrames]": "feb1367cee8917a890088b8967b8d422", - "-[MultiSegmentPlayerTests testPlaySegment]": "feb1367cee8917a890088b8967b8d422", - "-[MultiSegmentPlayerTests testPlaySegmentInTheFuture]": "00545f274477d014dcc51822d97f1705", - "-[MultiSegmentPlayerTests testPlayMultipleSegments]": "feb1367cee8917a890088b8967b8d422", - "-[MultiSegmentPlayerTests testPlayMultiplePlayersInSync]": "d405ff00ef9dd3c890486163b7499a52", - "-[MultiSegmentPlayerTests testPlayWithinSegment]": "adc3d1fef36f68e1f12dbb471eb4069b", - "-[NodeRecorderTests testBasicRecord]": "f98d952748c408b1e38325f2bfe2ce81", - "-[NodeTests testDisconnect]": "8c5c55d9f59f471ca1abb53672e3ffbf", - "-[NodeTests testDynamicConnection]": "c61c69779df208d80f371881346635ce", - "-[NodeTests testDynamicConnection2]": "8c5c55d9f59f471ca1abb53672e3ffbf", - "-[NodeTests testDynamicConnection3]": "70e6414b0f09f42f70ca7c0b0d576e84", - "-[NodeTests testDynamicOutput]": "faf8254c11a6b73eb3238d57b1c14a9f", - "-[NodeTests testNodeBasic]": "7e9104f6cbe53a0e3b8ec2d041f56396", - "-[NodeTests testNodeConnection]": "5fbcf0b327308ff4fc9b42292986e2d5", - "-[NodeTests testNodeDetach]": "8c5c55d9f59f471ca1abb53672e3ffbf", - "-[NodeTests testTwoEngines]": "42b1eafdf0fc632f46230ad0497a29bf", - "-[PeakLimiterTests testAttackTime]": "8e221adb58aca54c3ad94bce33be27db", - "-[PeakLimiterTests testDecayTime]": "5f3ea74e9760271596919bf5a41c5fab", - "-[PeakLimiterTests testDecayTime2]": "a2a33f30e573380bdacea55ea9ca2dae", - "-[PeakLimiterTests testDefault]": "61c67b55ea69bad8be2bbfe5d5cde055", - "-[PeakLimiterTests testParameters]": "e4abd97f9f0a0826823c167fb7ae730b", - "-[PeakLimiterTests testPreGain]": "2f1b0dd9020be6b1fa5b8799741baa5f", - "-[PeakLimiterTests testPreGainChangingAfterEngineStarted]": "ed14bc85f1732bd77feaa417c0c20cae", - "-[ReverbTests testBypass]": "6b2d34e86130813c7e7d9f1cf7a2a87c", - "-[ReverbTests testCathedral]": "7f1a07c82349bcd989a7838fd3f5ca9d", - "-[ReverbTests testDefault]": "28d2cb7a5c1e369ca66efa8931d31d4d", - "-[ReverbTests testSmallRoom]": "747641220002d1c968d62acb7bea552c", - "-[SequencerTrackTests testChangeTempo]": "3e05405bead660d36ebc9080920a6c1e", - "-[SequencerTrackTests testLoop]": "3a7ebced69ddc6669932f4ee48dabe2b", - "-[SequencerTrackTests testOneShot]": "3fbf53f1139a831b3e1a284140c8a53c", - "-[SequencerTrackTests testTempo]": "1eb7efc6ea54eafbe616dfa8e1a3ef36", - "-[TableTests testReverseSawtooth]": "b3188781c2e696f065629e2a86ef57a6", - "-[TableTests testSawtooth]": "6f37a4d0df529995d7ff783053ff18fe", - "-[TableTests testTriangle]": "789c1e77803a4f9d10063eb60ca03cea", +let validatedMD5s: [String: [String]] = [ + "-[AppleSamplerTests testAmplitude]": ["d0526514c48f769f48e237974a21a2e5"], + "-[AppleSamplerTests testPan]": ["6802732a1a3d132485509187fe476f9a"], + "-[AppleSamplerTests testSamplePlayback]": ["7e38e34c8d052d9730b24cddd160d328"], + "-[AppleSamplerTests testStop]": ["b42b86f6a7ff3a6fc85eb1760226cba0"], + "-[AppleSamplerTests testVolume]": ["0b71c337205812fb30c536a014af7765"], + "-[AudioPlayerTests testBasic]": ["feb1367cee8917a890088b8967b8d422"], + "-[AudioPlayerTests testEngineRestart]": ["b0dd4297f40fd11a2b648f6cb3aad13f"], + "-[AudioPlayerTests testCurrentTime]": ["af7c73c8c8c6f43a811401246c10cba4"], + "-[AudioPlayerTests testToggleEditTime]": ["ff165ef8695946c41d3bbb8b68e5d295"], + "-[AudioPlayerTests testLoop]": ["4288a0ae8722e446750e1e0b3b96068a"], + "-[AudioPlayerTests testPlayAfterPause]": ["ff480a484c1995e69022d470d09e6747"], + "-[AudioPlayerTests testScheduleFile]": ["ba487f42fa93379f0b24c7930d51fdd3"], + "-[AudioPlayerTests testSeek]": ["3bba42419e6583797e166b7a6d4bb45d"], + "-[AudioPlayerTests testVolume]": ["ba487f42fa93379f0b24c7930d51fdd3"], + "-[AudioPlayerTests testSwitchFilesDuringPlayback]": ["5bd0d50c56837bfdac4d9881734d0f8e"], + "-[AudioPlayerTests testCanStopPausedPlayback]": ["7076f63dc5c70f6bd006a7d4ff891aa3"], + "-[AudioPlayerTests testCurrentPosition]": ["8c5c55d9f59f471ca1abb53672e3ffbf"], + "-[AudioPlayerTests testSeekAfterPause]": ["271add78c1dc38d54b261d240dab100f"], + "-[AudioPlayerTests testSeekAfterStop]": ["90a31285a6ce11a3609a2c52f0b3ec66"], + "-[AudioPlayerTests testSeekForwardsAndBackwards]": ["31d6c565efa462738ac32e9438ccfed0"], + "-[AudioPlayerTests testSeekWillStop]": ["84b026cbdf45d9c5f5659f1106fdee6a"], + "-[AudioPlayerTests testSeekWillContinueLooping]": ["5becbd9530850f217f95ee1142a8db30"], + "-[AudioPlayerTests testPlaybackWillStopWhenSettingLoopingForBuffer]": ["5becbd9530850f217f95ee1142a8db30"], + "-[CompressorTests testAttackTime]": ["f2da585c3e9838c1a41f1a5f34c467d0"], + "-[CompressorTests testDefault]": ["3064ef82b30c512b2f426562a2ef3448"], + "-[CompressorTests testHeadRoom]": ["98ac5f20a433ba5a858c461aa090d81f"], + "-[CompressorTests testMasterGain]": ["b8ff41f64341a786bd6533670d238560"], + "-[CompressorTests testParameters]": ["6b99deb194dd53e8ceb6428924d6666b"], + "-[CompressorTests testThreshold]": ["e1133fc525a256a72db31453d293c47c"], + "-[MixerTests testSplitConnection]": ["6b2d34e86130813c7e7d9f1cf7a2a87c"], + "-[MultiSegmentPlayerTests testAttemptToPlayZeroFrames]": ["feb1367cee8917a890088b8967b8d422"], + "-[MultiSegmentPlayerTests testPlaySegment]": ["feb1367cee8917a890088b8967b8d422"], + "-[MultiSegmentPlayerTests testPlaySegmentInTheFuture]": ["00545f274477d014dcc51822d97f1705"], + "-[MultiSegmentPlayerTests testPlayMultipleSegments]": ["feb1367cee8917a890088b8967b8d422"], + "-[MultiSegmentPlayerTests testPlayMultiplePlayersInSync]": ["d405ff00ef9dd3c890486163b7499a52"], + "-[MultiSegmentPlayerTests testPlayWithinSegment]": ["adc3d1fef36f68e1f12dbb471eb4069b"], + "-[NodeRecorderTests testBasicRecord]": ["f98d952748c408b1e38325f2bfe2ce81"], + "-[NodeTests testDisconnect]": ["8c5c55d9f59f471ca1abb53672e3ffbf"], + "-[NodeTests testDynamicConnection]": ["8c39c3c9a55e4a8675dc352da8543974"], + "-[NodeTests testDynamicConnection2]": ["8c5c55d9f59f471ca1abb53672e3ffbf"], + "-[NodeTests testDynamicConnection3]": ["70e6414b0f09f42f70ca7c0b0d576e84"], + "-[NodeTests testDynamicOutput]": ["faf8254c11a6b73eb3238d57b1c14a9f"], + "-[NodeTests testNodeBasic]": ["7e9104f6cbe53a0e3b8ec2d041f56396"], + "-[NodeTests testNodeConnection]": ["5fbcf0b327308ff4fc9b42292986e2d5"], + "-[NodeTests testNodeDetach]": ["8c5c55d9f59f471ca1abb53672e3ffbf"], + "-[NodeTests testTwoEngines]": ["42b1eafdf0fc632f46230ad0497a29bf"], + "-[PeakLimiterTests testAttackTime]": ["8e221adb58aca54c3ad94bce33be27db"], + "-[PeakLimiterTests testDecayTime]": ["5f3ea74e9760271596919bf5a41c5fab"], + "-[PeakLimiterTests testDecayTime2]": ["a2a33f30e573380bdacea55ea9ca2dae"], + "-[PeakLimiterTests testDefault]": ["61c67b55ea69bad8be2bbfe5d5cde055"], + "-[PeakLimiterTests testParameters]": ["e4abd97f9f0a0826823c167fb7ae730b"], + "-[PeakLimiterTests testPreGain]": ["2f1b0dd9020be6b1fa5b8799741baa5f"], + "-[PeakLimiterTests testPreGainChangingAfterEngineStarted]": ["ed14bc85f1732bd77feaa417c0c20cae"], + "-[ReverbTests testBypass]": ["6b2d34e86130813c7e7d9f1cf7a2a87c"], + "-[ReverbTests testCathedral]": ["7f1a07c82349bcd989a7838fd3f5ca9d"], + "-[ReverbTests testDefault]": ["28d2cb7a5c1e369ca66efa8931d31d4d"], + "-[ReverbTests testSmallRoom]": ["747641220002d1c968d62acb7bea552c"], + "-[SequencerTrackTests testChangeTempo]": ["3e05405bead660d36ebc9080920a6c1e"], + "-[SequencerTrackTests testLoop]": ["3a7ebced69ddc6669932f4ee48dabe2b"], + "-[SequencerTrackTests testOneShot]": ["3fbf53f1139a831b3e1a284140c8a53c"], + "-[SequencerTrackTests testTempo]": ["1eb7efc6ea54eafbe616dfa8e1a3ef36"], + "-[TableTests testReverseSawtooth]": ["3c40428e755926307bffd903346dd652"], + "-[TableTests testSawtooth]": ["f31d4c79fd6822e9e457eaaa888378a2"], + "-[TableTests testTriangle]": ["9c1146981e940074bbbf63f1c2dd3896"], + "-[EngineTests testBasic]": ["6869abdc57172524cae42e6dfe156717", "f5b785dcc74759b4a0492aef430bfc2e"], + "-[EngineTests testCompressorWithSampler]": ["f2da585c3e9838c1a41f1a5f34c467d0"], + "-[EngineTests testDynamicChange]": ["7844ad59f69ddb82bb3c929878e41691", "27bfee745a1ff33f8705f0c0746f61a4"], + "-[EngineTests testEffect]": ["4a6c20066720f76eb7298b5e2c3de487", "87001076267eccfdeb864cfdef2aaed8"], + "-[EngineTests testMixer]": ["3e5a8031aa91b780628c04c596f02a72", "3080c286b6a0afb4a236f9081b71305f"], + "-[EngineTests testMixerDynamic]": ["3bec0f12d12bb148a76abc9a783638b8", "b769c079b3de2646072cdc1226278527"], + "-[EngineTests testMixerVolume]": ["9ceefd8024a0b3f68afa6cd185931b86", "258811a4f3df7ed61659950c68ccbd3e"], + "-[EngineTests testMultipleChanges]": ["2fd4428074d3bee04f04eddd5bcfb389", "6a7c75f86ded225279473587866eb454"], + "-[EngineTests testPlaygroundOscillator]": ["6854112b8fcdcde5604ba57c69e685ec", "15eb052f9415e0f90e447340bd609589"], + "-[EngineTests testSampler]": ["f44518ab94a8bab9a3ef8acfe1a4d45b"], + "-[EngineTests testSamplerMIDINote]": ["38f84463320c0824422b4105b771b67c"], + "-[EngineTests testTwoEffects]": ["4caf3fa63900e5091c8bbb8c2f40089f", "646e8387347deb4f5fbe3e24753b4543"], + ] diff --git a/Tests/AudioKitTests/WorkStealingQueueTests.swift b/Tests/AudioKitTests/WorkStealingQueueTests.swift new file mode 100644 index 0000000000..337087c2b7 --- /dev/null +++ b/Tests/AudioKitTests/WorkStealingQueueTests.swift @@ -0,0 +1,43 @@ +// Copyright AudioKit. All Rights Reserved. Revision History at http://github.com/AudioKit/AudioKit/ + +import XCTest +import AudioKit + +extension Int: DefaultInit { + public init() { self = 0 } +} + +final class WorkStealingQueueTests: XCTestCase { + + func testBasic() throws { + + let queue = WorkStealingQueue() + + for i in 0..<1000 { + queue.push(i) + } + + let owner = Thread { + while !queue.isEmpty { + if let item = queue.pop() { + print("popped \(item)") + } + } + } + + let thief = Thread { + while !queue.isEmpty { + if let item = queue.steal() { + print("stole \(item)") + } + } + } + + owner.start() + thief.start() + + sleep(2) + + } + +}