AirPodsDesktop

AirPodsDesktop 是一个开源的桌面用户体验增强程序,专门为 Windows 平台上的 AirPods 用户设计。通过蓝牙低功耗协议,该程序能够实时显示 AirPods 的电池状态、充电状态和佩戴状态,并提供自动媒体控制和低延迟音频模式等功能。

✨ 功能特性

  • 🔋 电池信息显示:实时监控并显示 AirPods 左右耳机及充电盒的电池电量,支持低电量提醒。
  • 👂 自动人耳检测:检测 AirPods 的佩戴状态,当耳机放入耳朵时,自动播放媒体;取出时自动暂停。
  • 🚀 低音频延迟模式:通过后台播放静音音频流,修复 AirPods 在 Windows 上播放短音频时可能出现的延迟或卡顿问题(可能增加电池消耗)。
  • 🌈 精美的动画:提供与 AirPods 型号匹配的、流畅的开合动画,提升视觉体验。
  • 🌐 多语言支持:支持英语、简体中文、繁体中文、德语、法语、日语、韩语、俄语等多种语言,并提供了完整的翻译指南。
  • ⚙️ 可自定义设置:用户可配置开机自启、任务栏电池显示方式、接收信号强度范围等。
  • 🔄 自动更新:支持检查并自动下载安装新版本。

🛠️ 安装指南

系统要求

  • 操作系统: Windows
  • 构建工具: CMake (>= v3.20), Visual Studio 2019
  • 依赖管理: vcpkg
  • Qt框架: Qt 5.15.2 (MSVC 2019 32-bit 组件)
  • 安装包生成 (可选): NSIS

从源码构建

  1. 获取代码

    git clone --recursive https://github.com/SpriteOvO/AirPodsDesktop.git
    cd AirPodsDesktop
    mkdir Build
    cd Build
    
  2. 准备环境

    • 安装 CMake (>= v3.20)。
    • 安装 Visual Studio 2019
    • 克隆并引导 vcpkg
    • 安装 Qt 5.15.2,至少选择 MSVC 2019 32-bit 组件。安装后,将 Qt 目录添加到 PATH 环境变量,或在 CMake 命令中通过 -DCMAKE_PREFIX_PATH 指定。
    • (可选) 安装 NSIS 用于生成安装程序。
  3. 配置与构建
    打开 PowerShell,进入 Build 目录,执行以下命令(请根据你的路径修改参数):

    cmake -G "Visual Studio 16 2019" -A Win32 -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_TOOLCHAIN_FILE=path\to\vcpkg\scripts\buildsystems\vcpkg.cmake ../
    cmake --build . --config RelWithDebInfo
    

    构建完成后,可执行文件位于 ./Binary 目录下。

🚀 使用说明

程序启动后,会常驻在系统托盘。点击托盘图标可以弹出主窗口,查看详细的 AirPods 电池信息和状态动画。右键点击托盘图标可以打开设置菜单。

基础使用

  1. 确保 AirPods 已与电脑配对并连接。
  2. 启动 AirPodsDesktop。程序会自动扫描并绑定附近的 AirPods 设备。
  3. 在主窗口或任务栏状态组件上查看实时电量。
  4. 当佩戴或取下 AirPods 时,程序会根据设置自动控制媒体播放/暂停。

设置选项

通过右键菜单打开“Settings”,可以进行以下配置:

  • 常规:选择程序语言、设置开机自启、解除设备绑定。
  • 视觉:配置系统托盘图标和任务栏的电池信息显示方式(始终显示、低电量时显示或禁用)。
  • 功能:启用/禁用低音频延迟模式、自动人耳检测,调整蓝牙信号接收范围以过滤远距离设备。
  • 关于:查看版本信息、打开日志文件目录。

💻 核心代码

以下是项目中的部分核心代码片段,展示了其核心功能的实现逻辑。

1. Apple 连续性协议解析 (AppleCP.h/AirPods 结构体)

此结构体定义了从 AirPods 广播包中解析出的数据格式,是获取所有状态信息的基础。

