diff --git a/README.md b/README.md index d198073..0ace78d 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,9 @@ # ALN - AirPods like Normal (Linux Only) -# Get Started! -## 1. Install the required packages +## Currently supported device(s) +- AirPods Pro 2 + +### 1. Install the required packages ```bash sudo apt install python3 python3-pip @@ -14,16 +16,16 @@ If you want to run it as a daemon (Refer to the [Daemon Version](#as-a-daemon-us pip3 install python-daemon ``` -## 2. Clone the repository +### 2. Clone the repository ```bash git clone https://github.com/kavishdevar/aln.git cd aln ``` -## 3. Preprare +### 3. Preprare Pair your AirPods with your machine before running this script! -:warning: **Note:** DO NOT FORGET TO EDIT THE `AIRPODS_MAC` VARIABLE IN `main.py`/`standalone.py` WITH YOUR AIRPODS MAC ADDRESS! +> **Note:** DO NOT FORGET TO EDIT THE `AIRPODS_MAC` VARIABLE IN EXAMPLE SCRIPTS WITH YOUR AIRPODS MAC ADDRESS BEFORE RUNNING THEM! # Versions @@ -79,7 +81,7 @@ python3 examples/daemon/ear-detection.py ![Tray Icon Hover Screenshot](imgs/tray-icon-hover.png) ![Tray Icon Menu Screenshot](imgs/tray-icon-menu.png) -This script is a simple tray icon that shows the battery percentage and set ANC modes. +This script is a simple tray icon that shows the battery percentage and set ANC modes. It can also control the media with the in-ear status. > Note: This script uses QT. ```bash @@ -94,4 +96,4 @@ python3 examples/daemon/tray.py ```bash python3 examples/standalone.py -``` \ No newline at end of file +``` diff --git a/examples/daemon/tray.py b/examples/daemon/tray.py index eb4eebe..dc8e2eb 100644 --- a/examples/daemon/tray.py +++ b/examples/daemon/tray.py @@ -7,6 +7,9 @@ from PyQt5.QtWidgets import QApplication, QSystemTrayIcon, QMenu, QAction, QMess from PyQt5.QtGui import QIcon from PyQt5.QtCore import QObject, pyqtSignal import logging +import subprocess +import time +import os SOCKET_PATH = "/tmp/airpods_daemon.sock" @@ -20,9 +23,104 @@ battery_status = { # Define a lock battery_status_lock = threading.Lock() +class MediaController: + def __init__(self): + self.earStatus = "Both out" + self.wasMusicPlayingInSingle = False + self.wasMusicPlayingInBoth = False + self.firstEarOutTime = 0 + self.stop_thread_event = threading.Event() + + def playMusic(self): + logging.info("Playing music") + subprocess.call(("playerctl", "play", "--ignore-player", "OnePlus_7")) + + def pauseMusic(self): + logging.info("Pausing music") + subprocess.call(("playerctl", "pause", "--ignore-player", "OnePlus_7")) + + def isPlaying(self): + return subprocess.check_output(["playerctl", "status", "--player", "spotify"]).decode("utf-8").strip() == "Playing" + + def handlePlayPause(self, data): + primary_status = data[0] + secondary_status = data[1] + + logging.debug(f"Handling play/pause with data: {data}, previousStatus: {self.earStatus}, wasMusicPlaying: {self.wasMusicPlayingInSingle or self.wasMusicPlayingInBoth}") + + def delayed_action(s): + if not self.stop_thread_event.is_set(): + if self.wasMusicPlayingInSingle: + self.playMusic() + self.wasMusicPlayingInBoth = False + elif self.wasMusicPlayingInBoth or s: + self.wasMusicPlayingInBoth = True + self.wasMusicPlayingInSingle = False + + if primary_status and secondary_status: + if self.earStatus != "Both out": + s = self.isPlaying() + if s: + self.pauseMusic() + os.system("pactl set-card-profile bluez_card.28_2D_7F_C2_05_5B off") + logging.info("Setting profile to off") + if self.earStatus == "Only one in": + if self.firstEarOutTime != 0 and time.time() - self.firstEarOutTime < 0.3: + self.wasMusicPlayingInSingle = True + self.wasMusicPlayingInBoth = True + self.stop_thread_event.set() + else: + if s: + self.wasMusicPlayingInSingle = True + else: + self.wasMusicPlayingInSingle = False + elif self.earStatus == "Both in": + s = self.isPlaying() + if s: + self.wasMusicPlayingInBoth = True + self.wasMusicPlayingInSingle = False + else: + self.wasMusicPlayingInSingle = False + self.earStatus = "Both out" + return "Both out" + elif not primary_status and not secondary_status: + if self.earStatus != "Both in": + if self.earStatus == "Both out": + os.system("pactl set-card-profile bluez_card.28_2D_7F_C2_05_5B a2dp-sink") + logging.info("Setting profile to a2dp-sink") + elif self.earStatus == "Only one in": + self.stop_thread_event.set() + s = self.isPlaying() + if s: + self.wasMusicPlayingInBoth = True + if self.wasMusicPlayingInSingle or self.wasMusicPlayingInBoth: + self.playMusic() + self.wasMusicPlayingInBoth = True + self.wasMusicPlayingInSingle = False + self.earStatus = "Both in" + return "Both in" + elif (primary_status and not secondary_status) or (not primary_status and secondary_status): + if self.earStatus != "Only one in": + self.stop_thread_event.clear() + s = self.isPlaying() + if s: + self.pauseMusic() + delayed_thread = threading.Timer(0.3, delayed_action, args=[s]) + delayed_thread.start() + self.firstEarOutTime = time.time() + if self.earStatus == "Both out": + os.system("pactl set-card-profile bluez_card.28_2D_7F_C2_05_5B a2dp-sink") + logging.info("Setting profile to a2dp-sink") + self.earStatus = "Only one in" + return "Only one in" + class BatteryStatusUpdater(QObject): battery_status_updated = pyqtSignal() + def __init__(self): + super().__init__() + self.media_controller = MediaController() + def listen_for_battery_updates(self): global battery_status with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as client: @@ -33,10 +131,11 @@ class BatteryStatusUpdater(QObject): try: response = json.loads(data.decode('utf-8')) if response["type"] == "battery": - print(response) with battery_status_lock: battery_status = response self.battery_status_updated.emit() + elif response["type"] == "ear_detection": + self.media_controller.handlePlayPause([response['primary'], response['secondary']]) except json.JSONDecodeError as e: logging.warning(f"Error deserializing data: {e}") except KeyError as e: @@ -51,7 +150,6 @@ def get_battery_status(): case = battery_status["CASE"] return f"Left: {left['level']}% - {left['status'].title().replace('_', ' ')} | Right: {right['level']}% - {right['status'].title().replace('_', ' ')} | Case: {case['level']}% - {case['status'].title().replace('_', ' ')}" - from aln import enums def set_anc_mode(mode): with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as client: @@ -69,7 +167,6 @@ def set_anc_mode(mode): response = client.recv(1024) return json.loads(response.decode()) - def control_anc(action): response = set_anc_mode(action) logging.info(f"ANC action: {action}, Response: {response}")