1
1
mirror of https://github.com/bitgapp/eqMac.git synced 2024-11-22 13:07:26 +03:00
This commit is contained in:
Nodeful 2021-08-22 20:11:18 +03:00
parent 12b990cd16
commit e712cb55b1
18 changed files with 788 additions and 646 deletions

View File

@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 52;
objectVersion = 47;
objects = {
/* Begin PBXBuildFile section */
@ -20,9 +20,12 @@
78A804D726C4903E0021981C /* EQMDriver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78A804D626C4903E0021981C /* EQMDriver.swift */; };
78A804DD26C563860021981C /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78A804DC26C563860021981C /* Constants.swift */; };
78A804E026C5676D0021981C /* EQMPlugIn.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78A804DF26C5676D0021981C /* EQMPlugIn.swift */; };
78A804E426C58F3C0021981C /* Atomics in Frameworks */ = {isa = PBXBuildFile; productRef = 78A804E326C58F3C0021981C /* Atomics */; };
78A804E726C595D80021981C /* EQMBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78A804E626C595D80021981C /* EQMBox.swift */; };
78A804EA26C596080021981C /* EQMDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78A804E926C596080021981C /* EQMDevice.swift */; };
78E8F7CE26D12B4C00B6B969 /* Volume.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78E8F7CD26D12B4C00B6B969 /* Volume.swift */; };
78E8F7D226D12BD500B6B969 /* AtomicCounter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78E8F7D126D12BD500B6B969 /* AtomicCounter.swift */; };
78E8F7D526D12C0100B6B969 /* EQMObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78E8F7D426D12C0100B6B969 /* EQMObject.swift */; };
78E8F7D926D166D800B6B969 /* EQMInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78E8F7D826D166D800B6B969 /* EQMInterface.swift */; };
E01A29F82456E50500369F41 /* CoreFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E01A29F72456E50500369F41 /* CoreFoundation.framework */; };
E0DAAFBD21FE545F00DCCCEC /* icon.icns in Resources */ = {isa = PBXBuildFile; fileRef = E0DAAFBC21FE545F00DCCCEC /* icon.icns */; };
/* End PBXBuildFile section */
@ -47,6 +50,10 @@
78A804DF26C5676D0021981C /* EQMPlugIn.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EQMPlugIn.swift; sourceTree = "<group>"; };
78A804E626C595D80021981C /* EQMBox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EQMBox.swift; sourceTree = "<group>"; };
78A804E926C596080021981C /* EQMDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EQMDevice.swift; sourceTree = "<group>"; };
78E8F7CD26D12B4C00B6B969 /* Volume.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = Volume.swift; path = ../shared/Volume.swift; sourceTree = "<group>"; };
78E8F7D126D12BD500B6B969 /* AtomicCounter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AtomicCounter.swift; path = ../shared/AtomicCounter.swift; sourceTree = "<group>"; };
78E8F7D426D12C0100B6B969 /* EQMObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EQMObject.swift; sourceTree = "<group>"; };
78E8F7D826D166D800B6B969 /* EQMInterface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EQMInterface.swift; sourceTree = "<group>"; };
E01A29F72456E50500369F41 /* CoreFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreFoundation.framework; path = System/Library/Frameworks/CoreFoundation.framework; sourceTree = SDKROOT; };
E0DAAFBC21FE545F00DCCCEC /* icon.icns */ = {isa = PBXFileReference; lastKnownFileType = image.icns; path = icon.icns; sourceTree = "<group>"; };
/* End PBXFileReference section */
@ -57,7 +64,6 @@
buildActionMask = 2147483647;
files = (
E01A29F82456E50500369F41 /* CoreFoundation.framework in Frameworks */,
78A804E426C58F3C0021981C /* Atomics in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -89,6 +95,8 @@
78A8049C26C486350021981C /* Bridge */,
78A804DC26C563860021981C /* Constants.swift */,
78A804D626C4903E0021981C /* EQMDriver.swift */,
78E8F7D826D166D800B6B969 /* EQMInterface.swift */,
78E8F7D426D12C0100B6B969 /* EQMObject.swift */,
78A804DF26C5676D0021981C /* EQMPlugIn.swift */,
78A804E626C595D80021981C /* EQMBox.swift */,
78A804E926C596080021981C /* EQMDevice.swift */,
@ -117,6 +125,8 @@
7838E34E26C897560039D466 /* SharedConstants.swift */,
7838E35026C897560039D466 /* StringExtensions.swift */,
7838E35E26C8A4190039D466 /* Utilities.swift */,
78E8F7CD26D12B4C00B6B969 /* Volume.swift */,
78E8F7D126D12BD500B6B969 /* AtomicCounter.swift */,
);
name = Shared;
sourceTree = "<group>";
@ -156,7 +166,6 @@
);
name = eqMac;
packageProductDependencies = (
78A804E326C58F3C0021981C /* Atomics */,
);
productName = EQMDriver;
productReference = 1CB8B3641BBBB78D000E2DD1 /* eqMac.driver */;
@ -189,7 +198,6 @@
);
mainGroup = 1CB8B3591BBBB69C000E2DD1;
packageReferences = (
78A804E226C58F3C0021981C /* XCRemoteSwiftPackageReference "swift-atomics" */,
);
productRefGroup = 1CB8B3651BBBB78D000E2DD1 /* Products */;
projectDirPath = "";
@ -226,10 +234,14 @@
78A804E026C5676D0021981C /* EQMPlugIn.swift in Sources */,
7838E35226C897560039D466 /* SharedConstants.swift in Sources */,
78A804EA26C596080021981C /* EQMDevice.swift in Sources */,
78E8F7D226D12BD500B6B969 /* AtomicCounter.swift in Sources */,
7838E36426C9CC430039D466 /* ArrayExtensions.swift in Sources */,
78E8F7D526D12C0100B6B969 /* EQMObject.swift in Sources */,
78E8F7D926D166D800B6B969 /* EQMInterface.swift in Sources */,
7838E35B26C898F30039D466 /* EQMControl.swift in Sources */,
78A804DD26C563860021981C /* Constants.swift in Sources */,
7838E35526C897560039D466 /* NumericTypesConversions.swift in Sources */,
78E8F7CE26D12B4C00B6B969 /* Volume.swift in Sources */,
78A804E726C595D80021981C /* EQMBox.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -392,9 +404,10 @@
CODE_SIGN_IDENTITY = "-";
CODE_SIGN_STYLE = Manual;
COMBINE_HIDPI_IMAGES = YES;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1.2.3;
DEAD_CODE_STRIPPING = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = "";
ENABLE_NS_ASSERTIONS = YES;
@ -402,7 +415,7 @@
GCC_DYNAMIC_NO_PIC = NO;
GCC_ENABLE_CPP_RTTI = YES;
GCC_INLINES_ARE_PRIVATE_EXTERN = NO;
GCC_PREPROCESSOR_DEFINITIONS = "";
GCC_PREPROCESSOR_DEFINITIONS = "DEBUG=1";
GCC_TREAT_WARNINGS_AS_ERRORS = NO;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES;
@ -413,16 +426,13 @@
INFOPLIST_FILE = "$(SRCROOT)/Supporting Files/Info.plist";
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Audio/Plug-Ins/HAL";
IPHONEOS_DEPLOYMENT_TARGET = "";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
"@loader_path/../Frameworks",
);
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 10.10;
MARKETING_VERSION = 1.2.3;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
OTHER_CODE_SIGN_FLAGS = "";
OTHER_SWIFT_FLAGS = "-D DEBUG";
PRODUCT_BUNDLE_IDENTIFIER = com.bitgapp.eqmac.driver;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -460,6 +470,7 @@
CODE_SIGN_IDENTITY = "Developer ID Application";
CODE_SIGN_STYLE = Manual;
COMBINE_HIDPI_IMAGES = YES;
COPY_PHASE_STRIP = YES;
CURRENT_PROJECT_VERSION = 1.2.3;
DEAD_CODE_STRIPPING = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
@ -480,11 +491,7 @@
INFOPLIST_FILE = "$(SRCROOT)/Supporting Files/Info.plist";
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Audio/Plug-Ins/HAL";
IPHONEOS_DEPLOYMENT_TARGET = "";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
"@loader_path/../Frameworks",
);
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 10.10;
MARKETING_VERSION = 1.2.3;
MTL_ENABLE_DEBUG_INFO = NO;
@ -525,25 +532,6 @@
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */
78A804E226C58F3C0021981C /* XCRemoteSwiftPackageReference "swift-atomics" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/apple/swift-atomics.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 0.0.3;
};
};
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
78A804E326C58F3C0021981C /* Atomics */ = {
isa = XCSwiftPackageProductDependency;
package = 78A804E226C58F3C0021981C /* XCRemoteSwiftPackageReference "swift-atomics" */;
productName = Atomics;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = 1CB8B35A1BBBB69C000E2DD1 /* Project object */;
}

View File

@ -35,7 +35,7 @@
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
debugAsWhichUser = "root"
launchStyle = "0"
launchStyle = "1"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
@ -46,7 +46,7 @@
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
<ActionContent
title = "Run Script"
scriptText = "export SUDO_ASKPASS=~/askpass.sh&#10;&#10;# Unload CoreAudio&#10;sudo -A launchctl unload /System/Library/LaunchDaemons/com.apple.audio.coreaudiod.plist&#10;&#10;# Uninstall any new driver leftovers&#10;sudo -A rm -rf /Library/Audio/Plug-Ins/HAL/eqMac.driver/&#10;# Install the new driver&#10;sudo -A cp -f -r &quot;$BUILT_PRODUCTS_DIR/$FULL_PRODUCT_NAME&quot; /Library/Audio/Plug-Ins/HAL/&#10;&#10;# Load CoreAudio&#10;#sudo -A launchctl load /System/Library/LaunchDaemons/com.apple.audio.coreaudiod.plist&#10;"
scriptText = "export SUDO_ASKPASS=~/askpass.sh&#10;&#10;# Unload CoreAudio&#10;sudo -A launchctl unload /System/Library/LaunchDaemons/com.apple.audio.coreaudiod.plist&#10;&#10;# Uninstall any new driver leftovers&#10;sudo -A rm -rf /Library/Audio/Plug-Ins/HAL/eqMac.driver/&#10;# Install the new driver&#10;sudo -A cp -f -r &quot;$BUILT_PRODUCTS_DIR/$FULL_PRODUCT_NAME&quot; /Library/Audio/Plug-Ins/HAL/&#10;"
shellToInvoke = "/bin/sh">
<EnvironmentBuildable>
<BuildableReference

View File

