Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 10 additions & 10 deletions MultiSoundChanger.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@

/* Begin PBXBuildFile section */
4743EFAB1E91493B0032F5AA /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4743EFAA1E91493B0032F5AA /* AppDelegate.swift */; };
6985C6FE251951F8003C2FDB /* OSD.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6985C6FD251951F8003C2FDB /* OSD.framework */; };
E4FFDC0757FD125F92CC0F62 /* Pods_MultiSoundChanger.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4C34C05E9BD81D579A0C4957 /* Pods_MultiSoundChanger.framework */; };
F312C54E25B3741C00205846 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F373D8C02561D24600642274 /* Main.storyboard */; };
F312C55025B3742200205846 /* Volume.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F373D8BC2561D22000642274 /* Volume.storyboard */; };
F3433FCB25B36E16009AAE86 /* Images.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3433FCA25B36E16009AAE86 /* Images.swift */; };
F373D8B32561D1A600642274 /* StatusBarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F373D8B02561D1A600642274 /* StatusBarController.swift */; };
F373D8B42561D1A600642274 /* AudioManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F373D8B12561D1A600642274 /* AudioManager.swift */; };
F373D8B52561D1A600642274 /* MediaManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F373D8B22561D1A600642274 /* MediaManager.swift */; };
AAA1B0C0D0E0F0A0B0C0D001 /* VolumeHUD.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAA1B0C0D0E0F0A0B0C0D002 /* VolumeHUD.swift */; };
F373D8BB2561D21900642274 /* Stories.swift in Sources */ = {isa = PBXBuildFile; fileRef = F373D8BA2561D21900642274 /* Stories.swift */; };
F373D8BF2561D22000642274 /* VolumeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F373D8BD2561D22000642274 /* VolumeViewController.swift */; };
F373D8C62561D2A600642274 /* Audio.swift in Sources */ = {isa = PBXBuildFile; fileRef = F373D8C52561D2A600642274 /* Audio.swift */; };
Expand All @@ -33,13 +33,13 @@
4743EFA71E91493B0032F5AA /* MultiSoundChanger.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MultiSoundChanger.app; sourceTree = BUILT_PRODUCTS_DIR; };
4743EFAA1E91493B0032F5AA /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
4C34C05E9BD81D579A0C4957 /* Pods_MultiSoundChanger.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_MultiSoundChanger.framework; sourceTree = BUILT_PRODUCTS_DIR; };
6985C6FD251951F8003C2FDB /* OSD.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = OSD.framework; sourceTree = "<group>"; };
6FD0ED04AFD1CC1242C9B3B3 /* Pods-MultiSoundChanger.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MultiSoundChanger.debug.xcconfig"; path = "Target Support Files/Pods-MultiSoundChanger/Pods-MultiSoundChanger.debug.xcconfig"; sourceTree = "<group>"; };
D184B2CD842B856AFFE7DF7E /* Pods-MultiSoundChanger.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MultiSoundChanger.release.xcconfig"; path = "Target Support Files/Pods-MultiSoundChanger/Pods-MultiSoundChanger.release.xcconfig"; sourceTree = "<group>"; };
F3433FCA25B36E16009AAE86 /* Images.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Images.swift; sourceTree = "<group>"; };
F373D8B02561D1A600642274 /* StatusBarController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatusBarController.swift; sourceTree = "<group>"; };
F373D8B12561D1A600642274 /* AudioManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AudioManager.swift; sourceTree = "<group>"; };
F373D8B22561D1A600642274 /* MediaManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaManager.swift; sourceTree = "<group>"; };
AAA1B0C0D0E0F0A0B0C0D002 /* VolumeHUD.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VolumeHUD.swift; sourceTree = "<group>"; };
F373D8BA2561D21900642274 /* Stories.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Stories.swift; sourceTree = "<group>"; };
F373D8BC2561D22000642274 /* Volume.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Volume.storyboard; sourceTree = "<group>"; };
F373D8BD2561D22000642274 /* VolumeViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VolumeViewController.swift; sourceTree = "<group>"; };
Expand All @@ -62,7 +62,6 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
6985C6FE251951F8003C2FDB /* OSD.framework in Frameworks */,
E4FFDC0757FD125F92CC0F62 /* Pods_MultiSoundChanger.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down Expand Up @@ -127,7 +126,6 @@
83889335DD9089B748A33010 /* Frameworks */ = {
isa = PBXGroup;
children = (
6985C6FD251951F8003C2FDB /* OSD.framework */,
4C34C05E9BD81D579A0C4957 /* Pods_MultiSoundChanger.framework */,
);
name = Frameworks;
Expand Down Expand Up @@ -161,6 +159,7 @@
F373D8B02561D1A600642274 /* StatusBarController.swift */,
F373D8B12561D1A600642274 /* AudioManager.swift */,
F373D8B22561D1A600642274 /* MediaManager.swift */,
AAA1B0C0D0E0F0A0B0C0D002 /* VolumeHUD.swift */,
);
path = Classes;
sourceTree = "<group>";
Expand Down Expand Up @@ -367,6 +366,7 @@
F373D8C62561D2A600642274 /* Audio.swift in Sources */,
F37C2ED1256AAA4C001C3D36 /* Strings.swift in Sources */,
F373D8B52561D1A600642274 /* MediaManager.swift in Sources */,
AAA1B0C0D0E0F0A0B0C0D001 /* VolumeHUD.swift in Sources */,
F373D8B42561D1A600642274 /* AudioManager.swift in Sources */,
F373D8B32561D1A600642274 /* StatusBarController.swift in Sources */,
4743EFAB1E91493B0032F5AA /* AppDelegate.swift in Sources */,
Expand Down Expand Up @@ -442,7 +442,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.10;
MACOSX_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = macosx;
Expand Down Expand Up @@ -497,7 +497,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.10;
MACOSX_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = macosx;
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
Expand All @@ -514,14 +514,14 @@
CODE_SIGN_STYLE = Manual;
COMBINE_HIDPI_IMAGES = YES;
DEVELOPMENT_TEAM = "";
EXCLUDED_ARCHS = arm64;
EXCLUDED_ARCHS = "";
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)",
);
INFOPLIST_FILE = MultiSoundChanger/Other/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 10.10;
MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 1.0.1;
PRODUCT_BUNDLE_IDENTIFIER = com.rlxone.multisoundchanger;
PRODUCT_NAME = "$(TARGET_NAME)";
Expand All @@ -540,14 +540,14 @@
CODE_SIGN_STYLE = Manual;
COMBINE_HIDPI_IMAGES = YES;
DEVELOPMENT_TEAM = "";
EXCLUDED_ARCHS = arm64;
EXCLUDED_ARCHS = "";
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)",
);
INFOPLIST_FILE = MultiSoundChanger/Other/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 10.10;
MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 1.0.1;
PRODUCT_BUNDLE_IDENTIFIER = com.rlxone.multisoundchanger;
PRODUCT_NAME = "$(TARGET_NAME)";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,5 @@
#define MultiSoundChanger_Bridging_Header_h

