forked from bitgapp/eqMac
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathEQMDevice.swift
More file actions
656 lines (574 loc) · 26.2 KB
/
Copy pathEQMDevice.swift
File metadata and controls
656 lines (574 loc) · 26.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
//
// EQMDevice.swift
// eqMac
//
// Created by Nodeful on 12/08/2021.
// Copyright © 2021 Bitgapp. All rights reserved.
//
import Foundation
import CoreAudio.AudioServerPlugIn
import Atomics
import Shared
class EQMDevice: EQMObject {
static var name = kEQMDeviceDefaultName
static var sampleRate = kDefaultSampleRate
static var running = false
static var shown = false {
didSet {
if (oldValue != shown) {
EQMDriver.propertiesUpdated(
objectId: kObjectID_Device,
changedProperties: [
]
)
}
}
}
static var latency: UInt32 = 0
static var ioCounter = ManagedAtomic<UInt64>(0)
static var anchorHostTime: UInt64 = 0
static var anchorSampleTime: UInt64 = 0
static var timestampCount: UInt64 = 0
static let ioMutex = Mutex()
static let ringBufferSize: UInt32 = 16384
static var ringBuffer: UnsafeMutablePointer<Float32>?
static func getDescription (for samplingRate: Float64) -> AudioStreamBasicDescription {
return AudioStreamBasicDescription(
mSampleRate: samplingRate,
mFormatID: kAudioFormatLinearPCM,
mFormatFlags: kAudioFormatFlagIsFloat | kAudioFormatFlagsNativeEndian | kAudioFormatFlagIsPacked,
mBytesPerPacket: kBytesPerFrame,
mFramesPerPacket: 1,
mBytesPerFrame: kBytesPerFrame,
mChannelsPerFrame: kChannelCount,
mBitsPerChannel: kBitsPerChannel,
mReserved: 0
)
}
static func hasProperty (objectID: AudioObjectID? = nil, address: AudioObjectPropertyAddress) -> Bool {
switch address.mSelector {
case kAudioObjectPropertyBaseClass,
kAudioObjectPropertyClass,
kAudioObjectPropertyOwner,
kAudioObjectPropertyName,
kAudioObjectPropertyManufacturer,
kAudioObjectPropertyOwnedObjects,
kAudioDevicePropertyDeviceUID,
kAudioDevicePropertyModelUID,
kAudioDevicePropertyTransportType,
kAudioDevicePropertyRelatedDevices,
kAudioDevicePropertyClockDomain,
kAudioDevicePropertyDeviceIsAlive,
kAudioDevicePropertyDeviceIsRunning,
kAudioObjectPropertyControlList,
kAudioDevicePropertyNominalSampleRate,
kAudioDevicePropertyAvailableNominalSampleRates,
kAudioDevicePropertyIsHidden,
kAudioDevicePropertyZeroTimeStampPeriod,
kAudioDevicePropertyIcon,
kAudioDevicePropertyStreams,
kAudioObjectPropertyCustomPropertyInfoList,
kAudioDevicePropertyDeviceCanBeDefaultSystemDevice,
EQMDeviceCustom.properties.shown,
EQMDeviceCustom.properties.version,
EQMDeviceCustom.properties.latency,
EQMDeviceCustom.properties.name:
return true
case kAudioDevicePropertyDeviceCanBeDefaultDevice,
kAudioDevicePropertyLatency,
kAudioDevicePropertySafetyOffset,
kAudioDevicePropertyPreferredChannelsForStereo,
kAudioDevicePropertyPreferredChannelLayout:
return address.mScope == kAudioObjectPropertyScopeInput || address.mScope == kAudioObjectPropertyScopeOutput
default:
return false
}
}
static func isPropertySettable (objectID: AudioObjectID? = nil, address: AudioObjectPropertyAddress) -> Bool {
switch address.mSelector {
case kAudioDevicePropertyNominalSampleRate,
EQMDeviceCustom.properties.shown,
EQMDeviceCustom.properties.latency,
EQMDeviceCustom.properties.name:
return true
default:
return false
}
}
static func getPropertyDataSize (objectID: AudioObjectID? = nil, address: AudioObjectPropertyAddress) -> UInt32? {
switch address.mSelector {
case kAudioObjectPropertyBaseClass: return Memory.sizeof(AudioClassID.self)
case kAudioObjectPropertyClass: return Memory.sizeof(AudioClassID.self)
case kAudioObjectPropertyOwner: return Memory.sizeof(AudioObjectID.self)
case kAudioObjectPropertyName: return Memory.sizeof(CFString.self)
case kAudioObjectPropertyManufacturer: return Memory.sizeof(CFString.self)
case kAudioObjectPropertyOwnedObjects:
switch (address.mScope) {
case kAudioObjectPropertyScopeGlobal: return 8 * Memory.sizeof(AudioObjectID.self)
case kAudioObjectPropertyScopeInput: return 4 * Memory.sizeof(AudioObjectID.self)
case kAudioObjectPropertyScopeOutput: return 4 * Memory.sizeof(AudioObjectID.self)
default:
return nil
}
case kAudioDevicePropertyDeviceUID: return Memory.sizeof(CFString.self)
case kAudioDevicePropertyModelUID: return Memory.sizeof(CFString.self)
case kAudioDevicePropertyTransportType: return Memory.sizeof(UInt32.self)
case kAudioDevicePropertyRelatedDevices: return Memory.sizeof(AudioObjectID.self)
case kAudioDevicePropertyClockDomain: return Memory.sizeof(UInt32.self)
case kAudioDevicePropertyDeviceIsAlive: return Memory.sizeof(AudioClassID.self)
case kAudioDevicePropertyDeviceIsRunning: return Memory.sizeof(UInt32.self)
case kAudioDevicePropertyDeviceCanBeDefaultDevice: return Memory.sizeof(UInt32.self)
case kAudioDevicePropertyDeviceCanBeDefaultSystemDevice: return Memory.sizeof(UInt32.self)
case kAudioDevicePropertyLatency: return Memory.sizeof(UInt32.self)
case kAudioDevicePropertyStreams:
switch(address.mScope) {
case kAudioObjectPropertyScopeGlobal: return 2 * Memory.sizeof(AudioObjectID.self)
case kAudioObjectPropertyScopeInput: return Memory.sizeof(AudioObjectID.self)
case kAudioObjectPropertyScopeOutput: return Memory.sizeof(AudioObjectID.self)
default:
return nil
}
case kAudioObjectPropertyControlList: return 3 * Memory.sizeof(AudioObjectID.self)
case kAudioDevicePropertySafetyOffset: return Memory.sizeof(UInt32.self)
case kAudioDevicePropertyNominalSampleRate: return Memory.sizeof(Float64.self)
case kAudioDevicePropertyAvailableNominalSampleRates: return UInt32(kEQMDeviceSupportedSampleRates.count) * Memory.sizeof(AudioValueRange.self)
case kAudioDevicePropertyIsHidden: return Memory.sizeof(UInt32.self)
case kAudioDevicePropertyPreferredChannelsForStereo: return 2 * Memory.sizeof(UInt32.self)
case kAudioDevicePropertyPreferredChannelLayout:
let layoutSize = Memory.sizeof(AudioChannelLayout.self)
// Layout will already contain size for 1 channel description
let channelsSize = Memory.sizeof(AudioChannelDescription.self) * kChannelCount - 1
return layoutSize + channelsSize
case kAudioDevicePropertyZeroTimeStampPeriod: return Memory.sizeof(UInt32.self)
case kAudioDevicePropertyIcon: return Memory.sizeof(CFURL.self)
case kAudioObjectPropertyCustomPropertyInfoList:
return Memory.sizeof(AudioServerPlugInCustomPropertyInfo.self) * EQMDeviceCustom.properties.count
case EQMDeviceCustom.properties.latency: return Memory.sizeof(CFNumber.self)
case EQMDeviceCustom.properties.shown: return Memory.sizeof(CFBoolean.self)
case EQMDeviceCustom.properties.version: return Memory.sizeof(CFString.self)
case EQMDeviceCustom.properties.name: return Memory.sizeof(CFString.self)
default:
return nil
}
}
static func getPropertyData (objectID: AudioObjectID? = nil, address: AudioObjectPropertyAddress, inData: UnsafeRawPointer?) -> EQMObjectProperty? {
switch address.mSelector {
case kAudioObjectPropertyBaseClass:
// The base class for kAudioDeviceClassID is kAudioObjectClassID
return .audioClassID(kAudioObjectClassID)
case kAudioObjectPropertyClass:
// The class is always kAudioDeviceClassID for devices created by drivers
return .audioClassID(kAudioDeviceClassID)
case kAudioObjectPropertyOwner:
// The device's owner is the plug-in object
return .audioObjectID(kObjectID_PlugIn)
case kAudioObjectPropertyName:
// This is the human readable name of the device.
return .string(name as CFString)
case kAudioObjectPropertyManufacturer:
// This is the human readable name of the maker of the plug-in.
return .string(kDeviceManufacturer as CFString)
case kAudioObjectPropertyOwnedObjects:
// Calculate the number of items that have been requested. Note that this
// number is allowed to be smaller than the actual size of the list. In such
// case, only that number of items will be returned
// The device owns its streams and controls. Note that what is returned here
// depends on the scope requested.
switch address.mScope {
case kAudioObjectPropertyScopeGlobal:
return .objectIDList([
kObjectID_Stream_Input,
kObjectID_Stream_Output,
kObjectID_Volume_Output_Master,
kObjectID_Mute_Output_Master,
kObjectID_DataSource_Output_Master,
])
case kAudioObjectPropertyScopeInput:
return .objectIDList([
kObjectID_Stream_Input,
])
case kAudioObjectPropertyScopeOutput:
return .objectIDList([
kObjectID_Stream_Output,
kObjectID_Volume_Output_Master,
kObjectID_Mute_Output_Master,
kObjectID_DataSource_Output_Master,
])
default: return .null()
}
case kAudioDevicePropertyDeviceUID:
// This is a CFString that is a persistent token that can identify the same
// audio device across boot sessions. Note that two instances of the same
// device must have different values for this property.
return .string(kDeviceUID as CFString)
case kAudioDevicePropertyModelUID:
// This is a CFString that is a persistent token that can identify audio
// devices that are the same kind of device. Note that two instances of the
// save device must have the same value for this property.
return .string(kDeviceModelUID as CFString)
case kAudioDevicePropertyTransportType:
// This value represents how the device is attached to the system. This can be
// any 32 bit integer, but common values for this property are defined in
// <CoreAudio/AudioHardwareBase.h>
return .integer(kAudioDeviceTransportTypeVirtual)
case kAudioDevicePropertyRelatedDevices:
// The related devices property identifys device objects that are very closely
// related. Generally, this is for relating devices that are packaged together
// in the hardware such as when the input side and the output side of a piece
// of hardware can be clocked separately and therefore need to be represented
// as separate AudioDevice objects. In such case, both devices would report
// that they are related to each other. Note that at minimum, a device is
// related to itself, so this list will always be at least one item long.
// Calculate the number of items that have been requested. Note that this
// number is allowed to be smaller than the actual size of the list. In such
// case, only that number of items will be returned
return .objectIDList([ kObjectID_Device ])
case kAudioDevicePropertyClockDomain:
// This property allows the device to declare what other devices it is
// synchronized with in hardware. The way it works is that if two devices have
// the same value for this property and the value is not zero, then the two
// devices are synchronized in hardware. Note that a device that either can't
// be synchronized with others or doesn't know should return 0 for this
// property.
return .integer(0)
case kAudioDevicePropertyDeviceIsAlive:
// This property returns whether or not the device is alive. Note that it is
// note uncommon for a device to be dead but still momentarily availble in the
// device list. In the case of this device, it will always be alive.
return .integer(1)
case kAudioDevicePropertyDeviceIsRunning:
// This property returns whether or not IO is running for the device. Note that
// we need to take both the state lock to check this value for thread safety.
return .integer(running ? 1 : 0)
case kAudioDevicePropertyDeviceCanBeDefaultDevice:
// This property returns whether or not the device wants to be able to be the
// default device for content. This is the device that iTunes and QuickTime
// will use to play their content on and FaceTime will use as its microphone.
// Nearly all devices should allow for this.
if address.mScope == kAudioObjectPropertyScopeInput {
return .integer(0)
} else {
return .integer(1)
}
case kAudioDevicePropertyDeviceCanBeDefaultSystemDevice:
// This property returns whether or not the device wants to be the system
// default device. This is the device that is used to play interface sounds and
// other incidental or UI-related sounds on. Most devices should allow this
// although devices with lots of latency may not want to.
return .integer(1)
case kAudioDevicePropertyLatency:
// This property returns the presentation latency of the device. For this,
// device, the value is 0 due to the fact that it always vends silence.
if address.mScope == kAudioObjectPropertyScopeInput {
return .integer(0)
} else {
return .integer(latency)
}
case kAudioDevicePropertyStreams:
// Calculate the number of items that have been requested. Note that this
// number is allowed to be smaller than the actual size of the list. In such
// case, only that number of items will be returned
// Note that what is returned here depends on the scope requested.
switch address.mScope {
case kAudioObjectPropertyScopeGlobal:
// global scope means return all streams
return .objectIDList([ kObjectID_Stream_Input, kObjectID_Stream_Output ])
case kAudioObjectPropertyScopeInput:
// input scope means just the objects on the input side
return .audioObjectID(kObjectID_Stream_Input)
case kAudioObjectPropertyScopeOutput:
// output scope means just the objects on the output side
return .audioObjectID(kObjectID_Stream_Output)
default:
return .null()
}
case kAudioObjectPropertyControlList:
// Calculate the number of items that have been requested. Note that this
// number is allowed to be smaller than the actual size of the list. In such
// case, only that number of items will be returned
return .objectIDList([
kObjectID_Volume_Output_Master,
kObjectID_Mute_Output_Master,
kObjectID_DataSource_Output_Master
])
case kAudioDevicePropertySafetyOffset:
// This property returns the how close to now the HAL can read and write. For
// this, device, the value is 0 due to the fact that it always vends silence.
return .integer(0)
case kAudioDevicePropertyNominalSampleRate:
// This property returns the nominal sample rate of the device. Note that we
// only need to take the state lock to get this value.
return .float64(sampleRate)
case kAudioDevicePropertyAvailableNominalSampleRates:
// This returns all nominal sample rates the device supports as an array of
// AudioValueRangeStructs. Note that for discrete sampler rates, the range
// will have the minimum value equal to the maximum value.
return .valueRangeList(
ContiguousArray(kEQMDeviceSupportedSampleRates.map { freq -> AudioValueRange in
let frequency = Float64(freq)
let range = AudioValueRange(mMinimum: frequency, mMaximum: frequency)
return range
})
)
case kAudioDevicePropertyIsHidden:
// This returns whether or not the device is visible to clients.
return .integer(shown ? 0 : 1)
case kAudioDevicePropertyPreferredChannelsForStereo:
// This property returns which two channesl to use as left/right for stereo
// data by default. Note that the channel numbers are 1-based.xz
return .integerList([ 1, 2 ])
case kAudioDevicePropertyPreferredChannelLayout:
// This property returns the default AudioChannelLayout to use for the device
// by default. For this device, we return a stereo ACL.
// calcualte how big the
let channelDescriptionsPtr = UnsafeMutablePointer<AudioChannelDescription>.allocate(capacity: Int(kChannelCount))
for channelNumber in 0 ..< Int(kChannelCount) {
channelDescriptionsPtr[channelNumber] = AudioChannelDescription(
mChannelLabel: AudioChannelLabel(channelNumber),
mChannelFlags: AudioChannelFlags(rawValue: 0),
mCoordinates: (0, 0, 0)
)
}
let channelLayout = AudioChannelLayout(
mChannelLayoutTag: kAudioChannelLayoutTag_UseChannelDescriptions,
mChannelBitmap: AudioChannelBitmap(rawValue: 0),
mNumberChannelDescriptions: kChannelCount,
mChannelDescriptions: channelDescriptionsPtr.pointee
)
return .channelLayout(channelLayout)
case kAudioDevicePropertyZeroTimeStampPeriod:
// This property returns how many frames the HAL should expect to see between
// successive sample times in the zero time stamps this device provides.
return .integer(ringBufferSize)
case kAudioDevicePropertyIcon:
// This is a CFURL that points to the device's Icon in the plug-in's resource bundle.
let bundle = CFBundleGetBundleWithIdentifier(DRIVER_BUNDLE_ID as CFString)
let url = CFBundleCopyResourceURL(bundle, "icon.icns" as CFString, nil, nil)
return .url(url!)
case kAudioObjectPropertyCustomPropertyInfoList:
// This property returns an array of AudioServerPlugInCustomPropertyInfo's that
// describe the type of data used by any custom properties.
let customProperties = ContiguousArray([
AudioServerPlugInCustomPropertyInfo(
mSelector: EQMDeviceCustom.properties.version,
mPropertyDataType: kAudioServerPlugInCustomPropertyDataTypeCFString,
mQualifierDataType: kAudioServerPlugInCustomPropertyDataTypeNone
),
AudioServerPlugInCustomPropertyInfo(
mSelector: EQMDeviceCustom.properties.shown,
mPropertyDataType: kAudioServerPlugInCustomPropertyDataTypeCFPropertyList,
mQualifierDataType: kAudioServerPlugInCustomPropertyDataTypeNone
),
AudioServerPlugInCustomPropertyInfo(
mSelector: EQMDeviceCustom.properties.latency,
mPropertyDataType: kAudioServerPlugInCustomPropertyDataTypeCFPropertyList,
mQualifierDataType: kAudioServerPlugInCustomPropertyDataTypeNone
),
AudioServerPlugInCustomPropertyInfo(
mSelector: EQMDeviceCustom.properties.name,
mPropertyDataType: kAudioServerPlugInCustomPropertyDataTypeCFString,
mQualifierDataType: kAudioServerPlugInCustomPropertyDataTypeNone
)
])
return .customPropertyInfoList(customProperties)
case EQMDeviceCustom.properties.version:
let version = CFCopyDescription(
CFBundleGetValueForInfoDictionaryKey(
CFBundleGetBundleWithIdentifier(DRIVER_BUNDLE_ID as CFString),
kCFBundleVersionKey
)
)
return .string(version!)
case EQMDeviceCustom.properties.shown:
return .bool(shown ? kCFBooleanTrue : kCFBooleanFalse)
default: return nil
}
}
static func setPropertyData(client: EQMClient?, objectID: AudioObjectID? = nil, address: AudioObjectPropertyAddress, data: UnsafeRawPointer, changedProperties: inout [AudioObjectPropertyAddress]) -> OSStatus {
switch address.mSelector {
case kAudioDevicePropertyNominalSampleRate:
// Changing the sample rate needs to be handled via the
// RequestConfigChange/PerformConfigChange machinery.
// check the arguments
let newSampleRate = data.load(as: Float64.self)
if !kEQMDeviceSupportedSampleRates.contains(newSampleRate) {
return kAudioHardwareIllegalOperationError
}
// make sure that the new value is different than the old value
if (sampleRate != newSampleRate) {
// we dispatch this so that the change can happen asynchronously
DispatchQueue.global(qos: .default).async {
_ = EQMDriver.host?.pointee.RequestDeviceConfigurationChange(
EQMDriver.host!,
kObjectID_Device,
UInt64(newSampleRate),
nil
)
}
}
return noErr
// Custom Properties
case EQMDeviceCustom.properties.shown:
// Only allow eqMac app to set this property
guard client?.bundleId == APP_BUNDLE_ID else { return noErr }
let shownRef = data.load(as: CFBoolean.self)
let newShown = CFBooleanGetValue(shownRef)
if newShown == shown { return noErr }
shown = newShown
return noErr
case EQMDeviceCustom.properties.latency:
// Only allow eqMac app to set this property
guard client?.bundleId == APP_BUNDLE_ID else { return noErr }
let newLatencyRef = data.load(as: CFNumber.self)
var newLatency: Int32 = 0
CFNumberGetValue(newLatencyRef, .sInt32Type, &newLatency)
if latency != newLatency {
latency = UInt32(newLatency)
}
changedProperties.append(
AudioObjectPropertyAddress(
mSelector: kAudioDevicePropertyLatency,
mScope: kAudioObjectPropertyScopeOutput,
mElement: kAudioObjectPropertyElementMaster
)
)
return noErr
case EQMDeviceCustom.properties.name:
// Only allow eqMac app to set this property
guard client?.bundleId == APP_BUNDLE_ID else { return noErr }
var newName = data.load(as: CFString.self) as String
if name != newName {
if newName == "" {
newName = kEQMDeviceDefaultName
}
name = newName
changedProperties.append(
.init(
mSelector: kAudioObjectPropertyName,
mScope: kAudioObjectPropertyScopeGlobal,
mElement: kAudioObjectPropertyElementMaster
)
)
}
return noErr
default: return kAudioHardwareUnknownPropertyError
}
}
static func startIO () -> OSStatus {
let ioCount = ioCounter.load(ordering: .relaxed)
// Reached max amount of possible IOs
if ioCount == UInt64.max {
return kAudioHardwareIllegalOperationError
}
ioMutex.lock()
// If IO is not running we need start it and init all the important vars
if ioCount == 0 {
running = true
timestampCount = 0
anchorSampleTime = 0
anchorHostTime = mach_absolute_time()
ringBuffer?.deallocate()
ringBuffer = UnsafeMutablePointer<Float32>.allocate(capacity: Int(ringBufferSize * 4 * kChannelCount))
} else {
// IO already running so increment the counter
ioCounter.wrappingIncrement(ordering: .relaxed)
}
ioMutex.unlock()
return noErr
}
static func stopIO () -> OSStatus {
// If IO is not running it's an invalid call
if !running {
return kAudioHardwareIllegalOperationError
}
var ioCount = ioCounter.load(ordering: .relaxed)
if ioCount > 0 {
ioCount = ioCounter.wrappingDecrementThenLoad(ordering: .relaxed)
}
ioMutex.lock()
// If IO reached zero deinit
if ioCount == 0 {
running = false
ringBuffer?.deallocate()
ringBuffer = nil
}
ioMutex.unlock()
return noErr
}
static func getZeroTimeStamp (outSampleTime: UnsafeMutablePointer<Float64>, outHostTime: UnsafeMutablePointer<UInt64>, outSeed: UnsafeMutablePointer<UInt64>) -> OSStatus {
ioMutex.lock()
// get the current host time
let currentHostTime = mach_absolute_time()
// calculate the next host time
let hostTicksPerRingBuffer = EQMDriver.hostTicksPerFrame! * Float64(ringBufferSize)
let hostTicksOffset = Float64(timestampCount + 1) * hostTicksPerRingBuffer
let nextHostTime = anchorHostTime + UInt64(hostTicksOffset)
if nextHostTime <= currentHostTime {
timestampCount += 1
}
outSampleTime.pointee = Float64(timestampCount * UInt64(ringBufferSize))
outHostTime.pointee = anchorHostTime + timestampCount * UInt64(hostTicksPerRingBuffer)
outSeed.pointee = 1
ioMutex.unlock()
return noErr
}
static func doIO (client: EQMClient?, operationID: UInt32, sample: UnsafeMutablePointer<Float32>, cycleInfo: AudioServerPlugInIOCycleInfo, frameSize: UInt32) -> OSStatus {
guard let buffer = ringBuffer else {
return noErr
}
ioMutex.lock()
switch operationID {
// Store
case AudioServerPlugInIOOperation.writeMix.rawValue:
let sampleTime = Int(cycleInfo.mOutputTime.mSampleTime)
for frame in 0 ..< frameSize {
for channel in 0 ..< kChannelCount {
let readFrame = Int(frame * kChannelCount + channel)
if EQMControl.muted {
// Muted
sample[readFrame] = 0
}
let writePosition = sampleTime + Int(frame)
let writeRemainder = writePosition % Int(ringBufferSize)
let writeFrame = writeRemainder * Int(kChannelCount) + Int(channel)
buffer[writeFrame] += sample[readFrame]
// Clean up buffer
let cleanCleanPosition = sampleTime + Int(frame) + 8192
let cleanRemainder = cleanCleanPosition % Int(ringBufferSize)
let cleanFrame = cleanRemainder * Int(kChannelCount) + Int(channel)
buffer[cleanFrame] = 0
}
}
// Make output buffer silent
sample.assign(repeating: 0, count: Int(frameSize * kChannelCount))
break
// Read
case AudioServerPlugInIOOperation.readInput.rawValue:
let sampleTime = Int(cycleInfo.mInputTime.mSampleTime)
for frame in 0 ..< frameSize {
for channel in 0 ..< kChannelCount {
let writeFrame = Int(frame * kChannelCount + channel)
if EQMControl.muted {
sample[writeFrame] = 0
} else {
let readPosition = sampleTime + Int(frame)
let readRemainder = readPosition % Int(ringBufferSize)
let readFrame = readRemainder * Int(kChannelCount) + Int(channel)
sample[writeFrame] = buffer[readFrame]
}
// Clean up buffer
let cleanPosition = sampleTime + Int(frame) - Int(ringBufferSize)
if (cleanPosition > -1) {
let cleanRemainder = cleanPosition % Int(ringBufferSize)
let cleanFrame = cleanRemainder * Int(kChannelCount) + Int(channel)
buffer[cleanFrame] = 0
}
}
}
break
default: break
}
ioMutex.unlock()
return noErr
}
}