@ -9,6 +9,12 @@
import Foundation
import CoreAudio.AudioServerPlugIn
#if DEBUG
let DEBUG = true
#else
let DEBUG = false
#endif
let kAudioServerPluginTypeUUID = CFUUIDGetConstantUUIDWithBytes(nil, 0x44, 0x3A, 0xBA, 0xB8, 0xE7, 0xB3, 0x49, 0x1A, 0xB9, 0x85, 0xBE, 0xB9, 0x18, 0x70, 0x30, 0xDB)!
let kAudioServerPluginDriverInterfaceUUID = CFUUIDGetConstantUUIDWithBytes(nil, 0xEE, 0xA5, 0x77, 0x3D, 0xCC, 0x43, 0x49, 0xF1, 0x8E, 0x00, 0x8F, 0x96, 0xE7, 0xD2, 0x3B, 0x17)!
let IUnknownUUID = CFUUIDGetConstantUUIDWithBytes(kCFAllocatorSystemDefault, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4)!
@ -21,9 +27,10 @@ let kDataSource_NumberItems: UInt32 = 1
let kDeviceName = "eqMac"
let kDeviceManufacturer = "Bitgapp Ltd"
let kBoxDefaultName = "eqMac Box"
let kPlugInBundleId = "com.bitgapp.eqmac.driver"
let kBoxUID = "EQMBox"
let kBoxUID = "eqMacBox_UID"
let kDeviceUID = "EQMDevice"
let kDeviceModelUID = "EQMDeviceModelUID"
@ -61,7 +68,7 @@ extension HRESULT {
static let accessDenied = HRESULT(bitPattern: 0x80000009)
}
let kObjectID_PlugIn: UInt32 = 1
let kObjectID_PlugIn: UInt32 = kAudioObjectPlugInObject
let kObjectID_Box: UInt32 = 2
let kObjectID_Device: UInt32 = 3
let kObjectID_Stream_Input: UInt32 = 4

View File

