From 7813712df3d0782b7bc38e71d2701961eccf9337 Mon Sep 17 00:00:00 2001 From: Christopher Snowhill Date: Thu, 9 Jun 2022 18:15:44 -0700 Subject: [PATCH] [Audio Threads] Correctly set real time priority I'm not sure about macOS Ventura, but stable releases of macOS, at least on Intel, require that threads joining Audio Interval workgroups already be set to run as real-time before joining. Not doing this results in an uncaught exception and a crash. Signed-off-by: Christopher Snowhill --- Audio/Chain/Node.m | 92 ++++++++++++++++++++++-- Audio/CogAudio.xcodeproj/project.pbxproj | 6 +- 2 files changed, 90 insertions(+), 8 deletions(-) diff --git a/Audio/Chain/Node.m b/Audio/Chain/Node.m index ec87734b3..d02953569 100644 --- a/Audio/Chain/Node.m +++ b/Audio/Chain/Node.m @@ -13,10 +13,12 @@ #import "OutputCoreAudio.h" +#import + #import -// This workgroup attribute isn't currently used. Set it to NULL. -static os_workgroup_attr_t _Nullable attr = nil; +// This workgroup attribute needs to be initialized. +static os_workgroup_attr_s attr = OS_WORKGROUP_ATTR_INITIALIZER_DEFAULT; // One nanosecond in seconds. static const double kOneNanosecond = 1.0e9; @@ -27,6 +29,86 @@ static const double kIOIntervalTime = 0.020; // The clock identifier that specifies interval timestamps. static const os_clockid_t clockId = OS_CLOCK_MACH_ABSOLUTE_TIME; +// Enables time-contraint policy and priority suitable for low-latency, +// glitch-resistant audio. +void SetPriorityRealtimeAudio(mach_port_t mach_thread_id) { + kern_return_t result; + + // Increase thread priority to real-time. + + // Please note that the thread_policy_set() calls may fail in + // rare cases if the kernel decides the system is under heavy load + // and is unable to handle boosting the thread priority. + // In these cases we just return early and go on with life. + + // Make thread fixed priority. + thread_extended_policy_data_t policy; + policy.timeshare = 0; // Set to 1 for a non-fixed thread. + result = thread_policy_set(mach_thread_id, + THREAD_EXTENDED_POLICY, + (thread_policy_t)&policy, + THREAD_EXTENDED_POLICY_COUNT); + if(result != KERN_SUCCESS) { + DLog(@"thread_policy_set extended policy failure: %d", result); + return; + } + + // Set to relatively high priority. + thread_precedence_policy_data_t precedence; + precedence.importance = 63; + result = thread_policy_set(mach_thread_id, + THREAD_PRECEDENCE_POLICY, + (thread_policy_t)&precedence, + THREAD_PRECEDENCE_POLICY_COUNT); + if(result != KERN_SUCCESS) { + DLog(@"thread_policy_set precedence policy failure: %d", result); + return; + } + + // Most important, set real-time constraints. + + // Define the guaranteed and max fraction of time for the audio thread. + // These "duty cycle" values can range from 0 to 1. A value of 0.5 + // means the scheduler would give half the time to the thread. + // These values have empirically been found to yield good behavior. + // Good means that audio performance is high and other threads won't starve. + const double kGuaranteedAudioDutyCycle = 0.75; + const double kMaxAudioDutyCycle = 0.85; + + // Define constants determining how much time the audio thread can + // use in a given time quantum. All times are in milliseconds. + + // About 128 frames @44.1KHz + const double kTimeQuantum = 2.9; + + // Time guaranteed each quantum. + const double kAudioTimeNeeded = kGuaranteedAudioDutyCycle * kTimeQuantum; + + // Maximum time each quantum. + const double kMaxTimeAllowed = kMaxAudioDutyCycle * kTimeQuantum; + + // Get the conversion factor from milliseconds to absolute time + // which is what the time-constraints call needs. + mach_timebase_info_data_t tb_info; + mach_timebase_info(&tb_info); + double ms_to_abs_time = + ((double)tb_info.denom / (double)tb_info.numer) * 1000000; + + thread_time_constraint_policy_data_t time_constraints; + time_constraints.period = kTimeQuantum * ms_to_abs_time; + time_constraints.computation = kAudioTimeNeeded * ms_to_abs_time; + time_constraints.constraint = kMaxTimeAllowed * ms_to_abs_time; + time_constraints.preemptible = 0; + + result = thread_policy_set(mach_thread_id, + THREAD_TIME_CONSTRAINT_POLICY, + (thread_policy_t)&time_constraints, + THREAD_TIME_CONSTRAINT_POLICY_COUNT); + if(result != KERN_SUCCESS) { + DLog(@"thread_policy_set constraint policy failure: %d", result); + } +} + @implementation Node - (id)initWithController:(id)c previous:(id)p { @@ -47,8 +129,6 @@ static const os_clockid_t clockId = OS_CLOCK_MACH_ABSOLUTE_TIME; nodeLossless = NO; if(@available(macOS 11, *)) { - workgroup = AudioWorkIntervalCreate("Node Work Interval", clockId, attr); - // Get the mach time info. struct mach_timebase_info timeBaseInfo; mach_timebase_info(&timeBaseInfo); @@ -131,6 +211,10 @@ static const os_clockid_t clockId = OS_CLOCK_MACH_ABSOLUTE_TIME; - (void)followWorkgroup { if(@available(macOS 11, *)) { if(!wg) { + if(!workgroup) { + workgroup = AudioWorkIntervalCreate([[NSString stringWithFormat:@"%@ Work Interval", [self className]] UTF8String], clockId, &attr); + SetPriorityRealtimeAudio(pthread_mach_thread_np(pthread_self())); + } wg = workgroup; if(wg) { int result = os_workgroup_join(wg, &wgToken); diff --git a/Audio/CogAudio.xcodeproj/project.pbxproj b/Audio/CogAudio.xcodeproj/project.pbxproj index 9123faabb..f892a6251 100644 --- a/Audio/CogAudio.xcodeproj/project.pbxproj +++ b/Audio/CogAudio.xcodeproj/project.pbxproj @@ -77,7 +77,6 @@ 839366681815923C006DD712 /* CogPluginMulti.m in Sources */ = {isa = PBXBuildFile; fileRef = 839366661815923C006DD712 /* CogPluginMulti.m */; }; 8399CF2C27B5D1D5008751F1 /* NSDictionary+Merge.h in Headers */ = {isa = PBXBuildFile; fileRef = 8399CF2A27B5D1D4008751F1 /* NSDictionary+Merge.h */; }; 8399CF2D27B5D1D5008751F1 /* NSDictionary+Merge.m in Sources */ = {isa = PBXBuildFile; fileRef = 8399CF2B27B5D1D4008751F1 /* NSDictionary+Merge.m */; }; - 83B69B262845A4B200D2435A /* pffft_double.c in Sources */ = {isa = PBXBuildFile; fileRef = 83B69B252845A4B200D2435A /* pffft_double.c */; }; 83B69B752845DF6500D2435A /* pffft_double.h in Headers */ = {isa = PBXBuildFile; fileRef = 83B69B6B2845DF6500D2435A /* pffft_double.h */; }; 83B69B762845DF6500D2435A /* pf_neon_double_from_avx.h in Headers */ = {isa = PBXBuildFile; fileRef = 83B69B6D2845DF6500D2435A /* pf_neon_double_from_avx.h */; }; 83B69B772845DF6500D2435A /* pf_double.h in Headers */ = {isa = PBXBuildFile; fileRef = 83B69B6E2845DF6500D2435A /* pf_double.h */; }; @@ -86,6 +85,7 @@ 83B69B7A2845DF6500D2435A /* pf_avx_double.h in Headers */ = {isa = PBXBuildFile; fileRef = 83B69B712845DF6500D2435A /* pf_avx_double.h */; }; 83B69B7B2845DF6500D2435A /* pf_scalar_double.h in Headers */ = {isa = PBXBuildFile; fileRef = 83B69B722845DF6500D2435A /* pf_scalar_double.h */; }; 83B69B7C2845DF6500D2435A /* pffft_priv_impl.h in Headers */ = {isa = PBXBuildFile; fileRef = 83B69B732845DF6500D2435A /* pffft_priv_impl.h */; }; + 83D40B572852CB3B003BB85C /* pffft_double.c in Sources */ = {isa = PBXBuildFile; fileRef = 83B69B742845DF6500D2435A /* pffft_double.c */; }; 83F18B1E27D1E8EF00385946 /* CDSPHBDownsampler.h in Headers */ = {isa = PBXBuildFile; fileRef = 83F18ADF27D1E8EF00385946 /* CDSPHBDownsampler.h */; }; 83F18B3327D1E8EF00385946 /* CDSPSincFilterGen.h in Headers */ = {isa = PBXBuildFile; fileRef = 83F18AF827D1E8EF00385946 /* CDSPSincFilterGen.h */; }; 83F18B3427D1E8EF00385946 /* r8butil.h in Headers */ = {isa = PBXBuildFile; fileRef = 83F18AF927D1E8EF00385946 /* r8butil.h */; }; @@ -209,7 +209,6 @@ 839366661815923C006DD712 /* CogPluginMulti.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CogPluginMulti.m; sourceTree = ""; }; 8399CF2A27B5D1D4008751F1 /* NSDictionary+Merge.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "NSDictionary+Merge.h"; path = "../../Utils/NSDictionary+Merge.h"; sourceTree = ""; }; 8399CF2B27B5D1D4008751F1 /* NSDictionary+Merge.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "NSDictionary+Merge.m"; path = "../../Utils/NSDictionary+Merge.m"; sourceTree = ""; }; - 83B69B252845A4B200D2435A /* pffft_double.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; name = pffft_double.c; path = "ThirdParty/r8brain-free-src/pffft_double/pffft_double.c"; sourceTree = ""; }; 83B69B6B2845DF6500D2435A /* pffft_double.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = pffft_double.h; sourceTree = ""; }; 83B69B6D2845DF6500D2435A /* pf_neon_double_from_avx.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = pf_neon_double_from_avx.h; sourceTree = ""; }; 83B69B6E2845DF6500D2435A /* pf_double.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = pf_double.h; sourceTree = ""; }; @@ -279,7 +278,6 @@ 0867D691FE84028FC02AAC07 /* CogAudio */ = { isa = PBXGroup; children = ( - 83B69B252845A4B200D2435A /* pffft_double.c */, 08FB77AEFE84172EC02AAC07 /* Classes */, 32C88DFF0371C24200C91783 /* Other Sources */, 089C1665FE841158C02AAC07 /* Resources */, @@ -698,7 +696,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 83B69B262845A4B200D2435A /* pffft_double.c in Sources */, + 83D40B572852CB3B003BB85C /* pffft_double.c in Sources */, 835EDD7B279FE23A001EDCCE /* HeadphoneFilter.mm in Sources */, 17D21CA20B8BE4BA00D1EBDE /* BufferChain.m in Sources */, 17D21CA60B8BE4BA00D1EBDE /* InputNode.m in Sources */,