Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
f47ca29
chore: drop Windows/Linux builds, focus on the native macOS app
flotttt Sep 10, 2026
663ff70
Merge pull request #1 from flotttt/chore/macos-only
flotttt Sep 10, 2026
4047d24
feat(macos): French localization (fr + en fallback)
flotttt Sep 10, 2026
b646cf7
Merge pull request #2 from flotttt/feat/trad
flotttt Sep 10, 2026
1da9d58
docs: menu bar app design spec
flotttt Sep 10, 2026
9e8a1d1
docs: menu bar app implementation plan
flotttt Sep 10, 2026
cdae371
feat(macos): menu bar agent app built with make (drop Xcode project a…
flotttt Sep 10, 2026
02ac559
test: reconnect/poll-guard/throttle logic, localization check, make test
flotttt Sep 10, 2026
886ba74
fix(xm6): read NC/ASM state on channel 0x19, decode 10-band equalizer…
flotttt Sep 10, 2026
b5e6228
fix(xm6): read NC/ASM state on channel 0x19, decode 10-band equalizer…
flotttt Sep 10, 2026
bdb2881
refactor(macos): menu-oriented HeadphonesModel (connection state, com…
flotttt Sep 10, 2026
f19743c
fix(bt): wait for the real ACK, ACK MDR_NO2 frames, keep 1-bit sequen…
flotttt Sep 10, 2026
06e6325
feat(macos): SwiftUI menu rows (header, level slider, switches, equal…
flotttt Sep 10, 2026
e0beefb
feat(macos): full headphones menu with per-mode status icon
flotttt Sep 10, 2026
cda1c3e
fix(build): make run launches via open --args; sandboxed app logs to …
flotttt Sep 11, 2026
cb877f7
fix(macos): menu layout (widths, insets, short values), EQ row height…
flotttt Sep 11, 2026
fd905d6
feat(macos): auto-connect, auto-reconnect and launch at login (menu o…
flotttt Sep 11, 2026
00fc2f7
add new shared
flotttt Sep 11, 2026
a32b3dd
Merge pull request #3 from flotttt/feat/settings
flotttt Sep 11, 2026
2aceb82
docs(readme): XM6 verified, drop release/stars badges, build-from-sou…
flotttt Sep 11, 2026
23ec865
add
flotttt Sep 11, 2026
d5680b4
Merge pull request #4 from flotttt/feat/parti8
flotttt Sep 11, 2026
b36aa29
docs: notch + local Spotify design spec (step 1)
flotttt Sep 11, 2026
61cf297
docs: notch step 1 implementation plan
flotttt Sep 11, 2026
5840cef
feat(notch): notch geometry and resting-state logic
flotttt Sep 11, 2026
2d07acc
feat(music): pure playback logic (now playing, clock, Spotify parsing)
flotttt Sep 17, 2026
eed95e2
feat(music): local Spotify source, music controller and Show Notch op…
flotttt Sep 17, 2026
d6613e4
feat(notch): notch panel with resting states and hover to open
flotttt Sep 17, 2026
e7226e5
feat(notch): headphones tab
flotttt Sep 17, 2026
2e18460
feat(notch): music player and headphones
flotttt Sep 17, 2026
e6d70d4
feat(notch): resting music control, notch
flotttt Sep 17, 2026
1305ccc
Merge pull request #5 from flotttt/feat/notch
flotttt Sep 17, 2026
4f758f1
chore: rename to SonyNotch, automatic releases, README rewrite
flotttt Sep 17, 2026
e9ed5b4
Merge pull request #6 from flotttt/feat/release
flotttt Sep 17, 2026
3c43394
add realese
flotttt Sep 17, 2026
8da50bc
docs: user-first README
flotttt Sep 17, 2026
ce02468
fix(notch): centre the playback controls, remove shuffle
flotttt Sep 17, 2026
707f140
Merge pull request #7 from flotttt/docs/readme-users
flotttt Sep 17, 2026
6bffc79
feat: update notice in the menu, Homebrew cask updated on release
flotttt Sep 17, 2026
6882830
add
flotttt Sep 17, 2026
aef7a1a
Merge pull request #8 from flotttt/feat/updates"
flotttt Sep 17, 2026
f38d74c
feat: one-click update from the menu, launch at login in the main menu
flotttt Sep 17, 2026
161a1b0
Merge pull request #9 from flotttt/feat/one-click-update
flotttt Sep 17, 2026
23bfee5
ci: build releases created from the GitHub UI, with or without a v pr…
flotttt Sep 17, 2026
d05afba
Merge pull request #10 from flotttt/fix/release-from-github-ui
flotttt Sep 17, 2026
115be2b
chore: remove inherited funding link and internal planning docs
flotttt Sep 17, 2026
f02d217
Merge pull request #11 from flotttt/chore/cleanup
flotttt Sep 17, 2026
f0487a1
feat: sign releases with a stable certificate so macOS keeps permissi…
flotttt Sep 17, 2026
e521769
Merge pull request #12 from flotttt/feat/stable-signature
flotttt Sep 17, 2026
e23222d
ci: find the signing certificate by SHA-1 in the build keychain
flotttt Sep 17, 2026
98d69cd
Merge pull request #13 from flotttt/fix/ci-signing-identity
flotttt Sep 17, 2026
931afc0
feat(notch): flip the artwork on track change, backwards after previous
flotttt Sep 17, 2026
9a2f8c1
feat(notch): swipe to change track and scroll for volume, with settings
flotttt Sep 17, 2026
d896301
feat(notch): trackpad gestures and haptic feedback, each configurable…
flotttt Sep 17, 2026
4828d06
feat(notch): artwork-coloured glow behind the player, with adjustable…
flotttt Sep 17, 2026
3d3b0b3
feat(notch): progress ring around the closed notch artwork, fix subme…
flotttt Sep 17, 2026
1d83a24
feat(notch): headphones battery ring on the closed notch, shown while…
flotttt Sep 17, 2026
0a88b72
feat(notch): click the artwork to bring Spotify to the front
flotttt Sep 17, 2026
7690c27
Merge pull request #14 from flotttt/feat/artwork-flip
flotttt Sep 17, 2026
9d6b567
feat: setup assistant for Bluetooth, headphones, Spotify control and …
flotttt Sep 18, 2026
60c5a36
feat: setup window for Bluetooth, headphones, Spotify control and lau…
flotttt Sep 18, 2026
4703d3c
Merge pull request #15 from flotttt/feat/setup-assistant
flotttt Sep 18, 2026
15bd89b
feat: choose notch only, headphones only or both in the setup window
flotttt Sep 18, 2026
d8cf729
Merge pull request #16 from flotttt/feat/usage-modes
flotttt Sep 18, 2026
169dac8
feat(notch): Apple Music, and Deezer, YouTube Music or the browser th…
flotttt Sep 18, 2026
e095d90
Merge pull request #17 from flotttt/feat/music-players
flotttt Sep 18, 2026
41e6ff1
fix(notch): keep the playback controls centred in notch-only mode
flotttt Sep 18, 2026
c564564
Merge pull request #18 from flotttt/fix/notch-only-centering
flotttt Sep 18, 2026
c4fa128
feat: rename the app Overhead, with a new icon
flotttt Sep 18, 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
2 changes: 0 additions & 2 deletions .github/FUNDING.yml

This file was deleted.

Binary file added .github/logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
21 changes: 21 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
name: macOS

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
build:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- name: Test
run: make test
- name: Build (release, universal)
run: make release
- uses: actions/upload-artifact@v4
with:
name: Overhead-macOS
path: build/Overhead.zip
72 changes: 0 additions & 72 deletions .github/workflows/cmake.yml

This file was deleted.

132 changes: 132 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
name: Release

# Publishing a release (on GitHub: Releases › Draft a new release › new tag like 1.2.0 or v1.2.0 › Publish, or
# `git tag v1.2.0 && git push origin v1.2.0`) creates a version tag, which runs this workflow. The app gets its
# version from the tag, is tested and built for Apple Silicon and Intel, and Overhead.zip is attached to the
# release with its SHA-256, along with SonyNotch.zip: the same app for the updater of SonyNotch 1.4 and older (see
# the Makefile). The Homebrew cask in flotttt/homebrew-tap is then pointed at it (needs the HOMEBREW_TAP_TOKEN
# secret, skipped without it).
#
# To build a release whose tag didn't run the workflow: Actions › Release › Run workflow, with that tag.
#
# The app is signed with the "SonyNotch Code Signing" certificate (SIGNING_CERT_P12 and SIGNING_CERT_PASSWORD
# secrets), the same for every release, so macOS keeps its Bluetooth and music app permissions across updates. It
# keeps the app's former name: a new certificate would cost users those permissions.

on:
push:
tags: ["v[0-9]+.[0-9]+.[0-9]+", "[0-9]+.[0-9]+.[0-9]+"]
workflow_dispatch:
inputs:
tag:
description: "Tag of the release to build (e.g. 1.2.0 or v1.2.0)"
required: true

permissions:
contents: write

env:
TAG: ${{ inputs.tag || github.ref_name }}

jobs:
release:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ env.TAG }}

- name: Set the app version from the tag
run: |
VERSION="${TAG#v}"
if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "The tag $TAG isn't a version like 1.2.0 or v1.2.0."
exit 1
fi
echo "VERSION=$VERSION" >> "$GITHUB_ENV"
/usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $VERSION" Client/macos/info.plist
/usr/libexec/PlistBuddy -c "Set :CFBundleVersion ${{ github.run_number }}" Client/macos/info.plist

- name: Test
run: make test

- name: Import the signing certificate
env:
SIGNING_CERT_P12: ${{ secrets.SIGNING_CERT_P12 }}
SIGNING_CERT_PASSWORD: ${{ secrets.SIGNING_CERT_PASSWORD }}
run: |
if [ -z "$SIGNING_CERT_P12" ]; then
echo "The SIGNING_CERT_P12 secret is missing: a release signed otherwise would lose users' permissions."
exit 1
fi
KEYCHAIN="$RUNNER_TEMP/signing.keychain-db"
KEYCHAIN_PASSWORD=$(openssl rand -hex 16)
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN"
security set-keychain-settings -lut 3600 "$KEYCHAIN"
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN"
echo "$SIGNING_CERT_P12" | base64 -D > "$RUNNER_TEMP/signing.p12"
security import "$RUNNER_TEMP/signing.p12" -k "$KEYCHAIN" -P "$SIGNING_CERT_PASSWORD" -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN" > /dev/null
rm "$RUNNER_TEMP/signing.p12"
# codesign only looks for identities in the keychain search list, whatever --keychain says.
security list-keychains -d user -s "$KEYCHAIN" $(security list-keychains -d user | tr -d '"')
security find-identity -p codesigning "$KEYCHAIN"
# The certificate's SHA-1 names it without a lookup by common name.
HASH=$(security find-certificate -c "SonyNotch Code Signing" -Z "$KEYCHAIN" | awk '/SHA-1 hash:/ { print $NF }')
if [ -z "$HASH" ]; then
echo "The signing certificate isn't in the keychain."
exit 1
fi
echo "SIGN_KEYCHAIN=$KEYCHAIN" >> "$GITHUB_ENV"
echo "SIGN_IDENTITY=$HASH" >> "$GITHUB_ENV"

- name: Build (release, universal)
run: make release

- name: Check the signature
run: codesign -dr - build/Overhead.app 2>&1 | grep -q 'certificate root'

- name: Checksum
run: |
cd build
shasum -a 256 Overhead.zip | tee Overhead.zip.sha256
shasum -a 256 SonyNotch.zip | tee SonyNotch.zip.sha256

# A release already made on GitHub for this tag (title and notes written there) only gets the files.
- name: Publish the release
env:
GH_TOKEN: ${{ github.token }}
run: |
FILES="build/Overhead.zip build/Overhead.zip.sha256 build/SonyNotch.zip build/SonyNotch.zip.sha256"
if gh release view "$TAG" >/dev/null 2>&1; then
gh release upload "$TAG" $FILES --clobber
else
gh release create "$TAG" $FILES \
--title "Overhead $VERSION" \
--generate-notes
fi

# The download URL uses the exact tag, since tags may or may not start with "v".
- name: Update the Homebrew cask
env:
GH_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
run: |
if [ -z "$GH_TOKEN" ]; then
echo "HOMEBREW_TAP_TOKEN isn't set, the Homebrew cask isn't updated."
exit 0
fi
FILE=Casks/overhead.rb
SHA256=$(cut -d ' ' -f 1 build/Overhead.zip.sha256)
URL="https://github.com/flotttt/Overhead/releases/download/$TAG/Overhead.zip"
gh api "repos/flotttt/homebrew-tap/contents/$FILE" > current.json
jq -r .content current.json | base64 -D > overhead.rb
sed -i '' -E \
-e "s|^ version \".*\"| version \"$VERSION\"|" \
-e "s|^ sha256 \".*\"| sha256 \"$SHA256\"|" \
-e "s|^ url \".*\"| url \"$URL\"|" \
overhead.rb
gh api -X PUT "repos/flotttt/homebrew-tap/contents/$FILE" \
-f message="overhead $VERSION" \
-f content="$(base64 -i overhead.rb)" \
-f sha="$(jq -r .sha current.json)" > /dev/null
echo "Homebrew cask updated to $VERSION"
55 changes: 0 additions & 55 deletions .github/workflows/xcodebuild.yml

This file was deleted.

5 changes: 2 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -523,10 +523,9 @@ MigrationBackup/
.ionide/

# End of https://www.toptal.com/developers/gitignore/api/c++,visualstudio,python
Client/imgui.ini

# vscode
.vscode/

# linux build
Client/linux/sonyheadphonesclient.build/
# local working notes
.superpowers/
3 changes: 0 additions & 3 deletions .gitmodules

This file was deleted.

30 changes: 23 additions & 7 deletions Client/BluetoothWrapper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ BluetoothWrapper& BluetoothWrapper::operator=(BluetoothWrapper&& other) noexcept
int BluetoothWrapper::sendCommand(const std::vector<char>& bytes)
{
std::lock_guard guard(this->_connectorMtx);
auto data = CommandSerializer::packageDataForBt(bytes, DATA_TYPE::DATA_MDR, this->_seqNumber++);
// Use the current seq, don't advance it here: the next seq comes from the device's ACK
// (_waitForAck), so if this frame goes unanswered its seq is simply reused on the next call.
auto data = CommandSerializer::packageDataForBt(bytes, DATA_TYPE::DATA_MDR, this->_seqNumber);
auto bytesSent = this->_connector->send(data.data(), data.size());

this->_waitForAck();
Expand Down Expand Up @@ -68,7 +70,9 @@ SonyProtocolVersion BluetoothWrapper::getProtocolVersion() noexcept
Buffer BluetoothWrapper::sendCommandAndReadResponse(const std::vector<char>& bytes, unsigned char retCommandId, int retSubType)
{
std::lock_guard guard(this->_connectorMtx);
auto data = CommandSerializer::packageDataForBt(bytes, DATA_TYPE::DATA_MDR, this->_seqNumber++);
// Use the current seq, don't advance it here: the next seq comes from the device's ACK
// (handled below), so if this frame goes unanswered its seq is simply reused on the next call.
auto data = CommandSerializer::packageDataForBt(bytes, DATA_TYPE::DATA_MDR, this->_seqNumber);
this->_connector->send(data.data(), data.size());

// The device replies with an ACK and then the RET frame; unrelated notifications may interleave.
Expand All @@ -92,8 +96,19 @@ Buffer BluetoothWrapper::sendCommandAndReadResponse(const std::vector<char>& byt

void BluetoothWrapper::_waitForAck()
{
auto msg = this->_readMessage();
this->_seqNumber = msg.seqNumber;
// The device may send its own DATA_MDR notifications (already ACKed inside _readMessage) before
// the ACK for our frame arrives; skip those and keep reading until we see the actual ACK, so
// _seqNumber never gets set from an unrelated message's seq.
for (int i = 0; i < 16; i++)
{
auto msg = this->_readMessage();
if (msg.dataType == DATA_TYPE::ACK)
{
this->_seqNumber = msg.seqNumber;
return;
}
}
throw RecoverableException("No ACK received from device", true);
}

CommandSerializer::Message BluetoothWrapper::_readMessage()
Expand Down Expand Up @@ -150,9 +165,10 @@ CommandSerializer::Message BluetoothWrapper::_readMessage()

auto msg = CommandSerializer::unpackBtMessage(msgBytes);

// The device retransmits any DATA_MDR frame it sends until the host ACKs it. The ack carries the
// toggled 1-bit sequence number (1 - deviceSeq), matching the device's own ack-to-us behaviour.
if (msg.dataType == DATA_TYPE::DATA_MDR)
// The device retransmits any DATA_MDR or DATA_MDR_NO2 frame it sends until the host ACKs it (e.g.
// slider drags are echoed as DATA_MDR_NO2). The ack carries the toggled 1-bit sequence number
// (1 - deviceSeq), matching the device's own ack-to-us behaviour.
if (msg.dataType == DATA_TYPE::DATA_MDR || msg.dataType == DATA_TYPE::DATA_MDR_NO2)
{
auto ack = CommandSerializer::packageDataForBt({}, DATA_TYPE::ACK, (unsigned char)(1 - msg.seqNumber));
this->_connector->send(ack.data(), ack.size());
Expand Down
Loading