@ -9,9 +9,9 @@
import Foundation
import CoreAudio.AudioServerPlugIn
class EQMBox: EQMObjectProtocol {
class EQMBox: EQMObject {
static let id = AudioObjectID(kBoxUID)!
static var name = "eqMac Box"
static var name: String? = kBoxDefaultName
static var acquired = false
static func hasProperty (objectID: AudioObjectID? = nil, address: AudioObjectPropertyAddress) -> Bool {
@ -77,8 +77,7 @@ class EQMBox: EQMObjectProtocol {
}
}
static func getPropertyData (objectID: AudioObjectID? = nil, address: AudioObjectPropertyAddress) -> EQMObjectProperty? {
static func getPropertyData (objectID: AudioObjectID? = nil, address: AudioObjectPropertyAddress, inData: UnsafeRawPointer?) -> EQMObjectProperty? {
switch address.mSelector {
case kAudioObjectPropertyBaseClass:
// The base class for kAudioBoxClassID is kAudioObjectClassID
@ -91,7 +90,7 @@ class EQMBox: EQMObjectProtocol {
return .integer(kObjectID_PlugIn)
case kAudioObjectPropertyName:
// This is the human readable name of the maker of the box.
return .string(name as CFString)
return .string((name ?? kBoxDefaultName) as CFString)
case kAudioObjectPropertyModelName:
// This is the human readable name of the maker of the box.
return .string("eqMac Model" as CFString)
@ -143,7 +142,7 @@ class EQMBox: EQMObjectProtocol {
}
}
static func setPropertyData(objectID: AudioObjectID? = nil, address: AudioObjectPropertyAddress, data: UnsafeRawPointer) -> OSStatus {
static func setPropertyData(objectID: AudioObjectID? = nil, address: AudioObjectPropertyAddress, data: UnsafeRawPointer, changedProperties: inout [AudioObjectPropertyAddress]) -> OSStatus {
switch address.mSelector {
case kAudioObjectPropertyName:
// Boxes should allow their name to be editable
@ -163,7 +162,37 @@ class EQMBox: EQMObjectProtocol {
if (acquired != wasAcquired) {
// the new value is different from the old value, so save it
acquired = wasAcquired
_ = EQMDriver.host?.pointee.WriteToStorage(EQMDriver.host!, "box acquired" as CFString, acquired ? kCFBooleanTrue : kCFBooleanFalse)
// and it means that this property and the device list property have changed
changedProperties.append(
AudioObjectPropertyAddress(
mSelector: kAudioBoxPropertyAcquired,
mScope: kAudioObjectPropertyScopeGlobal,
mElement: kAudioObjectPropertyElementMaster
)
)
changedProperties.append(
AudioObjectPropertyAddress(
mSelector: kAudioBoxPropertyDeviceList,
mScope: kAudioObjectPropertyScopeGlobal,
mElement: kAudioObjectPropertyElementMaster
)
)
// but it also means that the device list has changed for the plug-in too
DispatchQueue.global(qos: .default).async {
var address = AudioObjectPropertyAddress(
mSelector: kAudioPlugInPropertyDeviceList,
mScope: kAudioObjectPropertyScopeGlobal,
mElement: kAudioObjectPropertyElementMaster
)
_ = EQMDriver.host?.pointee.PropertiesChanged(EQMDriver.host!, kObjectID_PlugIn, 1, &address);
}
}
return noErr
default: return kAudioHardwareUnknownPropertyError
}

View File

@ -9,7 +9,7 @@
import Foundation
import CoreAudio.AudioServerPlugIn
class EQMControl: EQMObjectProtocol {
class EQMControl: EQMObject {
static var outputVolume: Float32 = 1
static var outputMuted = false
static var inputDataSource: UInt32 = 0
@ -148,7 +148,7 @@ class EQMControl: EQMObjectProtocol {
}
}
static func getPropertyData (objectID: AudioObjectID?, address: AudioObjectPropertyAddress) -> EQMObjectProperty? {
static func getPropertyData (objectID: AudioObjectID?, address: AudioObjectPropertyAddress, inData: UnsafeRawPointer?) -> EQMObjectProperty? {
switch objectID {
case kObjectID_Volume_Input_Master,
kObjectID_Volume_Output_Master:
@ -185,7 +185,7 @@ class EQMControl: EQMObjectProtocol {
default: return 0
}
})()
return .float32(volumeToScalar(volume))
return .float32(Volume.toScalar(volume))
case kAudioLevelControlPropertyDecibelValue:
// This returns the dB value of the control.
// Note that we need to take the state lock to examine the value.
@ -196,7 +196,7 @@ class EQMControl: EQMObjectProtocol {
default: return 0
}
})()
return .float32(volumeToDecibel(volume))
return .float32(Volume.toDecibel(volume))
case kAudioLevelControlPropertyDecibelRange:
// This returns the dB range of the control.
return .valueRange(
@ -311,7 +311,7 @@ class EQMControl: EQMObjectProtocol {
}
}
static func setPropertyData(objectID: AudioObjectID?, address: AudioObjectPropertyAddress, data: UnsafeRawPointer) -> OSStatus {
static func setPropertyData(objectID: AudioObjectID?, address: AudioObjectPropertyAddress, data: UnsafeRawPointer, changedProperties: inout [AudioObjectPropertyAddress]) -> OSStatus {
switch objectID {
case kObjectID_Volume_Input_Master,
kObjectID_Volume_Output_Master:
@ -323,7 +323,7 @@ class EQMControl: EQMObjectProtocol {
return kAudioHardwareBadPropertySizeError
}
var newVolume = scalarToVolume(scalar)
var newVolume = Volume.fromScalar(scalar)
newVolume = clamp(value: newVolume, min: 0.0, max: 1.0)
switch objectID {
@ -348,7 +348,7 @@ class EQMControl: EQMObjectProtocol {
}
decibel = clamp(value: decibel, min: kMinVolumeDB, max: kMaxVolumeDB)
var newVolume = decibelToVolume(decibel)
var newVolume = Volume.fromDecibel(decibel)
newVolume = clamp(value: newVolume, min: 0.0, max: 1.0)
switch objectID {

View File

@ -8,9 +8,8 @@
import Foundation
import CoreAudio.AudioServerPlugIn
import Atomics
class EQMDevice: EQMObjectProtocol {
class EQMDevice: EQMObject {
static let id = AudioObjectID(kDeviceUID)!
static var name = kDeviceName
static var sampleRate = kDefaultSampleRate
@ -18,7 +17,7 @@ class EQMDevice: EQMObjectProtocol {
static var shown = false
static var latency: UInt32 = 0
static var ringBufferSize: UInt32 = 16384
static var ioCounter = ManagedAtomic<UInt64>(0)
static var ioCounter = AtomicCounter<UInt64>()
static var hostTime: UInt64 = 0
static var sampleTime: UInt64 = 0
static var timestampCount: UInt64 = 0
@ -124,18 +123,18 @@ class EQMDevice: EQMObjectProtocol {
case kAudioDevicePropertyZeroTimeStampPeriod: return sizeof(UInt32.self)
case kAudioDevicePropertyIcon: return sizeof(CFURL.self)
case kAudioObjectPropertyCustomPropertyInfoList: return sizeof(AudioServerPlugInCustomPropertyInfo.self) * 3
case kEQMDeviceCustomPropertyLatency: return sizeof(UInt32.self)
case kEQMDeviceCustomPropertyShown: return sizeof(CFBoolean.self)
case kEQMDeviceCustomPropertyVersion: return sizeof(CFString.self)
case kAudioObjectPropertyCustomPropertyInfoList: return sizeof(AudioServerPlugInCustomPropertyInfo.self) * 4
default:
return nil
}
}
static func getPropertyData (objectID: AudioObjectID? = nil, address: AudioObjectPropertyAddress) -> EQMObjectProperty? {
static func getPropertyData (objectID: AudioObjectID? = nil, address: AudioObjectPropertyAddress, inData: UnsafeRawPointer?) -> EQMObjectProperty? {
switch address.mSelector {
case kAudioObjectPropertyBaseClass:
// The base class for kAudioDeviceClassID is kAudioObjectClassID
@ -350,9 +349,7 @@ class EQMDevice: EQMObjectProtocol {
return .url(url!)
case kAudioObjectPropertyCustomPropertyInfoList:
// This property returns an array of AudioServerPlugInCustomPropertyInfo's that
// describe the type of data used by any custom properties. For this example,
// the plug-in supports a single property whose data type is a CFString and
// whose qualifier is a CFString.
// describe the type of data used by any custom properties.
let customProperties = ContiguousArray([
AudioServerPlugInCustomPropertyInfo(
mSelector: kEQMDeviceCustomPropertyVersion,
@ -386,7 +383,7 @@ class EQMDevice: EQMObjectProtocol {
}
}
static func setPropertyData(objectID: AudioObjectID? = nil, address: AudioObjectPropertyAddress, data: UnsafeRawPointer) -> OSStatus {
static func setPropertyData(objectID: AudioObjectID? = nil, address: AudioObjectPropertyAddress, data: UnsafeRawPointer, changedProperties: inout [AudioObjectPropertyAddress]) -> OSStatus {
switch address.mSelector {
case kAudioDevicePropertyNominalSampleRate:
@ -440,7 +437,7 @@ class EQMDevice: EQMObjectProtocol {
}
static func startIO () -> OSStatus {
let ioCount = ioCounter.load(ordering: .relaxed)
let ioCount = ioCounter.value
// Reached max amount of possible IOs
if ioCount == UInt64.max {
@ -456,7 +453,7 @@ class EQMDevice: EQMObjectProtocol {
buffer = UnsafeMutablePointer<Float32>.allocate(capacity: Int(bufferSize * kChannelCount))
} else {
// IO already running so increment the counter
ioCounter.wrappingIncrement(ordering: .relaxed)
ioCounter.increment()
}
return noErr
@ -468,10 +465,11 @@ class EQMDevice: EQMObjectProtocol {
return kAudioHardwareIllegalOperationError
}
var ioCount = ioCounter.load(ordering: .relaxed)
var ioCount = ioCounter.value
if ioCount > 0 {
ioCount = ioCounter.wrappingDecrementThenLoad(ordering: .relaxed)
ioCounter.decrement()
ioCount = ioCounter.value
}
// If IO reached zero deinit

View File

@ -8,23 +8,41 @@
import Foundation
import CoreAudio.AudioServerPlugIn
import Atomics
@objc
class EQMDriver: NSObject {
@objc class EQMDriver: NSObject {
static var host: AudioServerPlugInHostRef?
static var hostTicksPerFrame: Float64?
static private var _interface: AudioServerPlugInDriverInterface?
static private var _interfacePtr: UnsafeMutablePointer<AudioServerPlugInDriverInterface>?
static private var _ref: AudioServerPlugInDriverRef?
static let refCounter = ManagedAtomic<UInt32>(0)
static var ref: AudioServerPlugInDriverRef {
if let ref = _ref {
return ref
static var refCounter = AtomicCounter<UInt32>()
@objc public static var ref: AudioServerPlugInDriverRef?
@objc
public static func create (allocator: CFAllocator!, requestedTypeUUID: CFUUID!) -> UnsafeMutableRawPointer? {
// This is the CFPlugIn factory function. Its job is to create the implementation for the given
// type provided that the type is supported. Because this driver is simple and all its
// initialization is handled via static iniitalization when the bundle is loaded, all that
// needs to be done is to return the AudioServerPlugInDriverRef that points to the driver's
// interface. A more complicated driver would create any base line objects it needs to satisfy
// the IUnknown methods that are used to discover that actual interface to talk to the driver.
// The majority of the driver's initilization should be handled in the Initialize() method of
// the driver's AudioServerPlugInDriverInterface.
if !CFEqual(requestedTypeUUID, kAudioServerPluginTypeUUID) {
return nil
}
return UnsafeMutableRawPointer(createRef())
}
private static func createRef () -> AudioServerPlugInDriverRef {
if ref != nil {
return ref!
}
_interface = AudioServerPlugInDriverInterface(
_reserved: nil,
QueryInterface: EQM_QueryInterface,
@ -50,424 +68,59 @@ class EQMDriver: NSObject {
DoIOOperation: EQM_DoIOOperation,
EndIOOperation: EQM_EndIOOperation
)
_interfacePtr = withUnsafeMutablePointer(to: &_interface!) { $0 }
let ref = withUnsafeMutablePointer(to: &_interfacePtr) { $0 }
_ref = ref
return ref
ref = withUnsafeMutablePointer(to: &_interfacePtr) { $0 }
return ref!
}
static func validateDriver (_ driverPointer: UnsafeMutableRawPointer?, reference: AudioServerPlugInDriverRef = EQMDriver.ref!) -> Bool {
guard driverPointer != nil else { return false }
let driver = driverPointer!.assumingMemoryBound(to: (UnsafeMutablePointer<AudioServerPlugInDriverInterface>?).self)
let valid = reference == driver
return valid
}
@objc
static func create (allocator: CFAllocator!, requestedTypeUUID: CFUUID!) -> UnsafeMutableRawPointer? {
// This is the CFPlugIn factory function. Its job is to create the implementation for the given
// type provided that the type is supported. Because this driver is simple and all its
// initialization is handled via static iniitalization when the bundle is loaded, all that
// needs to be done is to return the AudioServerPlugInDriverRef that points to the driver's
// interface. A more complicated driver would create any base line objects it needs to satisfy
// the IUnknown methods that are used to discover that actual interface to talk to the driver.
// The majority of the driver's initilization should be handled in the Initialize() method of
// the driver's AudioServerPlugInDriverInterface.
if (requestedTypeUUID != kAudioServerPluginTypeUUID) {
return nil
static func validateObject (_ objectID: AudioObjectID) -> Bool {
if getEQMObject(from: objectID) != nil {
return true
}
return UnsafeMutableRawPointer(ref)
}
}
func validate (_ driverPointer: UnsafeMutableRawPointer?, reference: AudioServerPlugInDriverRef = EQMDriver.ref) -> Bool {
if let driverPointer = driverPointer {
let driver = driverPointer.assumingMemoryBound(to: (UnsafeMutablePointer<AudioServerPlugInDriverInterface>?).self)
return reference == driver
}
return false
}
func calculateHostTicksPerFrame () -> Float64 {
// calculate the host ticks per frame
var theTimeBaseInfo = mach_timebase_info()
mach_timebase_info(&theTimeBaseInfo)
var theHostClockFrequency = Float64(theTimeBaseInfo.denom) / Float64(theTimeBaseInfo.numer)
theHostClockFrequency *= 1000000000.0
return theHostClockFrequency / EQMDevice.sampleRate
}
// MARK: - Inheritance
func EQM_QueryInterface (inDriver: UnsafeMutableRawPointer?, inUUID: REFIID, outInterface: UnsafeMutablePointer<LPVOID?>?) -> HRESULT {
// This function is called by the HAL to get the interface to talk to the plug-in through.
// AudioServerPlugIns are required to support the IUnknown interface and the
// AudioServerPlugInDriverInterface. As it happens, all interfaces must also provide the
// IUnknown interface, so we can always just return the single interface we made with
// gAudioServerPlugInDriverInterfacePtr regardless of which one is asked for.
guard validate(inDriver) else { return kAudioHardwareBadObjectError }
guard let uuid = CFUUIDCreateFromUUIDBytes(nil, inUUID), let interface = outInterface else {
return kAudioHardwareIllegalOperationError
log("Invalid object for ID: \(objectID)")
return false
}
guard uuid == IUnknownUUID || uuid == kAudioServerPluginTypeUUID else { return HRESULT.noInterface }
EQMDriver.refCounter.wrappingIncrement(ordering: .relaxed)
interface.pointee = UnsafeMutableRawPointer(EQMDriver.ref)
return HRESULT.ok
}
func EQM_AddRef (inDriver: UnsafeMutableRawPointer?) -> ULONG {
// This call returns the resulting reference count after the increment.
guard validate(inDriver) else { return ULONG(kAudioHardwareBadObjectError) }
return EQMDriver.refCounter.wrappingIncrementThenLoad(ordering: .relaxed)
}
func EQM_Release (inDriver: UnsafeMutableRawPointer?) -> ULONG {
// This call returns the resulting reference count after the decrement.
guard validate(inDriver) else { return ULONG(kAudioHardwareBadObjectError) }
return EQMDriver.refCounter.wrappingDecrementThenLoad(ordering: .relaxed)
}
// MARK: - Basic Operations
func EQM_Initialize (inDriver: AudioServerPlugInDriverRef, inHost: AudioServerPlugInHostRef) -> OSStatus {
// This method is called to initialize the instance of the plug-in.
// The job of this method is, as the name implies, to get the driver initialized. One specific
// thing that needs to be done is to store the AudioServerPlugInHostRef so that it can be used
// later. Note that when this call returns, the HAL will scan the various lists the driver
// maintains (such as the device list) to get the inital set of objects the driver is
// publishing. So, there is no need to notifiy the HAL about any objects created as part of the
// execution of this method.
guard validate(inDriver) else { return kAudioHardwareBadObjectError }
EQMDriver.host = inHost
EQMBox.acquired = true
EQMDriver.hostTicksPerFrame = calculateHostTicksPerFrame()
return kAudioHardwareNoError
}
func EQM_CreateDevice (inDriver: AudioServerPlugInDriverRef, inDescription: CFDictionary, inClientInfo: UnsafePointer<AudioServerPlugInClientInfo>, outDeviceObjectID: UnsafeMutablePointer<AudioObjectID>) -> OSStatus {
// Tells the plug-in to create a new device based on the given description.
// This method is used to tell a driver that implements the Transport Manager semantics to
// create an AudioEndpointDevice from a set of AudioEndpoints. Since this driver is not a
// Transport Manager, we just check the arguments and return
// kAudioHardwareUnsupportedOperationError.
guard validate(inDriver) else { return kAudioHardwareBadObjectError }
return kAudioHardwareUnsupportedOperationError
}
func EQM_DestroyDevice (inDriver: AudioServerPlugInDriverRef, inDeviceObjectID: AudioObjectID) -> OSStatus {
// Called to tell the plug-in about to destroy the given device.
// This method is used to tell a driver that implements the Transport Manager semantics to
// destroy an AudioEndpointDevice. Since this driver is not a Transport Manager, we just check
// the arguments and return kAudioHardwareUnsupportedOperationError.
guard validate(inDriver) else { return kAudioHardwareBadObjectError }
return kAudioHardwareUnsupportedOperationError
}
func EQM_AddDeviceClient (inDriver: AudioServerPlugInDriverRef, inDeviceObjectID: AudioObjectID, inClientInfo: UnsafePointer<AudioServerPlugInClientInfo>) -> OSStatus {
// Called to tell the plug-in about a new client of the Host for a particular device.
// This method is used to inform the driver about a new client that is using the given device.
// This allows the device to act differently depending on who the client is. This driver does
// not need to track the clients using the device, so we just check the arguments and return
// successfully.
guard validate(inDriver) else { return kAudioHardwareBadObjectError }
return noErr
}
func EQM_RemoveDeviceClient (inDriver: AudioServerPlugInDriverRef, inDeviceObjectID: AudioObjectID, inClientInfo: UnsafePointer<AudioServerPlugInClientInfo>) -> OSStatus {
// Called to tell the plug-in about a client that is no longer using the device.
// This method is used to inform the driver about a client that is no longer using the given
// device. This driver does not track clients, so we just check the arguments and return
// successfully.
guard validate(inDriver) else { return kAudioHardwareBadObjectError }
return noErr
}
func EQM_PerformDeviceConfigurationChange (inDriver: AudioServerPlugInDriverRef, inDeviceObjectID: AudioObjectID, inChangeAction: UInt64, inChangeInfo: UnsafeMutableRawPointer?) -> OSStatus {
// This is called by the Host to allow the device to perform a configuration change that had been previously requested via a call to the Host method, RequestDeviceConfigurationChange().
// This method is called to tell the device that it can perform the configuation change that it
// had requested via a call to the host method, RequestDeviceConfigurationChange(). The
// arguments, inChangeAction and inChangeInfo are the same as what was passed to
// RequestDeviceConfigurationChange().
//
// The HAL guarantees that IO will be stopped while this method is in progress. The HAL will
// also handle figuring out exactly what changed for the non-control related properties. This
// means that the only notifications that would need to be sent here would be for either
// custom properties the HAL doesn't know about or for controls.
//
// For the device implemented by this driver, only sample rate changes go through this process
// as it is the only state that can be changed for the device that isn't a control. For this
// change, the new sample rate is passed in the inChangeAction argument.
guard validate(inDriver) else { return kAudioHardwareBadObjectError }
if !kSupportedSamplingRates.contains(where: { UInt64($0) == inChangeAction }) { return kAudioHardwareBadObjectError }
EQMDevice.sampleRate = Float64(inChangeAction)
EQMDriver.hostTicksPerFrame = calculateHostTicksPerFrame()
return kAudioHardwareNoError
}
func EQM_AbortDeviceConfigurationChange (inDriver: AudioServerPlugInDriverRef, inDeviceObjectID: AudioObjectID, inChangeAction: UInt64, inChangeInfo: UnsafeMutableRawPointer?) -> OSStatus {
// This is called by the Host to tell the plug-in not to perform a configuration change that had been requested via a call to the Host method, RequestDeviceConfigurationChange().
// This method is called to tell the driver that a request for a config change has been denied.
// This provides the driver an opportunity to clean up any state associated with the request.
// For this driver, an aborted config change requires no action. So we just check the arguments
// and return
guard validate(inDriver) else { return kAudioHardwareBadObjectError }
return noErr
}
// MARK: - Property Operations
// Note that for each object, this driver implements all the required properties plus a few
// extras that are useful but not required. There is more detailed commentary about each
// property in the EQMPlugin.getPropertyData() and EQMDevice.getPropertyData() methods.
func EQM_HasProperty (inDriver: AudioServerPlugInDriverRef, inObjectID: AudioObjectID, inClientProcessID: pid_t, inAddress: UnsafePointer<AudioObjectPropertyAddress>) -> DarwinBoolean {
// This method returns whether or not the given object has the given property.
guard validate(inDriver) else { return false }
return DarwinBoolean(({
switch inObjectID {
case kObjectID_PlugIn: return EQMPlugIn.hasProperty(address: inAddress.pointee)
case kObjectID_Box: return EQMBox.hasProperty(address: inAddress.pointee)
case kObjectID_Device: return EQMDevice.hasProperty(address: inAddress.pointee)
static func getEQMObject(from objectID: AudioObjectID) -> EQMObject.Type? {
switch objectID {
case kObjectID_PlugIn: return EQMPlugIn.self
case kObjectID_Box: return EQMBox.self
case kObjectID_Device: return EQMDevice.self
case kObjectID_Stream_Input,
kObjectID_Stream_Output: return EQMStream.hasProperty(address: inAddress.pointee)
kObjectID_Stream_Output: return EQMStream.self
case kObjectID_Volume_Input_Master,
kObjectID_Volume_Output_Master,
kObjectID_Mute_Input_Master,
kObjectID_Mute_Output_Master,
kObjectID_DataSource_Input_Master,
kObjectID_DataSource_Output_Master: return EQMControl.hasProperty(objectID: inObjectID, address: inAddress.pointee)
default:
return false
kObjectID_DataSource_Output_Master: return EQMControl.self
default: return nil
}
})())
}
func EQM_IsPropertySettable (inDriver: AudioServerPlugInDriverRef, inObjectID: AudioObjectID, inClientProcessID: pid_t, inAddress: UnsafePointer<AudioObjectPropertyAddress>, outIsSettable: UnsafeMutablePointer<DarwinBoolean>) -> OSStatus {
// This method returns whether or not the given property on the object can have its value changed.
guard validate(inDriver) else { return kAudioHardwareBadObjectError }
let isSettable = DarwinBoolean(({
switch inObjectID {
case kObjectID_PlugIn: return EQMPlugIn.isPropertySettable(address: inAddress.pointee)
case kObjectID_Box: return EQMBox.isPropertySettable(address: inAddress.pointee)
case kObjectID_Device: return EQMDevice.isPropertySettable(address: inAddress.pointee)
case kObjectID_Stream_Input,
kObjectID_Stream_Output: return EQMStream.isPropertySettable(address: inAddress.pointee)
case kObjectID_Volume_Input_Master,
kObjectID_Volume_Output_Master,
kObjectID_Mute_Input_Master,
kObjectID_Mute_Output_Master,
kObjectID_DataSource_Input_Master,
kObjectID_DataSource_Output_Master: return EQMControl.isPropertySettable(objectID: inObjectID, address: inAddress.pointee)
default:
return false
}
static func getEQMObjectClassName (from objectID: AudioObjectID) -> String {
if let obj = getEQMObject(from: objectID) {
return String(describing: obj.self)
}
})())
outIsSettable.pointee = isSettable
return noErr
}
func EQM_GetPropertyDataSize (inDriver: AudioServerPlugInDriverRef, inObjectID: AudioObjectID, inClientProcessID: pid_t, inAddress: UnsafePointer<AudioObjectPropertyAddress>, inQualifierDataSize: UInt32, inQualifierData: UnsafeRawPointer?, outDataSize: UnsafeMutablePointer<UInt32>) -> OSStatus {
// This method returns the byte size of the property's data.
guard validate(inDriver) else { return kAudioHardwareBadObjectError }
if let size = ({ () -> UInt32? in
switch inObjectID {
case kObjectID_PlugIn: return EQMPlugIn.getPropertyDataSize(address: inAddress.pointee)
case kObjectID_Box: return EQMBox.getPropertyDataSize(address: inAddress.pointee)
case kObjectID_Device: return EQMDevice.getPropertyDataSize(address: inAddress.pointee)
case kObjectID_Stream_Input,
kObjectID_Stream_Output: return EQMStream.getPropertyDataSize(address: inAddress.pointee)
case kObjectID_Volume_Input_Master,
kObjectID_Volume_Output_Master,
kObjectID_Mute_Input_Master,
kObjectID_Mute_Output_Master,
kObjectID_DataSource_Input_Master,
kObjectID_DataSource_Output_Master: return EQMControl.getPropertyDataSize(objectID: inObjectID, address: inAddress.pointee)
default:
return nil
}
})() {
outDataSize.pointee = size
return noErr
} else {
return kAudioHardwareUnknownPropertyError
return "Unknown"
}
static func calculateHostTicksPerFrame () -> Float64 {
// calculate the host ticks per frame
var theTimeBaseInfo = mach_timebase_info()
mach_timebase_info(&theTimeBaseInfo)
var theHostClockFrequency = Float64(theTimeBaseInfo.denom) / Float64(theTimeBaseInfo.numer)
theHostClockFrequency *= 1000000000.0
return theHostClockFrequency / EQMDevice.sampleRate
}
}
func EQM_GetPropertyData (inDriver: AudioServerPlugInDriverRef, inObjectID: AudioObjectID, inClientProcessID: pid_t, inAddress: UnsafePointer<AudioObjectPropertyAddress>, inQualifierDataSize: UInt32, inQualifierData: UnsafeRawPointer?, inDataSize: UInt32, outDataSize: UnsafeMutablePointer<UInt32>, outData: UnsafeMutableRawPointer) -> OSStatus {
// Fetches the data of the given property and places it in the provided buffer.
guard validate(inDriver) else { return kAudioHardwareBadObjectError }
if let data = ({ () -> EQMObjectProperty? in
switch inObjectID {
case kObjectID_PlugIn: return EQMPlugIn.getPropertyData(address: inAddress.pointee)
case kObjectID_Box: return EQMBox.getPropertyData(address: inAddress.pointee)
case kObjectID_Device: return EQMDevice.getPropertyData(address: inAddress.pointee)
case kObjectID_Stream_Input,
kObjectID_Stream_Output: return EQMStream.getPropertyData(objectID: inObjectID, address: inAddress.pointee)
case kObjectID_Volume_Input_Master,
kObjectID_Volume_Output_Master,
kObjectID_Mute_Input_Master,
kObjectID_Mute_Output_Master,
kObjectID_DataSource_Input_Master,
kObjectID_DataSource_Output_Master: return EQMControl.getPropertyData(objectID: inObjectID, address: inAddress.pointee)
default:
return nil
}
})() {
data.write(to: outData, size: outDataSize)
return noErr
} else {
outDataSize.pointee = 0
return kAudioHardwareUnknownPropertyError
}
}
func EQM_SetPropertyData (inDriver: AudioServerPlugInDriverRef, inObjectID: AudioObjectID, inClientProcessID: pid_t, inAddress: UnsafePointer<AudioObjectPropertyAddress>, inQualifierDataSize: UInt32, inQualifierData: UnsafeRawPointer?, inDataSize: UInt32, inData: UnsafeRawPointer) -> OSStatus {
// Tells an object to change the value of the given property.
guard validate(inDriver) else { return kAudioHardwareBadObjectError }
switch inObjectID {
case kObjectID_PlugIn: return EQMPlugIn.setPropertyData(address: inAddress.pointee, data: inData)
case kObjectID_Box: return EQMBox.setPropertyData(address: inAddress.pointee, data: inData)
case kObjectID_Device: return EQMDevice.setPropertyData(address: inAddress.pointee, data: inData)
case kObjectID_Stream_Input,
kObjectID_Stream_Output: return EQMStream.setPropertyData(objectID: inObjectID, address: inAddress.pointee, data: inData)
case kObjectID_Volume_Input_Master,
kObjectID_Volume_Output_Master,
kObjectID_Mute_Input_Master,
kObjectID_Mute_Output_Master,
kObjectID_DataSource_Input_Master,
kObjectID_DataSource_Output_Master: return EQMControl.setPropertyData(objectID: inObjectID, address: inAddress.pointee, data: inData)
default:
return kAudioHardwareBadObjectError
}
}
// MARK: - IO Operations
func EQM_StartIO (inDriver: AudioServerPlugInDriverRef, inDeviceObjectID: AudioObjectID, inClientID: UInt32) -> OSStatus {
// This call tells the device that IO is starting for the given client. When this routine
// returns, the device's clock is running and it is ready to have data read/written. It is
// important to note that multiple clients can have IO running on the device at the same time.
// So, work only needs to be done when the first client starts. All subsequent starts simply
// increment the counter.
guard validate(inDriver) else { return kAudioHardwareBadObjectError }
return EQMDevice.startIO()
}
func EQM_StopIO (inDriver: AudioServerPlugInDriverRef, inDeviceObjectID: AudioObjectID, inClientID: UInt32) -> OSStatus {
// This call tells the device that the client has stopped IO. The driver can stop the hardware
// once all clients have stopped.
guard validate(inDriver) else { return kAudioHardwareBadObjectError }
return EQMDevice.stopIO()
}
func EQM_GetZeroTimeStamp (inDriver: AudioServerPlugInDriverRef, inDeviceObjectID: AudioObjectID, inClientID: UInt32, outSampleTime: UnsafeMutablePointer<Float64>, outHostTime: UnsafeMutablePointer<UInt64>, outSeed: UnsafeMutablePointer<UInt64>) -> OSStatus {
// This method returns the current zero time stamp for the device. The HAL models the timing of
// a device as a series of time stamps that relate the sample time to a host time. The zero
// time stamps are spaced such that the sample times are the value of
// kAudioDevicePropertyZeroTimeStampPeriod apart. This is often modeled using a ring buffer
// where the zero time stamp is updated when wrapping around the ring buffer.
//
// For this device, the zero time stamps' sample time increments every kDevice_RingBufferSize
// frames and the host time increments by kDevice_RingBufferSize * gDevice_HostTicksPerFrame.
guard validate(inDriver) else { return kAudioHardwareBadObjectError }
return EQMDevice.getZeroTimeStamp(
outSampleTime: outSampleTime,
outHostTime: outHostTime,
outSeed: outSeed
)
}
func EQM_WillDoIOOperation (inDriver: AudioServerPlugInDriverRef, inDeviceObjectID: AudioObjectID, inClientID: UInt32, inOperationID: UInt32, outWillDo: UnsafeMutablePointer<DarwinBoolean>, outWillDoInPlace: UnsafeMutablePointer<DarwinBoolean>) -> OSStatus {
// Asks the plug-in whether or not the device will perform the given phase of the IO cycle for a particular device.
// This method returns whether or not the device will do a given IO operation. For this device,
// we only support reading input data and writing output data.
guard validate(inDriver) else { return kAudioHardwareBadObjectError }
var willDo = false
var willDoInPlace = true
switch inOperationID {
case AudioServerPlugInIOOperation.writeMix.rawValue:
willDo = true
willDoInPlace = true
break
case AudioServerPlugInIOOperation.readInput.rawValue:
willDo = true
willDoInPlace = true
break
default: break
}
outWillDo.pointee = DarwinBoolean(willDo)
outWillDoInPlace.pointee = DarwinBoolean(willDoInPlace)
return noErr
}
func EQM_BeginIOOperation (inDriver: AudioServerPlugInDriverRef, inDeviceObjectID: AudioObjectID, inClientID: UInt32, inOperationID: UInt32, inIOBufferFrameSize: UInt32, inIOCycleInfo: UnsafePointer<AudioServerPlugInIOCycleInfo>) -> OSStatus {
// Tells the plug-in that the Host is about to begin a phase of the IO cycle for a particular device.
// This is called at the beginning of an IO operation. This device doesn't do anything, so just
// check the arguments and return.
guard validate(inDriver) else { return kAudioHardwareBadObjectError }
return noErr
}
func EQM_DoIOOperation (inDriver: AudioServerPlugInDriverRef, inDeviceObjectID: AudioObjectID, inStreamObjectID: AudioObjectID, inClientID: UInt32, inOperationID: UInt32, inIOBufferFrameSize: UInt32, inIOCycleInfo: UnsafePointer<AudioServerPlugInIOCycleInfo>, ioMainBuffer: UnsafeMutableRawPointer?, ioSecondaryBuffer: UnsafeMutableRawPointer?) -> OSStatus {
// This is called to actuall perform a given operation.
guard validate(inDriver) else { return kAudioHardwareBadObjectError }
guard let sample = ioMainBuffer?.assumingMemoryBound(to: Float32.self) else {
return noErr
}
return EQMDevice.doIO(
clientID: inClientID,
operationID: inOperationID,
sample: sample,
sampleTime: Int(inIOCycleInfo.pointee.mInputTime.mSampleTime),
frameSize: inIOBufferFrameSize
)
}
func EQM_EndIOOperation (inDriver: AudioServerPlugInDriverRef, inDeviceObjectID: AudioObjectID, inClientID: UInt32, inOperationID: UInt32, inIOBufferFrameSize: UInt32, inIOCycleInfo: UnsafePointer<AudioServerPlugInIOCycleInfo>) -> OSStatus {
// This is called at the end of an IO operation. This device doesn't do anything, so just check
// the arguments and return.
guard validate(inDriver) else { return kAudioHardwareBadObjectError }
return noErr
}

View File

@ -0,0 +1,371 @@
//
// EQMInterface.swift
// eqMac
//
// Created by Romans Kisils on 21/08/2021.
// Copyright © 2021 Bitgapp. All rights reserved.
//
import Foundation
import CoreAudio.AudioServerPlugIn
// MARK: - Inheritance
func EQM_QueryInterface (inDriver: UnsafeMutableRawPointer?, inUUID: REFIID, outInterface: UnsafeMutablePointer<LPVOID?>?) -> HRESULT {
// This function is called by the HAL to get the interface to talk to the plug-in through.
// AudioServerPlugIns are required to support the IUnknown interface and the
// AudioServerPlugInDriverInterface. As it happens, all interfaces must also provide the
// IUnknown interface, so we can always just return the single interface we made with
// gAudioServerPlugInDriverInterfacePtr regardless of which one is asked for.
guard EQMDriver.validateDriver(inDriver) else { return kAudioHardwareBadObjectError }
let uuid = CFUUIDCreateFromUUIDBytes(nil, inUUID)
if uuid == nil || outInterface == nil {
return kAudioHardwareIllegalOperationError
}
guard CFEqual(uuid, IUnknownUUID) || CFEqual(uuid, kAudioServerPluginDriverInterfaceUUID) else { return HRESULT.noInterface }
EQMDriver.refCounter.increment()
outInterface!.pointee = UnsafeMutableRawPointer(EQMDriver.ref)
return HRESULT.ok
}
func EQM_AddRef (inDriver: UnsafeMutableRawPointer?) -> ULONG {
// This call returns the resulting reference count after the increment.
guard EQMDriver.validateDriver(inDriver) else { return ULONG(kAudioHardwareBadObjectError) }
EQMDriver.refCounter.increment()
return EQMDriver.refCounter.value
}
func EQM_Release (inDriver: UnsafeMutableRawPointer?) -> ULONG {
// This call returns the resulting reference count after the decrement.
guard EQMDriver.validateDriver(inDriver) else { return ULONG(kAudioHardwareBadObjectError) }
EQMDriver.refCounter.decrement()
return EQMDriver.refCounter.value
}
// MARK: - Basic Operations
func EQM_Initialize (inDriver: AudioServerPlugInDriverRef, inHost: AudioServerPlugInHostRef) -> OSStatus {
// This method is called to initialize the instance of the plug-in.
// The job of this method is, as the name implies, to get the driver initialized. One specific
// thing that needs to be done is to store the AudioServerPlugInHostRef so that it can be used
// later. Note that when this call returns, the HAL will scan the various lists the driver
// maintains (such as the device list) to get the inital set of objects the driver is
// publishing. So, there is no need to notifiy the HAL about any objects created as part of the
// execution of this method.
guard EQMDriver.validateDriver(inDriver) else { return kAudioHardwareBadObjectError }
EQMDriver.host = inHost
// initialize the box acquired property from the settings
var settingsPointer: Unmanaged<CFPropertyList>?
_ = EQMDriver.host!.pointee.CopyFromStorage(EQMDriver.host!, "box acquired" as CFString, &settingsPointer)
if let settings = settingsPointer?.takeUnretainedValue() {
if (CFGetTypeID(settings) == CFBooleanGetTypeID()) {
EQMBox.acquired = CFBooleanGetValue((settings as! CFBoolean))
} else if (CFGetTypeID(settings) == CFNumberGetTypeID()) {
var value: Int32 = 0
CFNumberGetValue((settings as! CFNumber), .sInt32Type, &value)
EQMBox.acquired = value == 1
}
}
// initialize the box name from the settings
_ = EQMDriver.host!.pointee.CopyFromStorage(EQMDriver.host!, "box acquired" as CFString, &settingsPointer)
if let settings = settingsPointer?.takeUnretainedValue() {
if (CFGetTypeID(settings) == CFStringGetTypeID()) {
EQMBox.name = (settings as! CFString) as String
}
}
// set the box name directly as a last resort
if (EQMBox.name == nil) {
EQMBox.name = kBoxDefaultName
}
EQMBox.acquired = true
EQMDriver.hostTicksPerFrame = EQMDriver.calculateHostTicksPerFrame()
return kAudioHardwareNoError
}
func EQM_CreateDevice (inDriver: AudioServerPlugInDriverRef, inDescription: CFDictionary, inClientInfo: UnsafePointer<AudioServerPlugInClientInfo>, outDeviceObjectID: UnsafeMutablePointer<AudioObjectID>) -> OSStatus {
// Tells the plug-in to create a new device based on the given description.
// This method is used to tell a driver that implements the Transport Manager semantics to
// create an AudioEndpointDevice from a set of AudioEndpoints. Since this driver is not a
// Transport Manager, we just check the arguments and return
// kAudioHardwareUnsupportedOperationError.
guard EQMDriver.validateDriver(inDriver) else { return kAudioHardwareBadObjectError }
return kAudioHardwareUnsupportedOperationError
}
func EQM_DestroyDevice (inDriver: AudioServerPlugInDriverRef, inDeviceObjectID: AudioObjectID) -> OSStatus {
// Called to tell the plug-in about to destroy the given device.
// This method is used to tell a driver that implements the Transport Manager semantics to
// destroy an AudioEndpointDevice. Since this driver is not a Transport Manager, we just check
// the arguments and return kAudioHardwareUnsupportedOperationError.
guard EQMDriver.validateDriver(inDriver) else { return kAudioHardwareBadObjectError }
return kAudioHardwareUnsupportedOperationError
}
func EQM_AddDeviceClient (inDriver: AudioServerPlugInDriverRef, inDeviceObjectID: AudioObjectID, inClientInfo: UnsafePointer<AudioServerPlugInClientInfo>) -> OSStatus {
// Called to tell the plug-in about a new client of the Host for a particular device.
// This method is used to inform the driver about a new client that is using the given device.
// This allows the device to act differently depending on who the client is. This driver does
// not need to track the clients using the device, so we just check the arguments and return
// successfully.
guard EQMDriver.validateDriver(inDriver) else { return kAudioHardwareBadObjectError }
return noErr
}
func EQM_RemoveDeviceClient (inDriver: AudioServerPlugInDriverRef, inDeviceObjectID: AudioObjectID, inClientInfo: UnsafePointer<AudioServerPlugInClientInfo>) -> OSStatus {
// Called to tell the plug-in about a client that is no longer using the device.
// This method is used to inform the driver about a client that is no longer using the given
// device. This driver does not track clients, so we just check the arguments and return
// successfully.
guard EQMDriver.validateDriver(inDriver) else { return kAudioHardwareBadObjectError }
return noErr
}
func EQM_PerformDeviceConfigurationChange (inDriver: AudioServerPlugInDriverRef, inDeviceObjectID: AudioObjectID, inChangeAction: UInt64, inChangeInfo: UnsafeMutableRawPointer?) -> OSStatus {
// This is called by the Host to allow the device to perform a configuration change that had been previously requested via a call to the Host method, RequestDeviceConfigurationChange().
// This method is called to tell the device that it can perform the configuation change that it
// had requested via a call to the host method, RequestDeviceConfigurationChange(). The
// arguments, inChangeAction and inChangeInfo are the same as what was passed to
// RequestDeviceConfigurationChange().
//
// The HAL guarantees that IO will be stopped while this method is in progress. The HAL will
// also handle figuring out exactly what changed for the non-control related properties. This
// means that the only notifications that would need to be sent here would be for either
// custom properties the HAL doesn't know about or for controls.
//
// For the device implemented by this driver, only sample rate changes go through this process
// as it is the only state that can be changed for the device that isn't a control. For this
// change, the new sample rate is passed in the inChangeAction argument.
guard EQMDriver.validateDriver(inDriver) else { return kAudioHardwareBadObjectError }
if !kSupportedSamplingRates.contains(where: { UInt64($0) == inChangeAction }) { return kAudioHardwareBadObjectError }
EQMDevice.sampleRate = Float64(inChangeAction)
EQMDriver.hostTicksPerFrame = EQMDriver.calculateHostTicksPerFrame()
return kAudioHardwareNoError
}
func EQM_AbortDeviceConfigurationChange (inDriver: AudioServerPlugInDriverRef, inDeviceObjectID: AudioObjectID, inChangeAction: UInt64, inChangeInfo: UnsafeMutableRawPointer?) -> OSStatus {
// This is called by the Host to tell the plug-in not to perform a configuration change that had been requested via a call to the Host method, RequestDeviceConfigurationChange().
// This method is called to tell the driver that a request for a config change has been denied.
// This provides the driver an opportunity to clean up any state associated with the request.
// For this driver, an aborted config change requires no action. So we just check the arguments
// and return
guard EQMDriver.validateDriver(inDriver) else { return kAudioHardwareBadObjectError }
return noErr
}
// MARK: - Property Operations
// Note that for each object, this driver implements all the required properties plus a few
// extras that are useful but not required. There is more detailed commentary about each
// property in the EQMPlugin.getPropertyData() and EQMDevice.getPropertyData() methods.
func EQM_HasProperty (inDriver: AudioServerPlugInDriverRef, inObjectID: AudioObjectID, inClientProcessID: pid_t, inAddress: UnsafePointer<AudioObjectPropertyAddress>) -> DarwinBoolean {
// This method returns whether or not the given object has the given property.
log("Invoking: \(EQMDriver.getEQMObjectClassName(from: inObjectID)).hasProperty(\(inAddress.pointee.mSelector.code))")
let hasProperty: Bool = ({
guard EQMDriver.validateDriver(inDriver) else { return false }
if let obj = EQMDriver.getEQMObject(from: inObjectID) {
return obj.hasProperty(objectID: inObjectID, address: inAddress.pointee)
} else {
return false
}
})()
log("\(EQMDriver.getEQMObjectClassName(from: inObjectID)).hasProperty(\(inAddress.pointee.mSelector.code)) = \(hasProperty)")
return DarwinBoolean(hasProperty)
}
func EQM_IsPropertySettable (inDriver: AudioServerPlugInDriverRef, inObjectID: AudioObjectID, inClientProcessID: pid_t, inAddress: UnsafePointer<AudioObjectPropertyAddress>, outIsSettable: UnsafeMutablePointer<DarwinBoolean>) -> OSStatus {
// This method returns whether or not the given property on the object can have its value changed.
guard EQMDriver.validateDriver(inDriver) else { return kAudioHardwareBadObjectError }
log("Invoking: \(EQMDriver.getEQMObjectClassName(from: inObjectID)).isPropertySettable(\(inAddress.pointee.mSelector.code))")
let isSettable = DarwinBoolean(({
if let obj = EQMDriver.getEQMObject(from: inObjectID) {
return obj.isPropertySettable(objectID: inObjectID, address: inAddress.pointee)
} else {
return false
}
})())
log("\(EQMDriver.getEQMObjectClassName(from: inObjectID)).getPropertySettable(\(inAddress.pointee.mSelector.code)) = \(isSettable)")
outIsSettable.pointee = isSettable
return noErr
}
func EQM_GetPropertyDataSize (inDriver: AudioServerPlugInDriverRef, inObjectID: AudioObjectID, inClientProcessID: pid_t, inAddress: UnsafePointer<AudioObjectPropertyAddress>, inQualifierDataSize: UInt32, inQualifierData: UnsafeRawPointer?, outDataSize: UnsafeMutablePointer<UInt32>) -> OSStatus {
// This method returns the byte size of the property's data.
guard EQMDriver.validateDriver(inDriver) && EQMDriver.validateObject(inObjectID) else { return kAudioHardwareBadObjectError }
log("Invoking: \(EQMDriver.getEQMObjectClassName(from: inObjectID)).getPropertyDataSize(\(inAddress.pointee.mSelector.code))")
let size = ({ () -> UInt32? in
if let obj = EQMDriver.getEQMObject(from: inObjectID) {
return obj.getPropertyDataSize(objectID: inObjectID, address: inAddress.pointee)
} else {
return nil
}
})()
log("\(EQMDriver.getEQMObjectClassName(from: inObjectID)).getPropertyDataSize(\(inAddress.pointee.mSelector.code)) = \(size ?? 0)")
if size != nil {
outDataSize.pointee = size!
return noErr
} else {
return kAudioHardwareUnknownPropertyError
}
}
func EQM_GetPropertyData (inDriver: AudioServerPlugInDriverRef, inObjectID: AudioObjectID, inClientProcessID: pid_t, inAddress: UnsafePointer<AudioObjectPropertyAddress>, inQualifierDataSize: UInt32, inQualifierData: UnsafeRawPointer?, inDataSize: UInt32, outDataSize: UnsafeMutablePointer<UInt32>, outData: UnsafeMutableRawPointer) -> OSStatus {
// Fetches the data of the given property and places it in the provided buffer.
guard EQMDriver.validateDriver(inDriver) && EQMDriver.validateObject(inObjectID) else {
log("Invalid driver or object id while in EQM_GetPropertyData(\(inAddress.pointee.mSelector.code))")
return kAudioHardwareBadObjectError
}
log("Invoking: \(EQMDriver.getEQMObjectClassName(from: inObjectID)).getPropertyData(\(inAddress.pointee.mSelector.code))")
let data = ({ () -> EQMObjectProperty? in
if let obj = EQMDriver.getEQMObject(from: inObjectID) {
return obj.getPropertyData(objectID: inObjectID, address: inAddress.pointee, inData: inQualifierData)
} else {
return nil
}
})()
log("\(EQMDriver.getEQMObjectClassName(from: inObjectID)).getPropertyData(\(inAddress.pointee.mSelector.code)) = \(String(describing: data))")
if data != nil {
data!.write(to: outData, size: outDataSize, requestedSize: inDataSize)
return noErr
} else {
outDataSize.pointee = 0
return kAudioHardwareUnknownPropertyError
}
}
func EQM_SetPropertyData (inDriver: AudioServerPlugInDriverRef, inObjectID: AudioObjectID, inClientProcessID: pid_t, inAddress: UnsafePointer<AudioObjectPropertyAddress>, inQualifierDataSize: UInt32, inQualifierData: UnsafeRawPointer?, inDataSize: UInt32, inData: UnsafeRawPointer) -> OSStatus {
// Tells an object to change the value of the given property.
guard EQMDriver.validateDriver(inDriver) else { return kAudioHardwareBadObjectError }
log("\(EQMDriver.getEQMObjectClassName(from: inObjectID)).setPropertyData(\(inAddress.pointee.mSelector.code)) = \(String(describing: inData))")
if let obj = EQMDriver.getEQMObject(from: inObjectID) {
var changedProperties: [AudioObjectPropertyAddress] = []
let status = obj.setPropertyData(objectID: inObjectID, address: inAddress.pointee, data: inData, changedProperties: &changedProperties)
if changedProperties.count > 0 {
_ = EQMDriver.host!.pointee.PropertiesChanged(EQMDriver.host!, inObjectID, UInt32(changedProperties.count), changedProperties)
}
return status
} else {
return kAudioHardwareBadObjectError
}
}
// MARK: - IO Operations
func EQM_StartIO (inDriver: AudioServerPlugInDriverRef, inDeviceObjectID: AudioObjectID, inClientID: UInt32) -> OSStatus {
// This call tells the device that IO is starting for the given client. When this routine
// returns, the device's clock is running and it is ready to have data read/written. It is
// important to note that multiple clients can have IO running on the device at the same time.
// So, work only needs to be done when the first client starts. All subsequent starts simply
// increment the counter.
guard EQMDriver.validateDriver(inDriver) else { return kAudioHardwareBadObjectError }
return EQMDevice.startIO()
}
func EQM_StopIO (inDriver: AudioServerPlugInDriverRef, inDeviceObjectID: AudioObjectID, inClientID: UInt32) -> OSStatus {
// This call tells the device that the client has stopped IO. The driver can stop the hardware
// once all clients have stopped.
guard EQMDriver.validateDriver(inDriver) else { return kAudioHardwareBadObjectError }
return EQMDevice.stopIO()
}
func EQM_GetZeroTimeStamp (inDriver: AudioServerPlugInDriverRef, inDeviceObjectID: AudioObjectID, inClientID: UInt32, outSampleTime: UnsafeMutablePointer<Float64>, outHostTime: UnsafeMutablePointer<UInt64>, outSeed: UnsafeMutablePointer<UInt64>) -> OSStatus {
// This method returns the current zero time stamp for the device. The HAL models the timing of
// a device as a series of time stamps that relate the sample time to a host time. The zero
// time stamps are spaced such that the sample times are the value of
// kAudioDevicePropertyZeroTimeStampPeriod apart. This is often modeled using a ring buffer
// where the zero time stamp is updated when wrapping around the ring buffer.
//
// For this device, the zero time stamps' sample time increments every kDevice_RingBufferSize
// frames and the host time increments by kDevice_RingBufferSize * gDevice_HostTicksPerFrame.
guard EQMDriver.validateDriver(inDriver) else { return kAudioHardwareBadObjectError }
return EQMDevice.getZeroTimeStamp(
outSampleTime: outSampleTime,
outHostTime: outHostTime,
outSeed: outSeed
)
}
func EQM_WillDoIOOperation (inDriver: AudioServerPlugInDriverRef, inDeviceObjectID: AudioObjectID, inClientID: UInt32, inOperationID: UInt32, outWillDo: UnsafeMutablePointer<DarwinBoolean>, outWillDoInPlace: UnsafeMutablePointer<DarwinBoolean>) -> OSStatus {
// Asks the plug-in whether or not the device will perform the given phase of the IO cycle for a particular device.
// This method returns whether or not the device will do a given IO operation. For this device,
// we only support reading input data and writing output data.
guard EQMDriver.validateDriver(inDriver) else { return kAudioHardwareBadObjectError }
var willDo = false
var willDoInPlace = true
switch inOperationID {
case AudioServerPlugInIOOperation.writeMix.rawValue:
willDo = true
willDoInPlace = true
break
case AudioServerPlugInIOOperation.readInput.rawValue:
willDo = true
willDoInPlace = true
break
default: break
}
outWillDo.pointee = DarwinBoolean(willDo)
outWillDoInPlace.pointee = DarwinBoolean(willDoInPlace)
return noErr
}
func EQM_BeginIOOperation (inDriver: AudioServerPlugInDriverRef, inDeviceObjectID: AudioObjectID, inClientID: UInt32, inOperationID: UInt32, inIOBufferFrameSize: UInt32, inIOCycleInfo: UnsafePointer<AudioServerPlugInIOCycleInfo>) -> OSStatus {
// Tells the plug-in that the Host is about to begin a phase of the IO cycle for a particular device.
// This is called at the beginning of an IO operation. This device doesn't do anything, so just
// check the arguments and return.
guard EQMDriver.validateDriver(inDriver) else { return kAudioHardwareBadObjectError }
return noErr
}
func EQM_DoIOOperation (inDriver: AudioServerPlugInDriverRef, inDeviceObjectID: AudioObjectID, inStreamObjectID: AudioObjectID, inClientID: UInt32, inOperationID: UInt32, inIOBufferFrameSize: UInt32, inIOCycleInfo: UnsafePointer<AudioServerPlugInIOCycleInfo>, ioMainBuffer: UnsafeMutableRawPointer?, ioSecondaryBuffer: UnsafeMutableRawPointer?) -> OSStatus {
// This is called to actuall perform a given operation.
guard EQMDriver.validateDriver(inDriver) else { return kAudioHardwareBadObjectError }
guard let sample = ioMainBuffer?.assumingMemoryBound(to: Float32.self) else {
return noErr
}
return EQMDevice.doIO(
clientID: inClientID,
operationID: inOperationID,
sample: sample,
sampleTime: Int(inIOCycleInfo.pointee.mInputTime.mSampleTime),
frameSize: inIOBufferFrameSize
)
}
func EQM_EndIOOperation (inDriver: AudioServerPlugInDriverRef, inDeviceObjectID: AudioObjectID, inClientID: UInt32, inOperationID: UInt32, inIOBufferFrameSize: UInt32, inIOCycleInfo: UnsafePointer<AudioServerPlugInIOCycleInfo>) -> OSStatus {
// This is called at the end of an IO operation. This device doesn't do anything, so just check
// the arguments and return.
guard EQMDriver.validateDriver(inDriver) else { return kAudioHardwareBadObjectError }
return noErr
}

View File

@ -0,0 +1,101 @@
//
// EQMObject.swift
// eqMac
//
// Created by Romans Kisils on 21/08/2021.
// Copyright © 2021 Bitgapp. All rights reserved.
//
import Foundation
import CoreAudio.AudioServerPlugIn
protocol EQMObject: class {
static func hasProperty (objectID: AudioObjectID?, address: AudioObjectPropertyAddress) -> Bool
static func isPropertySettable (objectID: AudioObjectID?, address: AudioObjectPropertyAddress) -> Bool
static func getPropertyDataSize (objectID: AudioObjectID?, address: AudioObjectPropertyAddress) -> UInt32?
static func getPropertyData (objectID: AudioObjectID?, address: AudioObjectPropertyAddress, inData: UnsafeRawPointer?) -> EQMObjectProperty?
static func setPropertyData (objectID: AudioObjectID?, address: AudioObjectPropertyAddress, data: UnsafeRawPointer, changedProperties: inout [AudioObjectPropertyAddress]) -> OSStatus
}
enum EQMObjectProperty {
// Primitives
case audioClassID(AudioClassID)
case bool(CFBoolean)
case string(CFString)
case integer(UInt32)
case float32(Float32)
case float64(Float64)
case url(CFURL)
case scope(AudioObjectPropertyScope)
case element(AudioObjectPropertyElement)
// Structs
case streamDescription(AudioStreamBasicDescription)
case channelLayout(AudioChannelLayout)
case valueRange(AudioValueRange)
// Arrays
case objectIDList(ContiguousArray<AudioObjectID>)
case customPropertyInfoList(ContiguousArray<AudioServerPlugInCustomPropertyInfo>)
case streamDescriptionList(ContiguousArray<AudioStreamRangedDescription>)
case valueRangeList(ContiguousArray<AudioValueRange>)
case integerList(ContiguousArray<UInt32>)
func write(to address: UnsafeMutableRawPointer?, size: UnsafeMutablePointer<UInt32>, requestedSize: UInt32?) {
switch self {
case .bool(let data): self.write(element: data, address: address, size: size)
case .string(let data): self.write(element: data, address: address, size: size)
case .float32(let data): self.write(element: data, address: address, size: size)
case .float64(let data): self.write(element: data, address: address, size: size)
case .url(let data): self.write(element: data, address: address, size: size)
case .integer(let data),
.scope(let data),
.element(let data),
.audioClassID(let data): self.write(element: data, address: address, size: size)
case .streamDescription(let data): self.write(element: data, address: address, size: size)
case .channelLayout(let data): self.write(element: data, address: address, size: size)
case .valueRange(let data): self.write(element: data, address: address, size: size)
case .objectIDList(let data): self.write(array: data, address: address, size: size, requestedSize: requestedSize)
case .customPropertyInfoList(let data): self.write(array: data, address: address, size: size, requestedSize: requestedSize)
case .streamDescriptionList(let data): self.write(array: data, address: address, size: size, requestedSize: requestedSize)
case .valueRangeList(let data): self.write(array: data, address: address, size: size, requestedSize: requestedSize)
case .integerList(let data): self.write(array: data, address: address, size: size, requestedSize: requestedSize)
}
}
private func write<T>(element: T, address: UnsafeMutableRawPointer?, size: UnsafeMutablePointer<UInt32>) {
// Write data size
size.pointee = UInt32(MemoryLayout.size(ofValue: element))
// Write data
address?.assumingMemoryBound(to: T.self).pointee = element
}
private func write<T: Any>(array arr: ContiguousArray<T>, address: UnsafeMutableRawPointer?, size: UnsafeMutablePointer<UInt32>, requestedSize: UInt32?) {
var array = arr
let elementSize = sizeof(ContiguousArray<T>.Element.self)
if requestedSize != nil {
let requestedCount = Int(requestedSize! / elementSize)
array = ContiguousArray(array[0..<requestedCount])
log("Data Count: \(arr.count) - Data Size: \(UInt32(arr.count) * elementSize) - Requested Size: \(requestedSize!) - Resulting Count: \(array.count) - Resulting Size: \(UInt32(array.count) * elementSize)")
}
let totalSize = UInt32(array.count) * elementSize
// Write data size
size.pointee = totalSize
// Write data
guard let address = address, totalSize > 0 else {
return
}
var currentAddress = address.assumingMemoryBound(to: ContiguousArray<T>.Element.self)
for element in array {
currentAddress.pointee = element
currentAddress = currentAddress.advanced(by: 1)
}
}
}

View File

@ -9,7 +9,7 @@
import Foundation
import CoreAudio.AudioServerPlugIn
class EQMPlugIn: EQMObjectProtocol {
class EQMPlugIn: EQMObject {
static let id = AudioObjectID(kPlugInBundleId)!
static func hasProperty (objectID: AudioObjectID? = nil, address: AudioObjectPropertyAddress) -> Bool {
@ -74,7 +74,7 @@ class EQMPlugIn: EQMObjectProtocol {
}
}
static func getPropertyData (objectID: AudioObjectID? = nil, address: AudioObjectPropertyAddress) -> EQMObjectProperty? {
static func getPropertyData (objectID: AudioObjectID? = nil, address: AudioObjectPropertyAddress, inData: UnsafeRawPointer?) -> EQMObjectProperty? {
switch address.mSelector {
case kAudioObjectPropertyBaseClass:
// The base class for kAudioPlugInClassID is kAudioObjectClassID
@ -93,7 +93,12 @@ class EQMPlugIn: EQMObjectProtocol {
case kAudioPlugInPropertyBoxList:
return .objectIDList([kObjectID_Box])
case kAudioPlugInPropertyTranslateUIDToBox:
return .integer(kObjectID_Box)
let uid = inData?.assumingMemoryBound(to: CFString?.self).pointee
if (CFStringCompare(uid, kBoxUID as CFString, .init(rawValue: 0)) == CFComparisonResult.compareEqualTo) {
return .integer(kObjectID_Box)
} else {
return .integer(kAudioObjectUnknown)
}
case kAudioPlugInPropertyDeviceList:
return .objectIDList([kObjectID_Device])
case kAudioPlugInPropertyTranslateUIDToDevice:
@ -102,7 +107,12 @@ class EQMPlugIn: EQMObjectProtocol {
// just the one device. Note that it is not an error if the string in the
// qualifier doesn't match any devices. In such case, kAudioObjectUnknown is
// the object ID to return.
return .integer(kObjectID_Device)
let uid = inData?.assumingMemoryBound(to: CFString?.self).pointee
if (CFStringCompare(uid, kDeviceUID as CFString, .init(rawValue: 0)) == CFComparisonResult.compareEqualTo) {
return .integer(kObjectID_Device)
} else {
return .integer(kAudioObjectUnknown)
}
case kAudioPlugInPropertyResourceBundle:
// The resource bundle is a path relative to the path of the plug-in's bundle.
// To specify that the plug-in bundle itself should be used, we just return the
@ -119,7 +129,7 @@ class EQMPlugIn: EQMObjectProtocol {
}
}
static func setPropertyData(objectID: AudioObjectID? = nil, address: AudioObjectPropertyAddress, data: UnsafeRawPointer) -> OSStatus {
static func setPropertyData(objectID: AudioObjectID? = nil, address: AudioObjectPropertyAddress, data: UnsafeRawPointer, changedProperties: inout [AudioObjectPropertyAddress]) -> OSStatus {
switch address.mSelector {
default: return kAudioHardwareUnknownPropertyError
}

View File

@ -9,7 +9,7 @@
import Foundation
import CoreAudio.AudioServerPlugIn
class EQMStream: EQMObjectProtocol {
class EQMStream: EQMObject {
static var inputActive = true
static var outputActive = true
static var description: AudioStreamBasicDescription {
@ -78,7 +78,7 @@ class EQMStream: EQMObjectProtocol {
}
}
static func getPropertyData (objectID: AudioObjectID?, address: AudioObjectPropertyAddress) -> EQMObjectProperty? {
static func getPropertyData (objectID: AudioObjectID?, address: AudioObjectPropertyAddress, inData: UnsafeRawPointer?) -> EQMObjectProperty? {
switch address.mSelector {
case kAudioObjectPropertyBaseClass:
@ -177,7 +177,7 @@ class EQMStream: EQMObjectProtocol {
}
}
static func setPropertyData(objectID: AudioObjectID?, address: AudioObjectPropertyAddress, data: UnsafeRawPointer) -> OSStatus {
static func setPropertyData(objectID: AudioObjectID?, address: AudioObjectPropertyAddress, data: UnsafeRawPointer, changedProperties: inout [AudioObjectPropertyAddress]) -> OSStatus {
switch address.mSelector {
case kAudioStreamPropertyIsActive:
// Changing the active state of a stream doesn't affect IO or change the structure

View File

@ -9,124 +9,12 @@
import Foundation
import CoreAudio.AudioServerPlugIn
// MARK: - Protocols, Enums
protocol EQMObjectProtocol {
static func hasProperty (objectID: AudioObjectID?, address: AudioObjectPropertyAddress) -> Bool
static func isPropertySettable (objectID: AudioObjectID?, address: AudioObjectPropertyAddress) -> Bool
static func getPropertyDataSize (objectID: AudioObjectID?, address: AudioObjectPropertyAddress) -> UInt32?
static func getPropertyData (objectID: AudioObjectID?, address: AudioObjectPropertyAddress) -> EQMObjectProperty?
static func setPropertyData (objectID: AudioObjectID?, address: AudioObjectPropertyAddress, data: UnsafeRawPointer) -> OSStatus
}
enum EQMObjectProperty {
// Primitives
case audioClassID(AudioClassID)
case bool(CFBoolean)
case string(CFString)
case integer(UInt32)
case float32(Float32)
case float64(Float64)
case url(CFURL)
case scope(AudioObjectPropertyScope)
case element(AudioObjectPropertyElement)
// Structs
case streamDescription(AudioStreamBasicDescription)
case channelLayout(AudioChannelLayout)
case valueRange(AudioValueRange)
// Arrays
case objectIDList(ContiguousArray<AudioObjectID>)
case customPropertyInfoList(ContiguousArray<AudioServerPlugInCustomPropertyInfo>)
case streamDescriptionList(ContiguousArray<AudioStreamRangedDescription>)
case valueRangeList(ContiguousArray<AudioValueRange>)
case integerList(ContiguousArray<UInt32>)
func write(to address: UnsafeMutableRawPointer?, size: UnsafeMutablePointer<UInt32>) {
switch self {
case .bool(let data): self.write(element: data, address: address, size: size)
case .string(let data): self.write(element: data, address: address, size: size)
case .float32(let data): self.write(element: data, address: address, size: size)
case .float64(let data): self.write(element: data, address: address, size: size)
case .url(let data): self.write(element: data, address: address, size: size)
case .integer(let data),
.scope(let data),
.element(let data),
.audioClassID(let data): self.write(element: data, address: address, size: size)
case .streamDescription(let data): self.write(element: data, address: address, size: size)
case .channelLayout(let data): self.write(element: data, address: address, size: size)
case .valueRange(let data): self.write(element: data, address: address, size: size)
case .objectIDList(let data): self.write(array: data, address: address, size: size)
case .customPropertyInfoList(let data): self.write(array: data, address: address, size: size)
case .streamDescriptionList(let data): self.write(array: data, address: address, size: size)
case .valueRangeList(let data): self.write(array: data, address: address, size: size)
case .integerList(let data): self.write(array: data, address: address, size: size)
}
}
private func write<T>(element: T, address: UnsafeMutableRawPointer?, size: UnsafeMutablePointer<UInt32>) {
// Write data size
size.pointee = UInt32(MemoryLayout.size(ofValue: element))
// Write data
address?.assumingMemoryBound(to: T.self).pointee = element
}
private func write<T: Collection>(array: T, address: UnsafeMutableRawPointer?, size: UnsafeMutablePointer<UInt32>) {
// Write data size
size.pointee = UInt32(array.count) * sizeof(T.Element.self)
// Write data
guard let address = address else {
return
}
var currentAddress = address.assumingMemoryBound(to: T.Element.self)
for element in array {
currentAddress.pointee = element
currentAddress = currentAddress.advanced(by: 1)
}
}
}
// MARK: - Pure Functions
func volumeToDecibel (_ volume: Float32) -> Float32 {
if (volume <= powf(10.0, kMinVolumeDB / 20.0)) {
return kMinVolumeDB
} else {
return 20.0 * log10f(volume)
func log (_ msg: String) {
if DEBUG {
let message = "coreaudiod: eqMac - \(msg)"
Swift.print(message)
NSLog(message)
}
}
func decibelToVolume (_ decibel: Float32) -> Float32 {
if (decibel <= kMinVolumeDB) {
return 0.0
} else {
return powf(10.0, decibel / 20.0)
}
}
func volumeToScalar (_ volume: Float32) -> Float32 {
let decibel = volumeToDecibel(volume);
return (decibel - kMinVolumeDB) / (kMaxVolumeDB - kMinVolumeDB);
}
func scalarToVolume (_ scalar: Float32) -> Float32 {
let decibel = scalar * (kMaxVolumeDB - kMinVolumeDB) + kMinVolumeDB
return decibelToVolume(decibel)
}
func clamp <T: Comparable> (value val: T, min: T, max: T) -> T {
var value = val
if value < min {
value = min
} else if value > min {
value = max
}
return value
}
// MARK: - Extensions

View File

@ -28,9 +28,7 @@
<key>CFPlugInTypes</key>
<dict>
<key>443ABAB8-E7B3-491A-B985-BEB9187030DB</key>
<array>
<string>7080ba34-76cc-40b2-b2b3-819d28460e7e</string>
</array>
<string>7080ba34-76cc-40b2-b2b3-819d28460e7e</string>
</dict>
<key>NSHumanReadableCopyright</key>
<string>Copyright © 2015 - 2021 Bitgapp Ltd.</string>

View File

@ -0,0 +1,47 @@
//
// AtomicCounter.swift
// eqMac
//
// Created by Romans Kisils on 20/08/2021.
// Copyright © 2021 Bitgapp. All rights reserved.
//
import Foundation
struct AtomicCounter<T: FixedWidthInteger> {
private let serialQueue: DispatchQueue
private var _value: T
var value: T {
get {
return self.serialQueue.sync {
self._value
}
}
set {
self.serialQueue.sync {
self._value = newValue
}
}
}
init(_ initialValue: T = 0) {
self._value = initialValue
let uuid = CFUUIDCreateString(nil, CFUUIDCreate(nil)) as String
self.serialQueue = DispatchQueue(label: "eqMac.AtomicCounter.\(uuid)")
}
mutating func increment() {
self.serialQueue.sync {
guard self._value < T.max else { return }
self._value += 1
}
}
mutating func decrement() {
self.serialQueue.sync {
guard self._value > T.min else { return }
self._value -= 1
}
}
}

View File

@ -7,5 +7,9 @@ extension AudioObjectPropertySelector {
static func fromString (_ str: String) -> AudioObjectPropertySelector {
return AudioObjectPropertySelector(UInt32.init(from: str.byteArray))
}
var code: String {
return String(bytes: withUnsafeBytes(of: self.bigEndian, Array.init), encoding: .utf8)!
}
}

View File

@ -9,25 +9,26 @@
import Foundation
extension String {
func toJSON() -> Any? {
guard let data = self.data(using: .utf8, allowLossyConversion: false) else { return nil }
return try? JSONSerialization.jsonObject(with: data, options: .mutableContainers)
}
func toDictionary() -> [String: AnyObject]? {
if let data = self.data(using: .utf8) {
do {
return try JSONSerialization.jsonObject(with: data, options: []) as? [String: AnyObject]
} catch {
print(error.localizedDescription)
}
}
return nil
}
var byteArray: [UInt8] {
return Array(self.utf8)
func toJSON() -> Any? {
guard let data = self.data(using: .utf8, allowLossyConversion: false) else { return nil }
return try? JSONSerialization.jsonObject(with: data, options: .mutableContainers)
}
func toDictionary() -> [String: AnyObject]? {
if let data = self.data(using: .utf8) {
do {
return try JSONSerialization.jsonObject(with: data, options: []) as? [String: AnyObject]
} catch {
print(error.localizedDescription)
}
}
return nil
}
var byteArray: [UInt8] {
return Array(self.utf8)
}
func trim() -> String {
return self.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
}
@ -36,24 +37,24 @@ extension String {
extension String: Error {}
extension String: LocalizedError {
public var errorDescription: String? { return self }
public var errorDescription: String? { return self }
}
extension String {
func capitalizingFirstLetter() -> String {
return prefix(1).uppercased() + dropFirst()
}
var camelCasedString: String {
return self.components(separatedBy: " ")
.enumerated()
.map { (index, component) in
var result = component.lowercased()
if (index != 0) {
result = result.capitalizingFirstLetter()
}
return result
}
.joined()
}
func capitalizingFirstLetter() -> String {
return prefix(1).uppercased() + dropFirst()
}
var camelCasedString: String {
return self.components(separatedBy: " ")
.enumerated()
.map { (index, component) in
var result = component.lowercased()
if (index != 0) {
result = result.capitalizingFirstLetter()
}
return result
}
.joined()
}
}

View File

@ -11,3 +11,13 @@ import Foundation
func sizeof <T> (_ type: T) -> UInt32 {
return UInt32(MemoryLayout.size(ofValue: type))
}
func clamp <T: Comparable> (value val: T, min: T, max: T) -> T {
var value = val
if value < min {
value = min
} else if value > min {
value = max
}
return value
}

View File

@ -0,0 +1,37 @@
//
// Volume.swift
// eqMac
//
// Created by Romans Kisils on 21/08/2021.
// Copyright © 2021 Bitgapp. All rights reserved.
//
import Foundation
class Volume {
static func toDecibel (_ volume: Float32) -> Float32 {
if (volume <= powf(10.0, kMinVolumeDB / 20.0)) {
return kMinVolumeDB
} else {
return 20.0 * log10f(volume)
}
}
static func fromDecibel (_ decibel: Float32) -> Float32 {
if (decibel <= kMinVolumeDB) {
return 0.0
} else {
return powf(10.0, decibel / 20.0)
}
}
static func toScalar (_ volume: Float32) -> Float32 {
let decibel = toDecibel(volume);
return (decibel - kMinVolumeDB) / (kMaxVolumeDB - kMinVolumeDB);
}
static func fromScalar (_ scalar: Float32) -> Float32 {
let decibel = scalar * (kMaxVolumeDB - kMinVolumeDB) + kMinVolumeDB
return fromDecibel(decibel)
}
}