Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
c767aba
Rebuild app for ARM64 (Apple Silicon) support
claude Nov 12, 2025
0b40ff4
Fix build errors and SwiftLint violations
claude Nov 12, 2025
026233b
Fix remaining SwiftLint violations and deprecations
claude Nov 12, 2025
564d6a9
Fix SwiftLint archive build failure
claude Nov 13, 2025
6c5a198
Update macOS deployment target to 11.0
claude Nov 13, 2025
abb7e3f
Fix crash when using volume hotkeys
claude Nov 13, 2025
3fb9a8b
Fix OSD window memory management and crashes
claude Nov 13, 2025
c1f47ed
Fix EXC_BAD_ACCESS crash with improved threading and lifecycle
claude Nov 13, 2025
e790d74
Drastically simplify OSD window management to fix crashes
claude Nov 13, 2025
f80feb8
Stop OSD from stealing focus; reuse window; clean up dead code
solartrans Apr 21, 2026
b9b9274
Sort device menu, fix volume-icon edge case, add a11y label
solartrans Apr 21, 2026
73c008d
Fix typo rigthLevel -> rightLevel; size device buffers by AudioDeviceID
solartrans Apr 21, 2026
00107bd
Default selectedDevice to system default on init
solartrans Apr 21, 2026
a60aa69
Prompt for accessibility only on startup, not on every tap restart
solartrans Apr 21, 2026
e2cd671
Tighten rollback docs; surface 11.0 minimum in README
solartrans Apr 21, 2026
1f6f134
Add CLAUDE.md for future Claude Code sessions
solartrans Apr 21, 2026
f91912b
CLAUDE.md: require commit+push after every round of changes
solartrans Apr 21, 2026
455a040
Remove orphaned x86-only OSD.framework directory
solartrans Apr 21, 2026
854bf2f
Migrate kAudioObjectPropertyElementMaster to Main (macOS 12+)
solartrans Apr 21, 2026
586db2d
Audio: drop Main #available branch; remove dead getDeviceType
solartrans Apr 21, 2026
5c49a15
AudioManager: collapse master/L/R volume triple into single `volume`
solartrans Apr 21, 2026
104bad4
Audio: check OSStatus on every HAL call; log with 2s cooldown
solartrans Apr 21, 2026
e643b8d
Live-update device menu via CoreAudio property listeners
solartrans Apr 21, 2026
2548d13
Round 1 audit follow-ups
solartrans Apr 21, 2026
9c1b1f7
Round 2 audit follow-ups: dead code + whitespace
solartrans Apr 21, 2026
5ed3f0d
Pass-3 audit follow-ups: real bugs + housekeeping
solartrans Apr 21, 2026
d3f467f
Main.storyboard: fix stale customModule from DynamicsIllusion to Mult…
solartrans Apr 21, 2026
dcfc47b
Pass-4 audit follow-ups
solartrans Apr 21, 2026
224dd77
Pass-5 audit follow-ups: narrow AudioManager surface
solartrans Apr 21, 2026
9f477c2
Pass-6 audit follow-ups
solartrans Apr 21, 2026
5f159a4
Pass-7 audit follow-ups: fix typos and unify follow/adopt naming
solartrans Apr 21, 2026
e9d0f60
Audio: extract listener methods to extension — under type_body_length
solartrans Apr 21, 2026
cd9f094
Pass-11 audit follow-ups
solartrans Apr 21, 2026
a97db34
Build-warning + responsiveness fixes
solartrans Apr 21, 2026
2d2c1c0
Cut HAL IPC on hotkeys; paint first; debounce slider drag
solartrans Apr 21, 2026
9f28ede
AudioManager: debounce HAL writes for hotkeys too (move from VC)
solartrans Apr 21, 2026
ab83bcf
Runner: migrate launchApplication to post-macOS-11 NSWorkspace API
solartrans Apr 21, 2026
9d64c61
Silence SwiftLint build-phase dependency-analysis warning
solartrans Apr 21, 2026
670a70a
Podfile: apply Xcode-recommended build hygiene to Pods targets
solartrans Apr 21, 2026
87b57b3
12-agent security audit follow-ups
solartrans Apr 21, 2026
1a9db6b
Resolve deferred security findings: Hardened Runtime + POSIX log writ…
solartrans Apr 21, 2026
72ae9a9
Phase-1 review follow-ups: Logger error surface + write loop + surrog…
solartrans Apr 21, 2026
f915fec
ARM64_MIGRATION.md: update Compatibility Notes for Hardened Runtime
solartrans Apr 21, 2026
4f601f6
MediaManager.deinit: cancel pending accessibility-notification work
solartrans Apr 21, 2026
2268b94
Logger: document that isLogFileRemoved is guarded by fileWriteQueue
solartrans Apr 21, 2026
ca8622e
Logger: cache DateFormatter as a static
solartrans Apr 21, 2026
a7fc3d2
Entitlements: disable library validation for ad-hoc source builds
solartrans Apr 21, 2026
792e3b0
Add hierarchical Claude context files; update entitlements narrative
solartrans Apr 21, 2026
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
224 changes: 224 additions & 0 deletions ARM64_MIGRATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
# ARM64 Migration Guide

