Skip to content

Commit

Permalink
Merge pull request #28 from jcavar/feature/render-measure
Browse files Browse the repository at this point in the history
Implement RenderMeasurer
  • Loading branch information
aure authored Mar 20, 2024
2 parents 9ac80b2 + c759776 commit 028c7b4
Show file tree
Hide file tree
Showing 5 changed files with 158 additions and 1 deletion.
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);
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)
}
}
}

0 comments on commit 028c7b4

Please sign in to comment.