From c589361e8dbf7247c9a1203ad3572f593207c757 Mon Sep 17 00:00:00 2001 From: Josip Cavar Date: Sun, 25 Feb 2024 21:54:05 +0100 Subject: [PATCH 1/3] Implement RenderMeasurer RenderMeasurer measures CPU load of given audio unit. It can be used for user facing CPU meter, but also for automated performance testing. --- Sources/AudioKitEX/Nodes/RenderMeasurer.swift | 40 +++++++++++++ .../CAudioKitEX/Nodes/RenderMeasurerDSP.mm | 56 +++++++++++++++++++ Sources/CAudioKitEX/include/RenderMeasurer.h | 15 +++++ .../AudioKitEXTests/RenderMeasurerTests.swift | 46 +++++++++++++++ 4 files changed, 157 insertions(+) create mode 100644 Sources/AudioKitEX/Nodes/RenderMeasurer.swift create mode 100644 Sources/CAudioKitEX/Nodes/RenderMeasurerDSP.mm create mode 100644 Sources/CAudioKitEX/include/RenderMeasurer.h create mode 100644 Tests/AudioKitEXTests/RenderMeasurerTests.swift diff --git a/Sources/AudioKitEX/Nodes/RenderMeasurer.swift b/Sources/AudioKitEX/Nodes/RenderMeasurer.swift new file mode 100644 index 0000000..38356e0 --- /dev/null +++ b/Sources/AudioKitEX/Nodes/RenderMeasurer.swift @@ -0,0 +1,40 @@ +// Copyright AudioKit. All Rights Reserved. Revision History at http://github.com/AudioKit/AudioKit/ + +import AVFAudio +import CAudioKitEX + +/// A class to measure the proportion of buffer time +/// audio unit is spending in its render block +/// It can be used to measure CPU usage of the whole audio chain +/// by attaching it to `AVAudioEngine.outputNode`, +/// as well as any other audio unit. +public class RenderMeasurer { + private let renderMeasurer = akRenderMeasurerCreate() + private let node: AUAudioUnit + private let token: Int + private let timebaseRatio: Double + + public init(node: AUAudioUnit) { + self.node = node + var timebase = mach_timebase_info_data_t(numer: 0, denom: 0) + let status = mach_timebase_info(&timebase) + assert(status == 0) + timebaseRatio = Double(timebase.numer) / Double(timebase.denom) + let observer = akRenderMeasurerCreateObserver(renderMeasurer) + self.token = node.token(byAddingRenderObserver: observer!) + } + + deinit { + node.removeRenderObserver(token) + } + + /// Returns the proportion of buffer time + /// audio unit is spending in its render block + /// This is usually number between 0 - 1, but + /// it can be higher in case of dropouts + public func usage() -> Double { + let sampleRate = node.outputBusses[0].format.sampleRate + let currentUsage = akRenderMeasurerGetUsage(renderMeasurer) + return Double(currentUsage) * timebaseRatio * sampleRate / 1_000_000_000 + } +} diff --git a/Sources/CAudioKitEX/Nodes/RenderMeasurerDSP.mm b/Sources/CAudioKitEX/Nodes/RenderMeasurerDSP.mm new file mode 100644 index 0000000..dfe1a4f --- /dev/null +++ b/Sources/CAudioKitEX/Nodes/RenderMeasurerDSP.mm @@ -0,0 +1,56 @@ +// Copyright AudioKit. All Rights Reserved. Revision History at http://github.com/AudioKit/AudioKit/ + +#include "DSPBase.h" +#import "CAudioKit.h" +#include +#include "RenderMeasurer.h" + +struct RenderMeasurer { + +public: + RenderMeasurer() { + usage = 0; + } + + static RenderMeasurer *_Nonnull create() { + RenderMeasurer* measurer = new RenderMeasurer(); + return measurer; + } + + _Nonnull AURenderObserver createObserver() { + auto sharedThis = std::shared_ptr(this); + return ^void(AudioUnitRenderActionFlags actionFlags, + const AudioTimeStamp *timestamp, + AUAudioFrameCount frameCount, + NSInteger outputBusNumber) + { + uint64 time = mach_absolute_time(); + if (actionFlags == kAudioUnitRenderAction_PreRender) { + sharedThis->startTime = time; + return; + } + uint64 endTime = time; + sharedThis->usage.store((double)(endTime - sharedThis->startTime) / (double)frameCount); + }; + } + + double currentUsage() { + return usage.load(); + } + +private: + std::atomic usage; + uint64 startTime; +}; + +RenderMeasurerRef akRenderMeasurerCreate(void) { + return new RenderMeasurer(); +} + +AURenderObserver akRenderMeasurerCreateObserver(RenderMeasurerRef measurer) { + return measurer->createObserver(); +} + +double akRenderMeasurerGetUsage(RenderMeasurerRef measurer) { + return measurer->currentUsage(); +} diff --git a/Sources/CAudioKitEX/include/RenderMeasurer.h b/Sources/CAudioKitEX/include/RenderMeasurer.h new file mode 100644 index 0000000..6f7651b --- /dev/null +++ b/Sources/CAudioKitEX/include/RenderMeasurer.h @@ -0,0 +1,15 @@ +// Copyright AudioKit. All Rights Reserved. Revision History at http://github.com/AudioKit/AudioKit/ + +#pragma once + +#include + +typedef struct RenderMeasurer* RenderMeasurerRef; + +CF_EXTERN_C_BEGIN + +RenderMeasurerRef akRenderMeasurerCreate(void); +AURenderObserver akRenderMeasurerCreateObserver(RenderMeasurerRef measurer); +double akRenderMeasurerGetUsage(RenderMeasurerRef measurer); + +CF_EXTERN_C_END diff --git a/Tests/AudioKitEXTests/RenderMeasurerTests.swift b/Tests/AudioKitEXTests/RenderMeasurerTests.swift new file mode 100644 index 0000000..07241ff --- /dev/null +++ b/Tests/AudioKitEXTests/RenderMeasurerTests.swift @@ -0,0 +1,46 @@ +// Copyright AudioKit. All Rights Reserved. Revision History at http://github.com/AudioKit/AudioKit/ + +import AudioKit +import AudioKitEX +import XCTest +import AVFAudio +import CAudioKitEX + +class RenderMeasurerTests: XCTestCase { + let engine = AVAudioEngine() + var sleepProporition: Float = 1 + lazy var source = AVAudioSourceNode { _, _, frameCount, _ -> OSStatus in + usleep(UInt32(Float(frameCount) / 44100 * 1000 * 1000 * self.sleepProporition)) + return noErr + } + + override func setUp() { + engine.attach(source) + engine.connect(source, to: engine.mainMixerNode, format: nil) + try! engine.start() + } + + override func tearDown() { + engine.stop() + } + + func testUsageHigherThen1() async throws { + self.sleepProporition = 1 + let measurer = RenderMeasurer(node: source.auAudioUnit) + for _ in 1...10 { + try await Task.sleep(nanoseconds: 1_000_000_00) + XCTAssertGreaterThanOrEqual(measurer.usage(), 1) + } + } + + func testUsageHigherThen05() async throws { + self.sleepProporition = 0.5 + let measurer = RenderMeasurer(node: source.auAudioUnit) + for _ in 1...10 { + try await Task.sleep(nanoseconds: 1_000_000_00) + let usage = measurer.usage() + XCTAssertGreaterThanOrEqual(usage, 0.5) + XCTAssertLessThanOrEqual(usage, 1) + } + } +} From aca2dfe51a51663cb31ffa991d172cc7df048734 Mon Sep 17 00:00:00 2001 From: Josip Cavar Date: Sat, 16 Mar 2024 13:03:04 +0100 Subject: [PATCH 2/3] Update github actions Swift versions to latest --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 4c665f4..add1714 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -14,7 +14,7 @@ jobs: with: scheme: AudioKitEX platforms: iOS macOS tvOS - swift-versions: 5.5 5.6 + swift-versions: 5.9 # Send notification to Discord on failure. send_notification: From c759776f949d99970ebff340c622007b5040e786 Mon Sep 17 00:00:00 2001 From: Josip Cavar Date: Sat, 16 Mar 2024 13:14:58 +0100 Subject: [PATCH 3/3] Use uint64_t instead of uint64 --- Sources/CAudioKitEX/Nodes/RenderMeasurerDSP.mm | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/CAudioKitEX/Nodes/RenderMeasurerDSP.mm b/Sources/CAudioKitEX/Nodes/RenderMeasurerDSP.mm index dfe1a4f..14dab80 100644 --- a/Sources/CAudioKitEX/Nodes/RenderMeasurerDSP.mm +++ b/Sources/CAudioKitEX/Nodes/RenderMeasurerDSP.mm @@ -24,12 +24,12 @@ _Nonnull AURenderObserver createObserver() { AUAudioFrameCount frameCount, NSInteger outputBusNumber) { - uint64 time = mach_absolute_time(); + uint64_t time = mach_absolute_time(); if (actionFlags == kAudioUnitRenderAction_PreRender) { sharedThis->startTime = time; return; } - uint64 endTime = time; + uint64_t endTime = time; sharedThis->usage.store((double)(endTime - sharedThis->startTime) / (double)frameCount); }; } @@ -40,7 +40,7 @@ _Nonnull AURenderObserver createObserver() { private: std::atomic usage; - uint64 startTime; + uint64_t startTime; }; RenderMeasurerRef akRenderMeasurerCreate(void) {