Skip to content

Commit 028c7b4

Browse files
authored
Merge pull request #28 from jcavar/feature/render-measure
Implement RenderMeasurer
2 parents 9ac80b2 + c759776 commit 028c7b4

File tree

5 files changed

+158
-1
lines changed

5 files changed

+158
-1
lines changed

.github/workflows/tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ jobs:
1414
with:
1515
scheme: AudioKitEX
1616
platforms: iOS macOS tvOS
17-
swift-versions: 5.5 5.6
17+
swift-versions: 5.9
1818

1919
# Send notification to Discord on failure.
2020
send_notification:
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_t time = mach_absolute_time();
28+
if (actionFlags == kAudioUnitRenderAction_PreRender) {
29+
sharedThis->startTime = time;
30+
return;
31+
}
32+
uint64_t 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_t 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)