## Overview

This document describes the changes made to rebuild MultiSoundChanger for ARM64 (Apple Silicon) architecture.

## Changes Made

### 1. Removed x86_64-only OSD.framework Dependency

**Problem**: The original app depended on OSD.framework, which was compiled only for x86_64 architecture and blocked ARM64 compilation.

**Solution**: Created a native Swift replacement (`NativeOSDManager.swift`) that provides the same OSD (On-Screen Display) functionality using native macOS APIs.

**Files Changed**:
- **MultiSoundChanger/Sources/Frameworks/NativeOSDManager.swift** (NEW)
- Native Swift implementation of OSD display
- Uses NSWindow and custom drawing for volume indicator
- Fully compatible with both x86_64 and ARM64
- Supports speaker and muted speaker icons
- Animated fade-in/fade-out effects

- **MultiSoundChanger/Other/MultiSoundChanger-Bridging-Header.h** (MODIFIED)
- Removed `#import <OSD/OSDManager.h>`
- Added comment explaining the change

### 2. Updated Xcode Project Configuration

**Files Changed**:
- **MultiSoundChanger.xcodeproj/project.pbxproj** (MODIFIED)
- Removed `EXCLUDED_ARCHS = arm64` from Debug configuration
- Removed `EXCLUDED_ARCHS = arm64` from Release configuration
- Removed all references to OSD.framework:
- PBXBuildFile section
- PBXFileReference section
- PBXFrameworksBuildPhase section
- Frameworks group
- Added NativeOSDManager.swift to project:
- PBXBuildFile section
- PBXFileReference section
- Frameworks group
- Sources build phase

### 3. Dependencies Status

**MediaKeyTap**: The app uses a custom fork of MediaKeyTap from `https://github.com/the0neyouseek/MediaKeyTap.git`. This is a Swift-based framework and should support ARM64, but needs verification during build.

**SwiftLint**: Standard linting tool with ARM64 support.

## Architecture Support

After these changes, the app should build as a **Universal Binary** supporting:
- **x86_64** (Intel Macs)
- **ARM64** (Apple Silicon - M1, M2, M3, M4)

## Building the App

### Prerequisites
- macOS 11.0 or later (for ARM64 support)
- Xcode 12.0 or later
- CocoaPods

### Build Instructions

1. **Install Dependencies**
```bash
cd /path/to/MultiSoundChangerARM
pod install
```

2. **Open Workspace**
```bash
open MultiSoundChanger.xcworkspace
```
⚠️ **Important**: Open the `.xcworkspace` file, not the `.xcodeproj` file!

3. **Build**
- Select your target architecture in Xcode (or leave as "Any Mac" for universal binary)
- Product → Build (⌘B)

4. **Run**
- Product → Run (⌘R)

### Command Line Build

For x86_64:
```bash
xcodebuild -workspace MultiSoundChanger.xcworkspace \
-scheme MultiSoundChanger \
-configuration Release \
-arch x86_64 \
clean build
```

For ARM64:
```bash
xcodebuild -workspace MultiSoundChanger.xcworkspace \
-scheme MultiSoundChanger \
-configuration Release \
-arch arm64 \
clean build
```

