diff --git a/BGMApp/BGMApp/BGMAudioDevice.cpp b/BGMApp/BGMApp/BGMAudioDevice.cpp index d61507f7..5099d095 100644 --- a/BGMApp/BGMApp/BGMAudioDevice.cpp +++ b/BGMApp/BGMApp/BGMAudioDevice.cpp @@ -72,6 +72,20 @@ bool BGMAudioDevice::CanBeOutputDeviceInBGMApp() const canBeDefault; } +#pragma mark Device Type + +bool BGMAudioDevice::IsAggregate() const +{ + try + { + return GetTransportType() == kAudioDeviceTransportTypeAggregate; + } + catch(CAException) + { + return false; + } +} + #pragma mark Available Controls bool BGMAudioDevice::HasSettableMasterVolume(AudioObjectPropertyScope inScope) const @@ -115,6 +129,12 @@ bool BGMAudioDevice::HasSettableMasterMute(AudioObjectPropertyScope inScope) void BGMAudioDevice::CopyMuteFrom(const BGMAudioDevice inDevice, AudioObjectPropertyScope inScope) { + // Skip aggregate devices for the same reason as in CopyVolumeFrom. + if(IsAggregate() || inDevice.IsAggregate()) + { + return; + } + // TODO: Support for devices that have per-channel mute controls but no master mute control if(HasSettableMasterMute(inScope) && inDevice.HasMuteControl(inScope, kMasterChannel)) { @@ -127,6 +147,17 @@ void BGMAudioDevice::CopyMuteFrom(const BGMAudioDevice inDevice, void BGMAudioDevice::CopyVolumeFrom(const BGMAudioDevice inDevice, AudioObjectPropertyScope inScope) { + // Don't try to copy volume to/from aggregate devices. Aggregate devices rely on macOS to + // manage their virtual volume control through the sub-devices. Using the deprecated + // AudioHardwareService APIs (which we use for virtual master volume) to write to an aggregate + // device can corrupt its volume control state, causing the volume slider to become permanently + // disabled in System Settings. See https://github.com/kyleneideck/BackgroundMusic/issues/848 + if(IsAggregate() || inDevice.IsAggregate()) + { + DebugMsg("BGMAudioDevice::CopyVolumeFrom: Skipping volume copy for aggregate device"); + return; + } + // Get the volume of the other device. bool didGetVolume = false; Float32 volume = FLT_MIN; @@ -155,7 +186,7 @@ void BGMAudioDevice::CopyVolumeFrom(const BGMAudioDevice inDevice, if(numChannels > 0) // Avoid divide by zero. { - volume /= numChannels; + volume /= static_cast(numChannels); } } diff --git a/BGMApp/BGMApp/BGMAudioDevice.h b/BGMApp/BGMApp/BGMAudioDevice.h index 051dcdc3..71e3adb2 100644 --- a/BGMApp/BGMApp/BGMAudioDevice.h +++ b/BGMApp/BGMApp/BGMAudioDevice.h @@ -78,6 +78,14 @@ class BGMAudioDevice */ bool CanBeOutputDeviceInBGMApp() const; +#pragma mark Device Type + + /*! + @return True if this device is an aggregate device (i.e. a device created in Audio MIDI Setup + that combines multiple audio devices). + */ + bool IsAggregate() const; + #pragma mark Available Controls bool HasSettableMasterVolume(AudioObjectPropertyScope inScope) const; diff --git a/BGMApp/BGMApp/BGMAutoPauseMusic.mm b/BGMApp/BGMApp/BGMAutoPauseMusic.mm index c4514255..1154bd8e 100644 --- a/BGMApp/BGMApp/BGMAutoPauseMusic.mm +++ b/BGMApp/BGMApp/BGMAutoPauseMusic.mm @@ -206,7 +206,7 @@ - (void) queueUnpauseBlock { // TODO: Fading in and out would make short pauses a lot less jarring because, if they were short enough, we wouldn't // actually pause the music player. So you'd hear a dip in the music's volume rather than a gap. UInt64 unpauseDelayNsec = - static_cast((wentSilent - wentAudible) * kUnpauseDelayWeightingFactor); + static_cast(static_cast(wentSilent - wentAudible) * kUnpauseDelayWeightingFactor); // Convert from absolute time to nanos. mach_timebase_info_data_t info; diff --git a/BGMApp/BGMApp/BGMDeviceControlsList.cpp b/BGMApp/BGMApp/BGMDeviceControlsList.cpp index 7a0a186d..397dc7f0 100644 --- a/BGMApp/BGMApp/BGMDeviceControlsList.cpp +++ b/BGMApp/BGMApp/BGMDeviceControlsList.cpp @@ -186,9 +186,21 @@ bool BGMDeviceControlsList::MatchControlsListOf(AudioObjectID inDeviceID) // Check which controls the other device has. BGMAudioDevice device(inDeviceID); - bool hasMute = device.HasSettableMasterMute(inScope); - bool hasVolume = + // Aggregate devices (created in Audio MIDI Setup) rely on macOS to provide virtual volume + // control through the sub-devices. The deprecated AudioHardwareService APIs we use to detect + // volume support can return incorrect results for aggregate devices on newer macOS versions, + // which causes us to incorrectly disable BGMDevice's volume control. This then triggers + // PropagateControlListChange (which toggles the default device through a null device), and + // that can permanently break the aggregate device's volume slider in System Settings. + // + // To avoid this, always report that aggregate devices have volume and mute controls, since + // macOS provides virtual volume/mute for them regardless of what the HAL reports. + bool isAggregate = device.IsAggregate(); + + bool hasMute = isAggregate || device.HasSettableMasterMute(inScope); + + bool hasVolume = isAggregate || device.HasSettableMasterVolume(inScope) || device.HasSettableVirtualMasterVolume(inScope); if(!hasVolume) diff --git a/BGMApp/BGMApp/BGMPlayThrough.cpp b/BGMApp/BGMApp/BGMPlayThrough.cpp index 1d614a6d..ffbfacfd 100644 --- a/BGMApp/BGMApp/BGMPlayThrough.cpp +++ b/BGMApp/BGMApp/BGMPlayThrough.cpp @@ -606,8 +606,8 @@ OSStatus BGMPlayThrough::WaitForOutputDeviceToStart() noexcept DebugMsg("BGMPlayThrough::WaitForOutputDeviceToStart: Started %f ms after notification, %f " "ms after entering WaitForOutputDeviceToStart.", - static_cast(startedBy - mToldOutputDeviceToStartAt) * base / NSEC_PER_MSEC, - static_cast(startedBy - startedAt) * base / NSEC_PER_MSEC); + static_cast(startedBy - mToldOutputDeviceToStartAt) * static_cast(base) / NSEC_PER_MSEC, + static_cast(startedBy - startedAt) * static_cast(base) / NSEC_PER_MSEC); } // Figure out which error code to return. @@ -1121,7 +1121,7 @@ OSStatus BGMPlayThrough::OutputDeviceIOProc(AudioObjectID inDevice, refCon->mInToOutSampleOffset); // Recalculate the in-to-out offset and read head. - refCon->mInToOutSampleOffset = inOutputTime->mSampleTime - lastInputSampleTime; + refCon->mInToOutSampleOffset = inOutputTime->mSampleTime - static_cast(lastInputSampleTime); readHeadSampleTime = static_cast( inOutputTime->mSampleTime - refCon->mInToOutSampleOffset); } diff --git a/BGMDriver/BGMDriver/BGM_Device.cpp b/BGMDriver/BGMDriver/BGM_Device.cpp index 8cecf36e..fe420d82 100644 --- a/BGMDriver/BGMDriver/BGM_Device.cpp +++ b/BGMDriver/BGMDriver/BGM_Device.cpp @@ -1361,8 +1361,8 @@ void BGM_Device::GetZeroTimeStamp(Float64& outSampleTime, UInt64& outHostTime, U } // set the return values - outSampleTime = mLoopbackTime.numberTimeStamps * kLoopbackRingBufferFrameSize; - outHostTime = static_cast(mLoopbackTime.anchorHostTime + (static_cast(mLoopbackTime.numberTimeStamps) * theHostTicksPerRingBuffer)); + outSampleTime = static_cast(mLoopbackTime.numberTimeStamps) * kLoopbackRingBufferFrameSize; + outHostTime = static_cast(static_cast(mLoopbackTime.anchorHostTime) + (static_cast(mLoopbackTime.numberTimeStamps) * theHostTicksPerRingBuffer)); // TODO: I think we should increment outSeed whenever this device switches to/from having a wrapped engine outSeed = 1; } @@ -1523,16 +1523,13 @@ void BGM_Device::EndIOOperation(UInt32 inOperationID, UInt32 inIOBufferFrameSize void BGM_Device::ReadInputData(UInt32 inIOBufferFrameSize, Float64 inSampleTime, void* outBuffer) { // Wrap the provided buffer in an AudioBufferList. - AudioBufferList abl = { - .mNumberBuffers = 1, - .mBuffers[0] = { - .mNumberChannels = 2, - // Each frame is 2 Float32 samples (one per channel). The number of frames * the number - // of bytes per frame = the size of outBuffer in bytes. - .mDataByteSize = static_cast(inIOBufferFrameSize * sizeof(Float32) * 2), - .mData = outBuffer - } - }; + AudioBufferList abl; + abl.mNumberBuffers = 1; + abl.mBuffers[0].mNumberChannels = 2; + // Each frame is 2 Float32 samples (one per channel). The number of frames * the number + // of bytes per frame = the size of outBuffer in bytes. + abl.mBuffers[0].mDataByteSize = static_cast(inIOBufferFrameSize * sizeof(Float32) * 2); + abl.mBuffers[0].mData = outBuffer; // Copy the audio data from our ring buffer into the provided buffer. CARingBufferError err = @@ -1562,16 +1559,13 @@ void BGM_Device::ReadInputData(UInt32 inIOBufferFrameSize, Float64 inSampleTime, void BGM_Device::WriteOutputData(UInt32 inIOBufferFrameSize, Float64 inSampleTime, const void* inBuffer) { // Wrap the provided buffer in an AudioBufferList. - AudioBufferList abl = { - .mNumberBuffers = 1, - .mBuffers[0] = { - .mNumberChannels = 2, - // Each frame is 2 Float32 samples (one per channel). The number of frames * the number - // of bytes per frame = the size of inBuffer in bytes. - .mDataByteSize = static_cast(inIOBufferFrameSize * sizeof(Float32) * 2), - .mData = const_cast(inBuffer) - } - }; + AudioBufferList abl; + abl.mNumberBuffers = 1; + abl.mBuffers[0].mNumberChannels = 2; + // Each frame is 2 Float32 samples (one per channel). The number of frames * the number + // of bytes per frame = the size of inBuffer in bytes. + abl.mBuffers[0].mDataByteSize = static_cast(inIOBufferFrameSize * sizeof(Float32) * 2); + abl.mBuffers[0].mData = const_cast(inBuffer); // Copy the audio data from the provided buffer into our ring buffer. CARingBufferError err = @@ -1908,7 +1902,7 @@ Float64 BGM_Device::_HW_GetSampleRate() const CAException(kAudioHardwareUnspecifiedError), "BGM_Device::_HW_GetSampleRate: No wrapped audio device"); - return mWrappedAudioEngine->GetSampleRate(); + return static_cast(mWrappedAudioEngine->GetSampleRate()); } kern_return_t BGM_Device::_HW_SetSampleRate(Float64 inNewSampleRate) diff --git a/BGMDriver/BGMDriver/BGM_NullDevice.cpp b/BGMDriver/BGMDriver/BGM_NullDevice.cpp index cc80b0cd..aac0eef8 100644 --- a/BGMDriver/BGMDriver/BGM_NullDevice.cpp +++ b/BGMDriver/BGMDriver/BGM_NullDevice.cpp @@ -471,8 +471,8 @@ void BGM_NullDevice::GetZeroTimeStamp(Float64& outSampleTime, (static_cast(mNumberTimeStamps) * theHostTicksPerPeriod); // Set the return values. - outSampleTime = mNumberTimeStamps * kZeroTimeStampPeriod; - outHostTime = static_cast(mAnchorHostTime + theHostTicksSinceAnchor); + outSampleTime = static_cast(mNumberTimeStamps) * kZeroTimeStampPeriod; + outHostTime = static_cast(static_cast(mAnchorHostTime) + theHostTicksSinceAnchor); outSeed = 1; } diff --git a/BGMDriver/BGMDriver/BGM_VolumeControl.cpp b/BGMDriver/BGMDriver/BGM_VolumeControl.cpp index 9e5f4c19..23ec028f 100644 --- a/BGMDriver/BGMDriver/BGM_VolumeControl.cpp +++ b/BGMDriver/BGMDriver/BGM_VolumeControl.cpp @@ -440,7 +440,7 @@ void BGM_VolumeControl::SetVolumeRaw(SInt32 inNewVolumeRaw) // TODO: This assumes the control should never boost the signal. (So, technically, it never // actually applies gain, only loss.) SInt32 theRawRange = mMaxVolumeRaw - mMinVolumeRaw; - SInt32 theSliderPositionInRawSteps = static_cast(theSliderPosition * theRawRange); + SInt32 theSliderPositionInRawSteps = static_cast(theSliderPosition * static_cast(theRawRange)); theSliderPositionInRawSteps += mMinVolumeRaw; mAmplitudeGain = mVolumeCurve.ConvertRawToScalar(theSliderPositionInRawSteps); diff --git a/BGMDriver/PublicUtility/CAVolumeCurve.cpp b/BGMDriver/PublicUtility/CAVolumeCurve.cpp index a68eb699..ec225dac 100644 --- a/BGMDriver/PublicUtility/CAVolumeCurve.cpp +++ b/BGMDriver/PublicUtility/CAVolumeCurve.cpp @@ -400,7 +400,7 @@ Float32 CAVolumeCurve::ConvertRawToDB(SInt32 inRaw) const SInt32 theRawStepsToAdd = std::min(theRawRange, theNumberRawSteps); // add this many steps worth of db to the answer; - theAnswer += theRawStepsToAdd * theDBPerRaw; + theAnswer += static_cast(theRawStepsToAdd) * theDBPerRaw; // figure out how many steps are left theNumberRawSteps -= theRawStepsToAdd;