| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297 |
- /**
- * Source-agnostic playback controller.
- *
- * For AirPlay:
- * - Play/Pause: mute/unmute the DAC locally. If the source sent
- * DACP headers (AirPlay 1), we also forward the command via DACP
- * so the source UI updates. Modern iOS AirPlay 2 does not send
- * DACP headers, so local mute is the only option.
- * - Next/Prev: forwarded via DACP when available (AirPlay 1 only).
- * - Volume: adjusted locally (DAC + NVS persistence) and mirrored
- * to the source via DACP when available.
- *
- * For Bluetooth:
- * - All commands sent as AVRCP passthrough to source device
- * - Source device controls playback and sends volume back
- */
- #include "playback_control.h"
- #include "dac.h"
- #include "dacp_client.h"
- #include "rtsp_events.h"
- #include "rtsp_server.h"
- #include "settings.h"
- #include "esp_log.h"
- #ifdef CONFIG_BT_A2DP_ENABLE
- #include "a2dp_sink.h"
- #endif
- static const char *TAG = "playback_ctrl";
- #define VOLUME_STEP_DB 3.0f
- #define VOLUME_MIN_DB (-30.0f)
- #define VOLUME_MAX_DB 0.0f
- static playback_source_t s_source = PLAYBACK_SOURCE_NONE;
- static bool s_muted = false;
- static float s_pre_mute_db = -15.0f;
- esp_err_t playback_control_init(void) {
- dacp_init();
- ESP_LOGI(TAG, "Playback control initialized");
- return ESP_OK;
- }
- void playback_control_set_source(playback_source_t source) {
- s_muted = false;
- s_source = source;
- ESP_LOGI(TAG, "Source set to %d", source);
- }
- playback_source_t playback_control_get_source(void) {
- return s_source;
- }
- // ============================================================================
- // AirPlay local volume helpers
- // ============================================================================
- static float clamp_volume(float db) {
- if (db < VOLUME_MIN_DB) {
- return VOLUME_MIN_DB;
- }
- if (db > VOLUME_MAX_DB) {
- return VOLUME_MAX_DB;
- }
- return db;
- }
- // Convert AirPlay dB (-30..0) to DACP percent (0..100)
- static float db_to_dacp_percent(float db) {
- if (db <= VOLUME_MIN_DB) {
- return 0.0f;
- }
- if (db >= VOLUME_MAX_DB) {
- return 100.0f;
- }
- return ((db - VOLUME_MIN_DB) / (VOLUME_MAX_DB - VOLUME_MIN_DB)) * 100.0f;
- }
- static void airplay_adjust_volume(float step_db) {
- float current_db;
- if (settings_get_volume(¤t_db) != ESP_OK) {
- current_db = -15.0f; // default 50 %
- }
- float new_db = clamp_volume(current_db + step_db);
- airplay_set_volume(new_db);
- if (s_muted) {
- // Update saved level so unmute restores the new volume
- s_pre_mute_db = new_db;
- } else {
- dac_set_volume(new_db);
- }
- ESP_LOGI(TAG, "AirPlay volume: %.1f -> %.1f dB%s", current_db, new_db,
- s_muted ? " (muted)" : "");
- // Notify AirPlay client via DACP (best-effort, don't block local action)
- dacp_send_volume(db_to_dacp_percent(new_db));
- }
- // ============================================================================
- // Public API
- // ============================================================================
- void playback_control_play_pause(void) {
- switch (s_source) {
- case PLAYBACK_SOURCE_AIRPLAY: {
- if (dacp_is_active()) {
- // Tell the source to toggle playback — it will FLUSH the stream
- // on pause and RECORD on resume, so we don't need local muting.
- // Signal the v1 grace period loop (if active) so it sends the
- // DACP command at the right time and keeps waiting for reconnect.
- // If not in a grace period, the flag is harmless.
- rtsp_server_request_resume();
- dacp_send_playpause();
- ESP_LOGI(TAG, "AirPlay play/pause sent via DACP");
- } else {
- // Fallback: mute/unmute the DAC locally when no DACP session
- if (!s_muted) {
- if (settings_get_volume(&s_pre_mute_db) != ESP_OK) {
- s_pre_mute_db = -15.0f; // default 50 %
- }
- dac_set_volume(VOLUME_MIN_DB);
- s_muted = true;
- rtsp_events_emit(RTSP_EVENT_PAUSED, NULL);
- ESP_LOGI(TAG, "AirPlay muted locally (was %.1f dB)", s_pre_mute_db);
- } else {
- dac_set_volume(s_pre_mute_db);
- s_muted = false;
- rtsp_events_emit(RTSP_EVENT_PLAYING, NULL);
- ESP_LOGI(TAG, "AirPlay unmuted locally (%.1f dB)", s_pre_mute_db);
- }
- }
- break;
- }
- #ifdef CONFIG_BT_A2DP_ENABLE
- case PLAYBACK_SOURCE_BLUETOOTH:
- bt_a2dp_send_playpause();
- break;
- #endif
- default:
- ESP_LOGI(TAG, "Play/pause: no active source (source=%d)", s_source);
- break;
- }
- }
- void playback_control_volume_up(void) {
- switch (s_source) {
- case PLAYBACK_SOURCE_AIRPLAY:
- airplay_adjust_volume(VOLUME_STEP_DB);
- break;
- #ifdef CONFIG_BT_A2DP_ENABLE
- case PLAYBACK_SOURCE_BLUETOOTH:
- bt_a2dp_send_volume_up();
- break;
- #endif
- default:
- break;
- }
- }
- void playback_control_volume_down(void) {
- switch (s_source) {
- case PLAYBACK_SOURCE_AIRPLAY:
- airplay_adjust_volume(-VOLUME_STEP_DB);
- break;
- #ifdef CONFIG_BT_A2DP_ENABLE
- case PLAYBACK_SOURCE_BLUETOOTH:
- bt_a2dp_send_volume_down();
- break;
- #endif
- default:
- break;
- }
- }
- void playback_control_next(void) {
- switch (s_source) {
- case PLAYBACK_SOURCE_AIRPLAY:
- dacp_send_next();
- ESP_LOGI(TAG, "AirPlay next track via DACP");
- break;
- #ifdef CONFIG_BT_A2DP_ENABLE
- case PLAYBACK_SOURCE_BLUETOOTH:
- bt_a2dp_send_next();
- break;
- #endif
- default:
- break;
- }
- }
- void playback_control_prev(void) {
- switch (s_source) {
- case PLAYBACK_SOURCE_AIRPLAY:
- dacp_send_prev();
- ESP_LOGI(TAG, "AirPlay prev track via DACP");
- break;
- #ifdef CONFIG_BT_A2DP_ENABLE
- case PLAYBACK_SOURCE_BLUETOOTH:
- bt_a2dp_send_prev();
- break;
- #endif
- default:
- break;
- }
- }
- void playback_control_toggle_mute(void) {
- switch (s_source) {
- case PLAYBACK_SOURCE_AIRPLAY:
- if (!s_muted) {
- if (settings_get_volume(&s_pre_mute_db) != ESP_OK) {
- s_pre_mute_db = -15.0f; // default 50 %
- }
- dac_set_volume(VOLUME_MIN_DB);
- s_muted = true;
- rtsp_events_emit(RTSP_EVENT_PAUSED, NULL);
- ESP_LOGI(TAG, "AirPlay muted locally (was %.1f dB)", s_pre_mute_db);
- } else {
- dac_set_volume(s_pre_mute_db);
- s_muted = false;
- rtsp_events_emit(RTSP_EVENT_PLAYING, NULL);
- ESP_LOGI(TAG, "AirPlay unmuted locally (%.1f dB)", s_pre_mute_db);
- }
- break;
- #ifdef CONFIG_BT_A2DP_ENABLE
- case PLAYBACK_SOURCE_BLUETOOTH:
- // Bluetooth uses AVRCP absolute volume — no dedicated mute. Log.
- ESP_LOGI(TAG, "Bluetooth: mute toggle not supported (use source device)");
- break;
- #endif
- default:
- ESP_LOGI(TAG, "Toggle mute: no active source (source=%d)", s_source);
- break;
- }
- }
- bool playback_control_is_muted(void) {
- return s_muted;
- }
- int playback_control_get_volume_percent(void) {
- if (s_muted) {
- return 0;
- }
- float db;
- if (settings_get_volume(&db) != ESP_OK) {
- db = -15.0f;
- }
- return (int)(db_to_dacp_percent(clamp_volume(db)) + 0.5f);
- }
- void playback_control_set_volume_percent(int percent) {
- if (percent < 0) percent = 0;
- if (percent > 100) percent = 100;
- ESP_LOGD(TAG, "音量同步: %d%% (当前源=%d)", percent, s_source);
- switch (s_source) {
- case PLAYBACK_SOURCE_AIRPLAY: {
- float db = VOLUME_MIN_DB + (percent / 100.0f) * (VOLUME_MAX_DB - VOLUME_MIN_DB);
- db = clamp_volume(db);
- ESP_LOGD(TAG, "AirPlay 同步 - DB: %.2f", db);
- airplay_set_volume(db);
- ESP_LOGD(TAG, "AirPlay 同步 - airplay_set_volume 执行后");
- settings_set_volume(db);
- ESP_LOGD(TAG, "AirPlay 同步 - settings_set_volume 执行后");
- if (s_muted) {
- s_pre_mute_db = db;
- } else {
- dac_set_volume(db);
- }
- ESP_LOGD(TAG, "AirPlay 同步 - dac_set_volume 执行后");
- dacp_send_volume(percent);
- ESP_LOGD(TAG, "AirPlay 同步 - dacp_send_volume 执行后");
- break;
- }
- #ifdef CONFIG_BT_A2DP_ENABLE
- case PLAYBACK_SOURCE_BLUETOOTH: {
- uint8_t bt_vol = (uint8_t)(percent * 127 / 100);
- ESP_LOGI(TAG, "蓝牙同步 - 音量: %d/127", bt_vol);
- bt_a2dp_set_volume(bt_vol);
- ESP_LOGI(TAG, "蓝牙同步 - bt_a2dp_set_volume 执行后");
- break;
- }
- #endif
- default:
- ESP_LOGI(TAG, "无活跃音频源,不执行音量同步");
- break;
- }
- }
|