// AppleCP.h 中 AirPods 广播数据结构
#pragma pack(push)
#pragma pack(1)
struct AirPods {
    Header header;
    uint8_t flags;
    uint8_t modelId_upper;
    uint8_t modelId_lower;
    uint8_t status;
    uint8_t battery;
    struct {
        uint8_t curr : 4;   // 当前广播侧电量 (0-10)
        uint8_t anot : 4;   // 另一侧电量 (0-10)
        uint8_t caseBox : 4; // 充电盒电量 (0-10)
        uint8_t currCharging : 1; // 当前广播侧是否在充电
        uint8_t anotCharging : 1; // 另一侧是否在充电
        uint8_t caseCharging : 1; // 充电盒是否在充电
        uint8_t : 5; // 保留位
    } battery;
    uint8_t broadcastFrom; // 1=左耳广播,2=右耳广播
    uint8_t bothInCase;
    struct {
        uint8_t closed : 1; // 充电盒盖是否关闭
        uint8_t : 7;
    } lid;
    uint8_t currInEar; // 当前广播侧是否在耳中
    uint8_t anotInEar; // 另一侧是否在耳中
    uint8_t unk12[12];
    uint8_t color; // 设备颜色
    uint8_t unk[3];
    uint16_t buildNumber;
    uint32_t firmwareVersion;
};
#pragma pack(pop)

2. 蓝牙广播监视与状态管理 (AirPods.cpp - StateManager)

StateManager 类负责处理接收到的蓝牙广播,过滤出目标 AirPods 设备,并整合左右耳广播的数据,生成完整的设备状态。

// AirPods.cpp - StateManager::OnAdvReceived 方法片段
std::optional<UpdateEvent> StateManager::OnAdvReceived(Advertisement adv)
{
    std::lock_guard<std::mutex> lock{_mutex};

    // 1. 检查信号强度是否满足最小阈值
    if (adv.GetRssi() < _rssiMin) {
        LOG(Trace, "Advertisement rssi too low. rssi: {}, rssiMin: {}", adv.GetRssi(), _rssiMin);
        return std::nullopt;
    }

    // 2. 判断是否为“可能”的目标设备广播
    if (!IsPossibleDesiredAdv(adv)) {
        return std::nullopt;
    }

    // 3. 更新对应侧(左/右)的广播数据和时间戳
    UpdateAdv(std::move(adv));

    // 4. 尝试更新完整的设备状态(需要左右耳数据都有效)
    return UpdateState();
}

std::optional<StateManager::UpdateEvent> StateManager::UpdateState()
{
    // 检查是否已收到有效的左右耳广播
    bool leftReady = _adv.left.has_value();
    bool rightReady = _adv.right.has_value();
    if (!leftReady || !rightReady) {
        return std::nullopt;
    }

    // 从左右耳广播数据中提取状态
    auto &leftAdv = _adv.left->first;
    auto &rightAdv = _adv.right->first;
    const auto &leftState = leftAdv.GetAdvState();
    const auto &rightState = rightAdv.GetAdvState();

    // 合并状态,创建完整的 AirPods 状态对象
    State newState;
    newState.model = leftState.model; // 假设左右耳型号一致
    newState.displayName = _adv.left->first.GetAddress(); // 使用地址作为显示名
    newState.pods.left = PodState{
        .battery = leftState.pods.left.battery,
        .isCharging = leftState.pods.left.isCharging,
        .isInEar = leftState.pods.left.isInEar
    };
    // ... 类似地填充 right pod 和 case 状态 ...

    auto oldState = std::exchange(_cachedState, newState);
    // 重置状态重置计时器,因为收到了新数据
    _stateResetTimer.left.Stop();
    _stateResetTimer.right.Stop();

    // 如果状态发生变化,返回更新事件
    if (!oldState.has_value() || !(*oldState == newState)) {
        return UpdateEvent{.oldState = std::move(oldState), .newState = std::move(newState)};
    }
    return std::nullopt;
}

3. 低音频延迟模式控制器 (LowAudioLatency.cpp)

Controller 类通过循环播放一个静音音频文件,来维持一个活跃的音频会话,以解决 AirPods 在 Windows 上播放短音频时的延迟问题。

// LowAudioLatency.cpp - Controller 实现
Controller::Controller(QObject *parent) : QObject{parent}
{
    // 使用定时器进行延迟初始化,避免在无音频设备时出错
    _initTimer.callOnTimeout([this] {
        if (Initialize()) {
            _initTimer.stop();
        }
    });
    if (!Initialize()) {
        _initTimer.start(kRetryInterval); // 30秒后重试
    }
}