For Universal Binary:
```bash
xcodebuild -workspace MultiSoundChanger.xcworkspace \
-scheme MultiSoundChanger \
-configuration Release \
-arch "x86_64 arm64" \
clean build
```

## Testing Checklist

After building, verify the following functionality on ARM64:

- [ ] App launches successfully
- [ ] Menu bar icon appears and is responsive
- [ ] Audio device enumeration works
- [ ] Volume control works for standard audio devices
- [ ] Volume control works for aggregate audio devices
- [ ] Media keys (volume up/down/mute) are intercepted correctly
- [ ] **OSD (On-Screen Display) volume indicator appears when volume changes**
- [ ] OSD shows correct speaker icon
- [ ] OSD shows muted speaker icon when muted
- [ ] OSD displays on correct screen in multi-monitor setup
- [ ] OSD chiclets (volume bars) reflect correct volume level
- [ ] No crashes or unexpected behavior
- [ ] Accessibility permissions prompt works correctly

## OSD Implementation Details

The new native OSD implementation provides:

### Features
- Custom NSWindow-based overlay
- Centered on the display where mouse cursor is located
- Semi-transparent black background
- White speaker icon (or muted icon with red X)
- Sound waves animation for non-muted state
- Volume level chiclets (bars)
- Smooth fade-in/fade-out animations
- Appears above all windows (`.statusBar` level)
- Ignores mouse events
- Multi-monitor support

### Visual Appearance
```
┌─────────────────────┐
│ │
│ 🔊 │ ← Speaker icon (or 🔇 if muted)
│ │
│ ████████▒▒▒▒▒▒▒▒ │ ← Volume chiclets
│ │
└─────────────────────┘
```

### Behavior
- Displays for 1 second before fading out
- Shows on the screen containing the mouse cursor
- Animates in (0.2s) and out (0.3s)
- Updates in real-time as volume changes

## Compatibility Notes

- **Minimum macOS Version**: 11.0 (Big Sur) — raised from 10.10 to enable ARM64 support
- **Recommended macOS Version**: 11.0 or later
- **Code Signing**: Manual with ad-hoc identity (`CODE_SIGN_IDENTITY = "-"`) for local development. Hardened Runtime enabled (`ENABLE_HARDENED_RUNTIME = YES`) with an empty `MultiSoundChanger/Other/MultiSoundChanger.entitlements` file wired via `CODE_SIGN_ENTITLEMENTS`. No Hardened-Runtime exceptions are needed — the app has no JIT, no dylib injection, no outgoing Apple Events. Switch `CODE_SIGN_IDENTITY` to a Developer ID Application certificate and submit to `notarytool` for distribution.
- **Deployment**: Works on both Intel and Apple Silicon Macs

## Known Issues / Future Improvements

1. **OSD Visual Design**: The native OSD is a simplified version of the system OSD. Consider:
- Matching the exact system OSD appearance
- Adding support for other OSD types (brightness, keyboard backlight, etc.)

2. **MediaKeyTap**: Verify the custom fork supports ARM64. If issues arise:
- Update to the latest version
- Switch to the main MediaKeyTap repository
- Or fork and update the dependency

3. **Code Signing**: For distribution, proper code signing should be configured

## Rollback Instructions

