Skip to content

Commit c589361

Browse files
committed
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.
1 parent 9ac80b2 commit c589361

File tree

4 files changed

+157
-0
lines changed

4 files changed

+157
-0
lines changed
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// Copyright AudioKit. All Rights Reserved. Revision History at http://github.com/AudioKit/AudioKit/
2+
3+
import AVFAudio
4+
import CAudioKitEX
5+
6+
/// A class to measure the proportion of buffer time
7+
/// audio unit is spending in its render block
8+
/// It can be used to measure CPU usage of the whole audio chain
9+
/// by attaching it to `AVAudioEngine.outputNode`,
10+
/// as well as any other audio unit.
11+
public class RenderMeasurer {
12+
private let renderMeasurer = akRenderMeasurerCreate()
13+
private let node: AUAudioUnit
14+
private let token: Int
15+
private let timebaseRatio: Double
16+
17+
public init(node: AUAudioUnit) {
18+
self.node = node
19+
var timebase = mach_timebase_info_data_t(numer: 0, denom: 0)
20+
let status = mach_timebase_info(&timebase)
21+
assert(status == 0)
22+
timebaseRatio = Double(timebase.numer) / Double(timebase.denom)
23+
let observer = akRenderMeasurerCreateObserver(renderMeasurer)
24+
self.token = node.token(byAddingRenderObserver: observer!)
25+
}
26+
27+
deinit {
28+
node.removeRenderObserver(token)
29+
}
30+
31+
/// Returns the proportion of buffer time
32+
/// audio unit is spending in its render block
33+
/// This is usually number between 0 - 1, but
34+
/// it can be higher in case of dropouts
35+
public func usage() -> Double {
36+
let sampleRate = node.outputBusses[0].format.sampleRate
37+
let currentUsage = akRenderMeasurerGetUsage(renderMeasurer)
38+
return Double(currentUsage) * timebaseRatio * sampleRate / 1_000_000_000
39+
}
40+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// Copyright AudioKit. All Rights Reserved. Revision History at http://github.com/AudioKit/AudioKit/
2+
3+
#include "DSPBase.h"
4+
#import "CAudioKit.h"
5+
#include <mach/mach_time.h>
6+
#include "RenderMeasurer.h"
7+
8+
struct RenderMeasurer {
9+
10+
public:
11+
RenderMeasurer() {
12+
usage = 0;
13+
}
14+
15+
static RenderMeasurer *_Nonnull create() {
16+
RenderMeasurer* measurer = new RenderMeasurer();
17+
return measurer;
18+
}
19+
20+
_Nonnull AURenderObserver createObserver() {
21+
auto sharedThis = std::shared_ptr<RenderMeasurer>(this);
22+
return ^void(AudioUnitRenderActionFlags actionFlags,
23+
const AudioTimeStamp *timestamp,
24+
AUAudioFrameCount frameCount,
25+
NSInteger outputBusNumber)
26+
{
27+
uint64 time = mach_absolute_time();
28+
if (actionFlags == kAudioUnitRenderAction_PreRender) {
29+
sharedThis->startTime = time;
30+
return;
31+
}
32+
uint64 endTime = time;
33+
sharedThis->usage.store((double)(endTime - sharedThis->startTime) / (double)frameCount);
34+
};
35+
}
36+
37+
double currentUsage() {
38+
return usage.load();
39+
}
40+
41+
private:
42+
std::atomic<double> usage;
43+
uint64 startTime;
44+
};
45+
46+
RenderMeasurerRef akRenderMeasurerCreate(void) {
47+
return new RenderMeasurer();
48+
}
49+
50+
AURenderObserver akRenderMeasurerCreateObserver(RenderMeasurerRef measurer) {
51+
return measurer->createObserver();
52+
}
53+
54+
double akRenderMeasurerGetUsage(RenderMeasurerRef measurer) {
55+
return measurer->currentUsage();
56+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Copyright AudioKit. All Rights Reserved. Revision History at http://github.com/AudioKit/AudioKit/
2+
3+
#pragma once
4+
5+
#include <AudioUnit/AudioUnit.h>
6+
7+
typedef struct RenderMeasurer* RenderMeasurerRef;
8+
9+
CF_EXTERN_C_BEGIN
10+
11+
RenderMeasurerRef akRenderMeasurerCreate(void);
12+
AURenderObserver akRenderMeasurerCreateObserver(RenderMeasurerRef measurer);
13+
double akRenderMeasurerGetUsage(RenderMeasurerRef measurer);
14+
15+
CF_EXTERN_C_END
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Copyright AudioKit. All Rights Reserved. Revision History at http://github.com/AudioKit/AudioKit/
2+
3+
import AudioKit
4+
import AudioKitEX
5+
import XCTest
6+
import AVFAudio
7+
import CAudioKitEX
8+
9+
class RenderMeasurerTests: XCTestCase {
10+
let engine = AVAudioEngine()
11+
var sleepProporition: Float = 1
12+
lazy var source = AVAudioSourceNode { _, _, frameCount, _ -> OSStatus in
13+
usleep(UInt32(Float(frameCount) / 44100 * 1000 * 1000 * self.sleepProporition))
14+
return noErr
15+
}
16+
17+
override func setUp() {
18+
engine.attach(source)
19+
engine.connect(source, to: engine.mainMixerNode, format: nil)
20+
try! engine.start()
21+
}
22+
23+
override func tearDown() {
24+
engine.stop()
25+
}
26+
27+
func testUsageHigherThen1() async throws {
28+
self.sleepProporition = 1
29+
let measurer = RenderMeasurer(node: source.auAudioUnit)
30+
for _ in 1...10 {
31+
try await Task.sleep(nanoseconds: 1_000_000_00)
32+
XCTAssertGreaterThanOrEqual(measurer.usage(), 1)
33+
}
34+
}
35+
36+
func testUsageHigherThen05() async throws {
37+
self.sleepProporition = 0.5
38+
let measurer = RenderMeasurer(node: source.auAudioUnit)
39+
for _ in 1...10 {
40+
try await Task.sleep(nanoseconds: 1_000_000_00)
41+
let usage = measurer.usage()
42+
XCTAssertGreaterThanOrEqual(usage, 0.5)
43+
XCTAssertLessThanOrEqual(usage, 1)
44+
}
45+
}
46+
}

0 commit comments

Comments
 (0)