bool Controller::Initialize()
{
    // 检查是否有可用的音频输出设备
    if (QAudioDeviceInfo::availableDevices(QAudio::AudioOutput).empty()) {
        LOG(Warn, "LowAudioLatency: Try to init, but no audio output device is enabled.");
        return false;
    }

    _mediaPlayer = std::make_unique<QMediaPlayer>();
    _mediaPlaylist = std::make_unique<QMediaPlaylist>();

    // 加载内置的静音音频资源并设置为循环播放
    _mediaPlaylist->addMedia(QUrl{"qrc:/Resource/Audio/Silence.mp3"});
    _mediaPlaylist->setPlaybackMode(QMediaPlaylist::Loop);
    _mediaPlayer->setPlaylist(_mediaPlaylist.get());

    // 连接错误处理信号
    connect(_mediaPlayer.get(), qOverload<QMediaPlayer::Error>(&QMediaPlayer::error),
            this, &Controller::OnError);

    _inited = true;
    LOG(Info, "LowAudioLatency: Init successful.");

    // 如果设置已启用,则立即开始播放
    if (_enabled) {
        Control(true);
    }
    return true;
}

void Controller::Control(bool enable)
{
    LOG(Info, "LowAudioLatency::Controller Control: {}, _inited: {}", enable, _inited);
    if (_inited) {
        if (enable) {
            _mediaPlayer->play(); // 开始播放静音流
        } else {
            _mediaPlayer->stop(); // 停止播放
        }
    }
    _enabled = enable; // 记录用户设置
}

4. 自动人耳检测与媒体控制 (AirPods.cpp - Manager)

Manager 类监听 AirPods 状态变化,并在检测到佩戴状态改变时,调用全局媒体控制接口来播放或暂停媒体。

// AirPods.cpp - Manager::OnStateChanged 方法片段
void Manager::OnStateChanged(Details::StateManager::UpdateEvent updateEvent)
{
    const auto &newState = updateEvent.newState;

    // 检查自动人耳检测功能是否启用
    if (_automaticEarDetection) {
        bool oldBothInEar = false;
        bool newBothInEar = newState.pods.left.isInEar && newState.pods.right.isInEar;

        if (updateEvent.oldState.has_value()) {
            const auto &oldState = updateEvent.oldState.value();
            oldBothInEar = oldState.pods.left.isInEar && oldState.pods.right.isInEar;
        }

        // 如果佩戴状态发生变化:从“未都佩戴”变为“都佩戴”,则播放;反之则暂停。
        if (oldBothInEar != newBothInEar) {
            if (newBothInEar) {
                Core::GlobalMedia::Play();
            } else {
                Core::GlobalMedia::Pause();
            }
        }
    }

    // 发送状态更新信号,通知GUI更新显示
    // (例如:ApdApp->GetMainWindow()->UpdateStateSafely(newState);)
    // ... 其他逻辑 ...
}

5. 电池信息显示组件 (Battery.h)

这是一个自定义的 Qt 电池显示控件,用于在主窗口和任务栏状态中绘制电池图标和电量百分比。

// Battery.h - Battery 控件属性与绘制
class Battery : public QWidget
{
    Q_OBJECT
    Q_PROPERTY(ValueType value READ getValue WRITE setValue)
    Q_PROPERTY(bool isCharging READ isCharging WRITE setCharging)
    // ... 其他属性 (颜色、边框、圆角等) ...

protected:
    void paintEvent(QPaintEvent *event) override
    {
        QPainter painter{this};
        painter.setRenderHints(QPainter::Antialiasing | QPainter::TextAntialiasing);

        // 1. 绘制电池边框
        drawBorder(painter);
        // 2. 根据电量值绘制填充背景(低电量显示警告色)
        drawBackground(painter);
        // 3. 绘制电池正极凸起
        drawHead(painter);
        // 4. 如果正在充电,绘制充电图标(闪电符号)
        if (_isCharging) {
            drawChargingIcon(painter);
        }
        // 5. 如果启用文本,绘制电量百分比
        if (_isShowText) {
            drawText(painter);
        }
    }

private:
    ValueType _value{0}; // 电量值 (0-100)
    bool _isCharging{false}; // 充电状态
    bool _isShowText{true}; // 是否显示文字
    QColor _normalColor{101, 196, 102}; // 正常电量颜色 (绿色)
    QColor _alarmColor{235, 77, 61}; // 低电量报警颜色 (红色)
    // ... 其他成员变量 ...
};

NQyrpOw0FrDpSnxL/35B3qAQRcbrgTD5T19FcQeDHx8=
更多精彩内容 请关注我的个人公众号 公众号(办公AI智能小助手)
对网络安全、黑客技术感兴趣的朋友可以关注我的安全公众号(网络安全技术点滴分享)

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