#import <Foundation/Foundation.h>
#import <OSD/OSDManager.h>

#endif /* MultiSoundChanger_Bridging_Header_h */
26 changes: 1 addition & 25 deletions MultiSoundChanger/Sources/Classes/MediaManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,31 +43,7 @@ final class MediaManagerImpl: MediaManager {
}

func showOSD(volume: Float, chicletsCount: Int = 16) {
guard let manager = OSDManager.sharedManager() as? OSDManager else {
return
}

let mouseloc: NSPoint = NSEvent.mouseLocation
var displayForPoint: CGDirectDisplayID = 0
var count: UInt32 = 0

if CGGetDisplaysWithPoint(mouseloc, 1, &displayForPoint, &count) != .success {
Logger.warning(Constants.InnerMessages.getDisplayError)
displayForPoint = CGMainDisplayID()
}

let image = (volume == 0) ? OSDGraphicSpeakerMuted.rawValue : OSDGraphicSpeaker.rawValue
let volumeStep: Float = 100 / Float(chicletsCount)

manager.showImage(
Int64(image),
onDisplayID: displayForPoint,
priority: 0x1F4,
msecUntilFade: 1_000,
filledChiclets: UInt32(volume / volumeStep),
totalChiclets: UInt32(100.0 / volumeStep),
locked: false
)
VolumeHUD.shared.show(volume: volume, muted: volume == 0)
}

// MARK: Private
Expand Down
171 changes: 171 additions & 0 deletions MultiSoundChanger/Sources/Classes/VolumeHUD.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
//
// VolumeHUD.swift
// MultiSoundChanger
//

import Cocoa

