Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement RenderMeasurer #28

Merged
merged 3 commits into from
Mar 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
40 changes: 40 additions & 0 deletions Sources/AudioKitEX/Nodes/RenderMeasurer.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
56 changes: 56 additions & 0 deletions Sources/CAudioKitEX/Nodes/RenderMeasurerDSP.mm
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// Copyright AudioKit. All Rights Reserved. Revision History at http://github.com/AudioKit/AudioKit/

#include "DSPBase.h"
#import "CAudioKit.h"
#include <mach/mach_time.h>
#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<RenderMeasurer>(this);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this pattern is realtime safe, but I would like someone more experienced with C++ to comment on it.

return ^void(AudioUnitRenderActionFlags actionFlags,
const AudioTimeStamp *timestamp,
AUAudioFrameCount frameCount,
NSInteger outputBusNumber)
{
uint64_t time = mach_absolute_time();
if (actionFlags == kAudioUnitRenderAction_PreRender) {
sharedThis->startTime = time;
return;
}
uint64_t endTime = time;
sharedThis->usage.store((double)(endTime - sharedThis->startTime) / (double)frameCount);
};
}

double currentUsage() {
return usage.load();
}

private:
std::atomic<double> usage;
uint64_t startTime;
};

RenderMeasurerRef akRenderMeasurerCreate(void) {
return new RenderMeasurer();
}

AURenderObserver akRenderMeasurerCreateObserver(RenderMeasurerRef measurer) {
return measurer->createObserver();
}

double akRenderMeasurerGetUsage(RenderMeasurerRef measurer) {
return measurer->currentUsage();
}
15 changes: 15 additions & 0 deletions Sources/CAudioKitEX/include/RenderMeasurer.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Copyright AudioKit. All Rights Reserved. Revision History at http://github.com/AudioKit/AudioKit/

#pragma once

#include <AudioUnit/AudioUnit.h>

typedef struct RenderMeasurer* RenderMeasurerRef;

CF_EXTERN_C_BEGIN

RenderMeasurerRef akRenderMeasurerCreate(void);
AURenderObserver akRenderMeasurerCreateObserver(RenderMeasurerRef measurer);
double akRenderMeasurerGetUsage(RenderMeasurerRef measurer);

CF_EXTERN_C_END
46 changes: 46 additions & 0 deletions Tests/AudioKitEXTests/RenderMeasurerTests.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
Loading