Skip to content
Draft
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
6 changes: 4 additions & 2 deletions src-tauri/crates/berd-call/native/siri_tts_bridge.m
Original file line number Diff line number Diff line change
Expand Up @@ -906,13 +906,15 @@ - (void)stop {
self.stopped = YES;
self.pendingBuffers = 0;
}
// Render-notify callbacks copy these blocks on the audio thread. Stop
// output before unregistering them so no callback can copy a freed block.
[self.player stop];
[self.engine stop];
for (NSUInteger i = 0; i < self.renderObserverTokens.count; i++) {
[self.renderUnits[i] removeRenderObserver:self.renderObserverTokens[i].integerValue];
}
self.renderObserverTokens = nil;
self.renderUnits = nil;
[self.player stop];
[self.engine stop];
}

- (void)dealloc { [self stop]; }
Expand Down
52 changes: 51 additions & 1 deletion src-tauri/crates/berd-call/tests/native/render_health.m
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ @interface TestRunningEngine : NSObject
@property(nonatomic, strong) TestAudioNode *mainMixerNode;
@property(nonatomic, weak) BerdPocketAudioPlayer *player;
@property(nonatomic, assign) BOOL running;
@property(nonatomic, copy) void (^onStop)(void);
- (BOOL)isRunning;
- (void)stop;
@end
Expand All @@ -28,9 +29,57 @@ - (BOOL)isRunning {
dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC)) == 0);
return self.running;
}
- (void)stop {}
- (void)stop {
if (self.onStop) self.onStop();
self.running = NO;
}
@end

@interface TestRenderUnit : NSObject
@property(nonatomic, weak) TestRunningEngine *engine;
@property(nonatomic, copy) AURenderObserver observer;
- (NSInteger)tokenByAddingRenderObserver:(AURenderObserver)observer;
- (void)removeRenderObserver:(NSInteger)token;
@end
@implementation TestRenderUnit
- (NSInteger)tokenByAddingRenderObserver:(AURenderObserver)observer {
self.observer = observer;
return 1;
}
- (void)removeRenderObserver:(NSInteger)token {
assert(!self.engine.running && "render observers must outlive active output");
assert(token == 1 && self.observer);
self.observer = nil;
}
@end

static void observersSurviveEngineShutdown(void) {
TestRunningEngine *engine = [TestRunningEngine new];
engine.running = YES;
NSMutableArray<TestRenderUnit *> *units = [NSMutableArray new];
TestAudioNode *nodes[3];
for (NSUInteger i = 0; i < 3; i++) {
TestRenderUnit *unit = [TestRenderUnit new];
unit.engine = engine;
[units addObject:unit];
nodes[i] = [TestAudioNode new];
nodes[i].AUAudioUnit = (AUAudioUnit *)unit;
}
engine.outputNode = nodes[0];
engine.mainMixerNode = nodes[2];
engine.onStop = ^{
// Output may finish a render callback while the engine is stopping.
for (TestRenderUnit *unit in units) assert(unit.observer);
};
BerdPocketAudioPlayer *player = [BerdPocketAudioPlayer new];
player.engine = (AVAudioEngine *)engine;
player.timePitch = (AVAudioUnitTimePitch *)nodes[1];
[player observeRenderErrors];
[player stop];
for (TestRenderUnit *unit in units) assert(!unit.observer);
[player stop];
}

static AUAudioUnit *offlineUnit(void) {
AudioComponentDescription description = {
kAudioUnitType_FormatConverter, kAudioUnitSubType_NewTimePitch,
Expand All @@ -49,6 +98,7 @@ - (void)stop {}

int main(void) {
@autoreleasepool {
observersSurviveEngineShutdown();
for (NSUInteger failedNode = 0; failedNode < 3; failedNode++) {
AUAudioUnit *units[] = {offlineUnit(), offlineUnit(), offlineUnit()};
TestAudioNode *nodes[3];
Expand Down
Loading