final class VolumeHUD {
static let shared = VolumeHUD()

private var window: NSWindow?
private var hideWorkItem: DispatchWorkItem?
private let chicletCount = 16

private init() {}

func show(volume: Float, muted: Bool) {
DispatchQueue.main.async { [weak self] in
self?.present(volume: volume, muted: muted)
}
}

private func present(volume: Float, muted: Bool) {
let win = window ?? makeWindow()
window = win

let view = win.contentView as? HUDView
view?.update(volume: volume, muted: muted, chicletCount: chicletCount)

positionWindow(win)
win.orderFrontRegardless()

hideWorkItem?.cancel()
let item = DispatchWorkItem { [weak self] in
self?.fadeOut()
}
hideWorkItem = item
DispatchQueue.main.asyncAfter(deadline: .now() + 1.2, execute: item)

win.alphaValue = 1.0
}

private func fadeOut() {
guard let win = window else { return }
NSAnimationContext.runAnimationGroup({ ctx in
ctx.duration = 0.3
win.animator().alphaValue = 0.0
}, completionHandler: { [weak self] in
self?.window?.orderOut(nil)
})
}

private func makeWindow() -> NSWindow {
let size = NSSize(width: 220, height: 220)
let w = NSWindow(
contentRect: NSRect(origin: .zero, size: size),
styleMask: .borderless,
backing: .buffered,
defer: false
)
w.isOpaque = false
w.backgroundColor = .clear
w.level = .statusBar
w.ignoresMouseEvents = true
w.collectionBehavior = [.canJoinAllSpaces, .stationary, .ignoresCycle, .fullScreenAuxiliary]
w.hasShadow = false
w.contentView = HUDView(frame: NSRect(origin: .zero, size: size))
return w
}

private func positionWindow(_ win: NSWindow) {
let mouseLoc = NSEvent.mouseLocation
let screen = NSScreen.screens.first { NSMouseInRect(mouseLoc, $0.frame, false) } ?? NSScreen.main
guard let screen = screen else { return }
let frame = screen.frame
let size = win.frame.size
let origin = NSPoint(
x: frame.midX - size.width / 2,
y: frame.minY + 140
)
win.setFrameOrigin(origin)
}
}

// MARK: - HUD View