The ARM64 migration began with commit `c767aba` ("Rebuild app for ARM64
(Apple Silicon) support"). To revert fully to the x86_64-only 1.0.1
release:

```bash
git checkout 135f003 # [Release] 1.0.1 — last pre-ARM64 tagged commit
```

Or, to keep your branch but reset to the pre-migration parent:

```bash
git reset --hard c767aba^
```

Note: the on-disk `OSD.framework/` directory has already been removed
from this branch (it was a leftover from the pre-migration state and
was no longer referenced by `project.pbxproj`, the bridging header, or
any source file). Rolling back to a pre-deletion commit will restore it
alongside the rest of the tree.

## Questions or Issues?

If you encounter any issues with the ARM64 build:

1. Ensure you're using Xcode 12.0 or later
2. Verify CocoaPods installed all dependencies correctly
3. Check that you opened `.xcworkspace` not `.xcodeproj`
4. Clean build folder: Product → Clean Build Folder (⌘⇧K)
5. Try removing and reinstalling Pods:
```bash
rm -rf Pods Podfile.lock
pod install
```

## Credits

- **Original App**: MultiSoundChanger by Dmitry Medyuho
- **ARM64 Migration**: Converted from x86_64 to universal binary (x86_64 + ARM64)
- **Native OSD Implementation**: Custom Swift/Cocoa implementation replacing OSD.framework
96 changes: 96 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

For deep-dive subsystem docs, see `docs/`:

- **[`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md)** — subsystem-by-subsystem design: audio HAL wrapper, aggregate device model, debounce pipeline, OSD window, media-key flow, menu UI, logger. **Read this before non-trivial changes.**
- **[`docs/BUILD_AND_SIGNING.md`](docs/BUILD_AND_SIGNING.md)** — Xcode workspace, CocoaPods pinning, Hardened Runtime, entitlements file, distribution posture.
- **[`docs/AUDIT_NOTES.md`](docs/AUDIT_NOTES.md)** — things audit agents keep misflagging as bugs but aren't. **Read this before proposing "fixes" to flagged patterns** — the list exists because each item was raised, triaged, and documented as intentional.

## Project

MultiSoundChanger is a macOS menu-bar utility that adjusts output volume — including on **aggregate devices**, which macOS's own volume controller cannot handle. This repo is the ARM64-migrated fork (universal binary; Intel + Apple Silicon). Deployment target: macOS 11.0. See `ARM64_MIGRATION.md` for the full migration history.

## Build / Run

**Always open `MultiSoundChanger.xcworkspace`, not `MultiSoundChanger.xcodeproj`.** The app depends on CocoaPods (SwiftLint, MediaKeyTap), so building through the `.xcodeproj` directly will fail.

```bash
pod install # first-time or after Podfile changes
open MultiSoundChanger.xcworkspace # then ⌘B / ⌘R in Xcode
```

Command-line universal build:

```bash
xcodebuild -workspace MultiSoundChanger.xcworkspace \
-scheme MultiSoundChanger -configuration Release \
-arch "x86_64 arm64" clean build
```

On this user's machine `xcodebuild` is broken (unrelated `IDESimulatorFoundation` plugin issue). For automated verification use Swift typecheck instead — see `docs/BUILD_AND_SIGNING.md`.

There is no test target. SwiftLint runs as a build phase (`.swiftlint.yml` uses `whitelist_rules`, not default rules; `Pods/` excluded).

## Architecture at a glance

Status-bar-only Cocoa app (no main window). Entry point → dependency graph:

```
AppDelegate
└── ApplicationControllerImp (owns the three managers; is AudioManagerDelegate + MediaManagerDelegate)
├── AudioManagerImpl — selected-device state, debounced volume writes, mute + aggregate fan-out
│ └── AudioImpl — CoreAudio HAL wrapper (AudioObjectGet/SetPropertyData with OSStatus checking)
├── MediaManagerImpl — MediaKeyTap delegate + OSD trigger + Accessibility permission flow
│ └── OSDManager (inline @objc singleton in NativeOSDManager.swift)
└── StatusBarControllerImpl — NSStatusItem menu, device list (sorted), NSMenuDelegate, VolumeViewController host
└── VolumeViewController (loaded from Volume.storyboard)
```

Every class is defined as `protocol Foo` + `final class FooImpl` and injected by its parent. Stick to that pattern when adding components.

### Key invariants (one-liners — expanded in `docs/ARCHITECTURE.md`)

- **Aggregate devices**: writes fan out to every sub-device; reads return `.max()` from the *first* output sub-device. Intentionally asymmetric. Preserve.
- **`AudioManagerImpl.setSelectedDeviceVolume`** is debounced (33 ms trailing edge). The getter returns `pendingTargetVolume` when set so rapid-repeat quantization sees user intent, not stale HAL state.
- **OSDManager** is a thread-safe `static let` singleton. `showImage` branches on `Thread.isMainThread` to avoid an unnecessary runloop hop from the hotkey path.
- **Volume quantization**: `Constants.chicletsCount = 16` steps so hardware keys align with the OSD chiclets.
- **MediaKeyTap** is pinned by commit hash in `Podfile` (supply-chain fix — never switch back to a floating branch ref).
- **Log writes** use raw POSIX `open(O_NOFOLLOW, 0o600)` — symlink defense against `~/Library/Caches/<bundle>/app.log` being redirected at a sensitive file.
- **Hardened Runtime on** (both Debug + Release). Entitlements declare only `com.apple.security.cs.disable-library-validation` — required because CocoaPods embeds MediaKeyTap as a dynamic framework and our ad-hoc signing has no team identity for library validation to match. See `docs/BUILD_AND_SIGNING.md`.
- **Storyboard identifiers must match class names exactly** (`Stories.swift` instantiates by `String(describing:)`).

## Workflow

- **Active branch**: `claude/rebuild-x86-app-011CV4gXVczxQsxNuHeA9X9o` (targets PR #39 on `rlxone/MultiSoundChanger`).
- **After every round of changes, commit and push to that branch.** Don't batch rounds locally. Push target is `origin` (`solartrans/MultiSoundChangerARM`); the PR against upstream updates automatically.
- **Credentials**: a GitHub fine-grained PAT is stored in the macOS Keychain under `git credential-osxkeychain`. `git push` works without any additional setup. If a push ever fails with "Authentication failed", the token likely expired or lost `Contents: Read and write` permission — tell the user, don't try to work around it.
- If SSH/git tooling ever breaks in the sandboxed shell for unrelated reasons, ask the user to run `! git push origin claude/rebuild-x86-app-011CV4gXVczxQsxNuHeA9X9o` as a fallback rather than leaving commits unpushed.

## Conventions

- Swift-only source lives under `MultiSoundChanger/Sources/`; non-code assets and `Constants.swift` under `MultiSoundChanger/Other/`.
- UI is storyboard-based (`Volume.storyboard`, `Main.storyboard`). View controllers are loaded via the `Stories` enum helper, which instantiates by `String(describing: classType)` — storyboard identifiers **must match the class name exactly**.
- All user-visible strings go through `Strings.*` (see `Other/Localization/`). Log/debug strings go through `Constants.InnerMessages`.
- Logging: `Logger.debug / info / warning / error`. Writes to `~/Library/Caches/<bundleID>/app.log` via a serial background queue + raw POSIX `open(O_NOFOLLOW, 0o600)`; does NOT block main. See `docs/ARCHITECTURE.md` for the rationale.
- Device names are sanitized (newlines/tabs stripped) before logging — see `AudioManagerImpl.sanitizedForLog`. Audio HAL may return any string a plugin chose.
- `fileprivate` is preferred over `private` for same-file extension access (e.g., the listener methods extension on `AudioImpl`). Avoid leaking internal details past the file.

## What NOT to do

Collected from a long audit-fix loop; each item has been flagged multiple times by different agents and each is intentional:

- Don't "fix" `AudioManagerImpl.readDeviceVolumeFromHAL` returning `nil` on an aggregate with no output sub-device — it's documented design.
- Don't rewrite `StatusBarController`'s `100 / 3 * 2` threshold — it IS the correct two-thirds boundary (`(100/3)*2 = 66.66…`), not a precedence bug.
- Don't add an explicit lock around `Logger.isLogFileRemoved` — all access is already serialized via the `fileWriteQueue`.
- Don't switch OSD positioning from `screen.visibleFrame.midY + height/4` to lower-half; the upper-center placement is intentional.
- Don't revert the `@NSApplicationMain` to `@main` — the app's storyboard entry requires the former on this target configuration.
- Don't re-add `Runner.shell` and the `open -b` path for System Settings; the `x-apple.systempreferences:` URL via `NSWorkspace` replaced it for both subprocess-surface reduction and macOS Ventura+ compatibility.
- Full list: **[`docs/AUDIT_NOTES.md`](docs/AUDIT_NOTES.md)**.

## When in doubt

- Need to touch the audio path? Read `docs/ARCHITECTURE.md` § Audio subsystem first.
- Need to change build / signing / pods? Read `docs/BUILD_AND_SIGNING.md`.
- Have a finding from a static analyzer or audit agent and unsure if it's real? Check `docs/AUDIT_NOTES.md` — if it's listed, it's already been triaged and it's not a bug.
Loading