final class HUDView: NSView {
private var volume: Float = 0
private var muted: Bool = false
private var chicletCount: Int = 16

override init(frame: NSRect) {
super.init(frame: frame)
wantsLayer = true
}

required init?(coder: NSCoder) { super.init(coder: coder) }

func update(volume: Float, muted: Bool, chicletCount: Int) {
self.volume = volume
self.muted = muted
self.chicletCount = chicletCount
needsDisplay = true
}

override var isFlipped: Bool { false }

override func draw(_ dirtyRect: NSRect) {
// Rounded background panel
let panelRect = bounds.insetBy(dx: 0, dy: 0)
let panelPath = NSBezierPath(roundedRect: panelRect, xRadius: 22, yRadius: 22)
NSColor(white: 0.10, alpha: 0.86).setFill()
panelPath.fill()

// Speaker icon
let iconRect = NSRect(x: bounds.midX - 32, y: bounds.maxY - 90, width: 64, height: 64)
let speakerName: String = muted ? "speaker.slash.fill" : (volume < 1 ? "speaker.fill" : (volume < 50 ? "speaker.wave.1.fill" : (volume < 80 ? "speaker.wave.2.fill" : "speaker.wave.3.fill")))
if let img = NSImage(systemSymbolName: speakerName, accessibilityDescription: nil) {
let conf = NSImage.SymbolConfiguration(pointSize: 44, weight: .regular)
let configured = img.withSymbolConfiguration(conf) ?? img
configured.isTemplate = true
let tinted = NSImage(size: configured.size, flipped: false) { rect in
configured.draw(in: rect)
NSColor.white.set()
rect.fill(using: .sourceAtop)
return true
}
tinted.draw(in: iconRect, from: .zero, operation: .sourceOver, fraction: 1.0,
respectFlipped: true, hints: [.interpolation: NSImageInterpolation.high.rawValue])
}

// Chiclets
let chicletAreaWidth: CGFloat = 180
let chicletAreaHeight: CGFloat = 14
let originX = bounds.midX - chicletAreaWidth / 2
let originY: CGFloat = 50
let gap: CGFloat = 2
let totalGap = gap * CGFloat(chicletCount - 1)
let chicletWidth = (chicletAreaWidth - totalGap) / CGFloat(chicletCount)
let step = 100.0 / Float(chicletCount)
let filled = Int((volume / step).rounded())

for i in 0..<chicletCount {
let x = originX + CGFloat(i) * (chicletWidth + gap)
let rect = NSRect(x: x, y: originY, width: chicletWidth, height: chicletAreaHeight)
let path = NSBezierPath(roundedRect: rect, xRadius: 2, yRadius: 2)
if i < filled && !muted {
NSColor.white.setFill()
} else {
NSColor(white: 1.0, alpha: 0.22).setFill()
}
path.fill()
}

// Percentage text
let pctText = muted ? "Muted" : "\(Int(volume))%"
let attrs: [NSAttributedString.Key: Any] = [
.font: NSFont.systemFont(ofSize: 14, weight: .medium),
.foregroundColor: NSColor.white
]
let str = NSAttributedString(string: pctText, attributes: attrs)
let textSize = str.size()
let textRect = NSRect(
x: bounds.midX - textSize.width / 2,
y: 20,
width: textSize.width,
height: textSize.height
)
str.draw(in: textRect)
}
}
16 changes: 14 additions & 2 deletions MultiSoundChanger/Sources/Stories/Volume/Volume.storyboard
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,34 @@
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<subviews>
<slider verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Imv-Ah-oMz">
<rect key="frame" x="18" y="-2" width="217" height="28"/>
<rect key="frame" x="18" y="-2" width="177" height="28"/>
<sliderCell key="cell" continuous="YES" state="on" alignment="left" maxValue="100" doubleValue="50" tickMarkPosition="above" sliderType="linear" id="Prx-bs-PU2"/>
<connections>
<action selector="volumeSliderAction:" target="EzY-mR-Bqy" id="Ata-d7-ikN"/>
</connections>
</slider>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="VoL-Lb-001">
<rect key="frame" x="200" y="6" width="40" height="16"/>
<textFieldCell key="cell" lineBreakMode="clipping" alignment="right" title="50%" id="VoL-Lb-002">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</subviews>
<constraints>
<constraint firstAttribute="bottom" secondItem="Imv-Ah-oMz" secondAttribute="bottom" constant="4" id="0Ws-Fb-wEv"/>
<constraint firstItem="Imv-Ah-oMz" firstAttribute="top" secondItem="Ot1-Uv-DQ0" secondAttribute="top" constant="4" id="6YS-3j-emH"/>
<constraint firstItem="Imv-Ah-oMz" firstAttribute="leading" secondItem="Ot1-Uv-DQ0" secondAttribute="leading" constant="20" id="CLf-zm-zAD"/>
<constraint firstAttribute="trailing" secondItem="Imv-Ah-oMz" secondAttribute="trailing" constant="20" id="hpE-zY-iJ2"/>
<constraint firstItem="VoL-Lb-001" firstAttribute="leading" secondItem="Imv-Ah-oMz" secondAttribute="trailing" constant="4" id="sLd-vL-001"/>
<constraint firstAttribute="trailing" secondItem="VoL-Lb-001" secondAttribute="trailing" constant="8" id="sLd-vL-002"/>
<constraint firstItem="VoL-Lb-001" firstAttribute="centerY" secondItem="Ot1-Uv-DQ0" secondAttribute="centerY" id="sLd-vL-003"/>
<constraint firstItem="VoL-Lb-001" firstAttribute="width" constant="40" id="sLd-vL-004"/>
</constraints>
</customView>
<connections>
<outlet property="volumeSlider" destination="Imv-Ah-oMz" id="jvJ-bq-KlG"/>
<outlet property="volumeLabel" destination="VoL-Lb-001" id="jvJ-bq-VL1"/>
</connections>
</viewController>
<customObject id="0bv-Tu-Tud" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import MediaKeyTap

final class VolumeViewController: NSViewController {
@IBOutlet weak var volumeSlider: NSSlider!
@IBOutlet weak var volumeLabel: NSTextField!
private var muted: Bool = false

weak var statusBarController: StatusBarController?
Expand All @@ -22,11 +23,14 @@ final class VolumeViewController: NSViewController {
}

func updateSliderVolume(volume: Float) {
volumeSlider.floatValue = volume.clamped(to: 0...100)
let clamped = volume.clamped(to: 0...100)
volumeSlider.floatValue = clamped
volumeLabel?.stringValue = "\(Int(clamped))%"
}

@IBAction func volumeSliderAction(_ sender: Any) {
changeDeviceVolume(value: volumeSlider.floatValue / 100)
statusBarController?.changeStatusItemImage(value: volumeSlider.floatValue)
volumeLabel?.stringValue = "\(Int(volumeSlider.floatValue))%"
}
}