pebble/src/fw/services/normal/music.c
2025-01-27 11:38:16 -08:00

461 lines
15 KiB
C

/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "music_internal.h"
#include "apps/system_apps/music_app.h"
#include "drivers/rtc.h"
#include "kernel/events.h"
#include "process_management/app_manager.h"
#include "shell/system_app_ids.auto.h"
#include "os/mutex.h"
#include "os/tick.h"
#include "system/logging.h"
#include "util/math.h"
//! @file This module implements the music service. It provides an abstraction layer on top of the
//! various underlying music metadata and control services: the Pebble Protocol
//! music endpoint (see music_endpoint.c) and Apple Media Service (see ams.c).
//! This module also caches the last known metadata and media player state.
//! @note Only one underlying backend is supported at a time. If a second backend tries to "connect"
//! it is ignored.
#define MUSIC_NORMAL_PLAYBACK_RATE_PERCENT ((int32_t) 100)
//! Cache of the most recently received now playing data. Note that this is read and written from
//! multiple threads, so access is protected by the mutex member.
struct MusicServiceContext {
PebbleRecursiveMutex *mutex;
//! The connected server that provides media metadata and accepts control commands
const MusicServerImplementation *implementation;
//! The volume setting of the current player
uint8_t player_volume_percent;
char player_name[MUSIC_BUFFER_LENGTH];
char title[MUSIC_BUFFER_LENGTH];
char artist[MUSIC_BUFFER_LENGTH];
char album[MUSIC_BUFFER_LENGTH];
uint32_t track_length_ms;
//! Position that was last communicated to Pebble by the server.
//! @note This is not necessarily the actual position. See music_get_pos()
uint32_t track_pos_ms;
//! The time when track_pos_ms was last updated.
RtcTicks track_pos_updated_at;
//! The current playback rate in percent units.
//! Example values:
//! 100 = normal playback rate
//! 0 = paused
//! 200 = 2x playback rate (Apple's Podcast app can vary the playback rate)
//! -100 = backwards at normal rate
int32_t playback_rate_percent;
//! The current playback state
MusicPlayState playback_state;
} s_music_ctx;
void music_init(void) {
s_music_ctx.mutex = mutex_create_recursive();
}
static void copy_and_truncate(char *dest, const char *src, size_t src_length) {
size_t cropped_length = MIN(MUSIC_BUFFER_LENGTH - 1, src_length);
if (src) {
memcpy(dest, src, cropped_length);
}
dest[cropped_length] = 0;
}
static void prv_put_now_playing_changed_event(void) {
PebbleEvent e = {
.type = PEBBLE_MEDIA_EVENT,
.media.type = PebbleMediaEventTypeNowPlayingChanged
};
event_put(&e);
}
bool music_set_connected_server(const MusicServerImplementation *implementation, bool connected) {
enum {
Disconnected = -1,
None = 0,
Connected = 1,
} change_type = None;
mutex_lock_recursive(s_music_ctx.mutex);
if (connected) {
if (s_music_ctx.implementation == NULL) {
change_type = Connected;
s_music_ctx.implementation = implementation;
PBL_LOG(LOG_LEVEL_INFO, "Music server connected: %s", implementation->debug_name);
} else {
PBL_LOG(LOG_LEVEL_ERROR, "Server <0x%p> connected, but another <0x%p> is already registered",
implementation, s_music_ctx.implementation);
}
} else {
if (s_music_ctx.implementation == implementation) {
// Previously registered server got disconnected
change_type = Disconnected;
s_music_ctx.implementation = NULL;
PBL_LOG(LOG_LEVEL_INFO, "Music server disconnected: %s", implementation->debug_name);
} else {
PBL_LOG(LOG_LEVEL_ERROR, "Unknown server <%p> disconnected", implementation);
}
}
if (change_type != None) {
// Upon connect and disconnect, reset the cached data:
music_update_player_volume_percent(0);
// Taking short-cut here, music_update_now_playing already puts NowPlayingChanged event, no
// need to put it again by calling music_update_player_name:
s_music_ctx.player_name[0] = 0;
music_update_now_playing(NULL, 0, NULL, 0, NULL, 0);
music_update_track_duration(0);
const MusicPlayerStateUpdate state = {
.playback_state = MusicPlayStateUnknown,
.playback_rate_percent = 0,
.elapsed_time_ms = 0,
};
music_update_player_playback_state(&state);
PebbleEvent event = {
.type = PEBBLE_MEDIA_EVENT,
.media = {
.type = (change_type == Connected) ? PebbleMediaEventTypeServerConnected :
PebbleMediaEventTypeServerDisconnected,
},
};
event_put(&event);
}
mutex_unlock_recursive(s_music_ctx.mutex);
return (change_type != None);
}
const char * music_get_connected_server_debug_name(void) {
const char *debug_name = NULL;
mutex_lock_recursive(s_music_ctx.mutex);
if (s_music_ctx.implementation) {
debug_name = s_music_ctx.implementation->debug_name;
}
mutex_unlock_recursive(s_music_ctx.mutex);
return debug_name;
}
void music_update_now_playing(const char *title, size_t title_length,
const char *artist, size_t artist_length,
const char *album, size_t album_length) {
mutex_lock_recursive(s_music_ctx.mutex);
copy_and_truncate(s_music_ctx.title, title, title_length);
copy_and_truncate(s_music_ctx.artist, artist, artist_length);
copy_and_truncate(s_music_ctx.album, album, album_length);
mutex_unlock_recursive(s_music_ctx.mutex);
prv_put_now_playing_changed_event();
}
static void prv_update_string_and_put_event(const char *value, size_t value_length, off_t offset) {
mutex_lock_recursive(s_music_ctx.mutex);
char *buffer = ((char *) &s_music_ctx) + offset;
copy_and_truncate(buffer, value, value_length);
mutex_unlock_recursive(s_music_ctx.mutex);
prv_put_now_playing_changed_event();
}
void music_update_player_name(const char *player_name, size_t player_name_length) {
// TODO: actually do something with this
off_t o = offsetof(__typeof__(s_music_ctx), player_name);
prv_update_string_and_put_event(player_name, player_name_length, o);
}
void music_update_track_title(const char *title, size_t title_length) {
off_t o = offsetof(__typeof__(s_music_ctx), title);
prv_update_string_and_put_event(title, title_length, o);
}
void music_update_track_artist(const char *artist, size_t artist_length) {
off_t o = offsetof(__typeof__(s_music_ctx), artist);
prv_update_string_and_put_event(artist, artist_length, o);
}
void music_update_track_album(const char *album, size_t album_length) {
off_t o = offsetof(__typeof__(s_music_ctx), album);
prv_update_string_and_put_event(album, album_length, o);
}
static void prv_put_pos_changed_event(void) {
PebbleEvent e = {
.type = PEBBLE_MEDIA_EVENT,
.media.type = PebbleMediaEventTypeTrackPosChanged,
};
event_put(&e);
}
void music_update_track_position(uint32_t track_pos_ms) {
mutex_lock_recursive(s_music_ctx.mutex);
s_music_ctx.track_pos_ms = track_pos_ms;
s_music_ctx.track_pos_updated_at = rtc_get_ticks();
mutex_unlock_recursive(s_music_ctx.mutex);
prv_put_pos_changed_event();
}
void music_update_track_duration(uint32_t track_duration_ms) {
mutex_lock_recursive(s_music_ctx.mutex);
s_music_ctx.track_length_ms = track_duration_ms;
mutex_unlock_recursive(s_music_ctx.mutex);
prv_put_pos_changed_event();
}
void music_get_now_playing(char *title, char *artist, char *album) {
mutex_lock_recursive(s_music_ctx.mutex);
if (title) {
strcpy(title, s_music_ctx.title);
}
if (artist) {
strcpy(artist, s_music_ctx.artist);
}
if (album) {
strcpy(album, s_music_ctx.album);
}
mutex_unlock_recursive(s_music_ctx.mutex);
}
bool music_get_player_name(char *player_name_out) {
mutex_lock_recursive(s_music_ctx.mutex);
const bool has_player_name = (s_music_ctx.player_name[0] != 0);
if (player_name_out) {
strcpy(player_name_out, s_music_ctx.player_name);
}
mutex_unlock_recursive(s_music_ctx.mutex);
return has_player_name;
}
bool music_has_now_playing(void) {
bool has_now_playing = false;
mutex_lock_recursive(s_music_ctx.mutex);
if (s_music_ctx.title[0] != 0 ||
s_music_ctx.artist[0] != 0) {
has_now_playing = true;
}
mutex_unlock_recursive(s_music_ctx.mutex);
return has_now_playing;
}
uint32_t music_get_ms_since_pos_last_updated(void) {
mutex_lock_recursive(s_music_ctx.mutex);
const RtcTicks time_elapsed_ticks = rtc_get_ticks() - s_music_ctx.track_pos_updated_at;
const uint32_t time_elapsed_ms = ticks_to_milliseconds(time_elapsed_ticks);
mutex_unlock_recursive(s_music_ctx.mutex);
return time_elapsed_ms;
}
void music_get_pos(uint32_t *track_pos_ms, uint32_t *track_length_ms) {
mutex_lock_recursive(s_music_ctx.mutex);
const int32_t time_elapsed_ms = music_get_ms_since_pos_last_updated();
const int32_t track_time_elapsed =
(time_elapsed_ms * s_music_ctx.playback_rate_percent) / MUSIC_NORMAL_PLAYBACK_RATE_PERCENT;
const int32_t pos_ms = s_music_ctx.track_pos_ms + track_time_elapsed;
const int32_t length_ms = s_music_ctx.track_length_ms;
*track_pos_ms = CLIP(pos_ms, 0, length_ms);
*track_length_ms = length_ms;
mutex_unlock_recursive(s_music_ctx.mutex);
}
int32_t music_get_playback_rate_percent(void) {
mutex_lock_recursive(s_music_ctx.mutex);
int32_t playback_rate_percent = s_music_ctx.playback_rate_percent;
mutex_unlock_recursive(s_music_ctx.mutex);
return playback_rate_percent;
}
uint8_t music_get_volume_percent(void) {
mutex_lock_recursive(s_music_ctx.mutex);
int32_t player_volume_percent = s_music_ctx.player_volume_percent;
mutex_unlock_recursive(s_music_ctx.mutex);
return player_volume_percent;
}
static void prv_put_state_changed_event(MusicPlayState playback_state) {
PebbleEvent event = {
.type = PEBBLE_MEDIA_EVENT,
.media = {
.type = PebbleMediaEventTypePlaybackStateChanged,
.playback_state = playback_state,
},
};
event_put(&event);
}
void music_update_player_playback_state(const MusicPlayerStateUpdate *state) {
mutex_lock_recursive(s_music_ctx.mutex);
s_music_ctx.playback_state = state->playback_state;
s_music_ctx.playback_rate_percent = state->playback_rate_percent;
s_music_ctx.track_pos_ms = state->elapsed_time_ms;
s_music_ctx.track_pos_updated_at = rtc_get_ticks();
mutex_unlock_recursive(s_music_ctx.mutex);
prv_put_state_changed_event(state->playback_state);
prv_put_pos_changed_event();
}
void music_update_player_volume_percent(uint8_t volume_percent) {
mutex_lock_recursive(s_music_ctx.mutex);
s_music_ctx.player_volume_percent = volume_percent;
mutex_unlock_recursive(s_music_ctx.mutex);
PebbleEvent event = {
.type = PEBBLE_MEDIA_EVENT,
.media = {
.type = PebbleMediaEventTypeVolumeChanged,
.volume_percent = volume_percent,
},
};
event_put(&event);
}
MusicPlayState music_get_playback_state(void) {
if (!music_is_playback_state_reporting_supported()) {
return MusicPlayStateUnknown;
}
MusicPlayState result;
mutex_lock_recursive(s_music_ctx.mutex);
result = s_music_ctx.playback_state;
mutex_unlock_recursive(s_music_ctx.mutex);
return result;
}
static void * prv_implementation_function_for_offset(off_t offset) {
typedef void (*FuncPtr)(void);
FuncPtr func_ptr = NULL;
mutex_lock_recursive(s_music_ctx.mutex);
if (s_music_ctx.implementation) {
func_ptr = *(FuncPtr *) (((const uint8_t *)s_music_ctx.implementation) + offset);
}
mutex_unlock_recursive(s_music_ctx.mutex);
return func_ptr;
}
void music_command_send(MusicCommand command) {
const off_t o = offsetof(__typeof__(*s_music_ctx.implementation), command_send);
void (*command_send)(MusicCommand) = prv_implementation_function_for_offset(o);
if (command_send) {
command_send(command);
}
}
void music_request_reduced_latency(bool reduced_latency) {
const off_t o = offsetof(__typeof__(*s_music_ctx.implementation), request_reduced_latency);
void (*request_reduced_latency)(bool) = prv_implementation_function_for_offset(o);
if (request_reduced_latency) {
request_reduced_latency(reduced_latency);
}
}
void music_request_low_latency_for_period(uint32_t period_ms) {
const off_t o = offsetof(__typeof__(*s_music_ctx.implementation), request_low_latency_for_period);
void (*request_low_latency_for_period)(uint32_t) = prv_implementation_function_for_offset(o);
if (request_low_latency_for_period) {
request_low_latency_for_period(period_ms);
}
}
bool music_is_command_supported(MusicCommand command) {
const off_t o = offsetof(__typeof__(*s_music_ctx.implementation),
is_command_supported);
bool (*func_ptr)(MusicCommand) = prv_implementation_function_for_offset(o);
if (!func_ptr) {
return false;
}
return func_ptr(command);
}
static bool prv_call_implementation_bool_return_void_args(off_t offset) {
bool (*func_ptr)(void) = prv_implementation_function_for_offset(offset);
if (!func_ptr) {
return false; // defaults to false when there is no "connected" implementation
}
return func_ptr();
}
bool music_needs_user_to_start_playback_on_phone(void) {
const off_t o = offsetof(__typeof__(*s_music_ctx.implementation),
needs_user_to_start_playback_on_phone);
return prv_call_implementation_bool_return_void_args(o);
}
static bool prv_is_capability_supported(MusicServerCapability capability) {
const off_t o = offsetof(__typeof__(*s_music_ctx.implementation), get_capability_bitset);
MusicServerCapability (*func_ptr)(void) = prv_implementation_function_for_offset(o);
if (!func_ptr) {
return false;
}
return (func_ptr() & capability);
}
bool music_is_playback_state_reporting_supported(void) {
return prv_is_capability_supported(MusicServerCapabilityPlaybackStateReporting);
}
bool music_is_progress_reporting_supported(void) {
// Check capability and that track length is greater than 0
uint32_t track_length_ms;
mutex_lock_recursive(s_music_ctx.mutex);
track_length_ms = s_music_ctx.track_length_ms;
mutex_unlock_recursive(s_music_ctx.mutex);
return (prv_is_capability_supported(MusicServerCapabilityProgressReporting) && track_length_ms);
}
bool music_is_volume_reporting_supported(void) {
return prv_is_capability_supported(MusicServerCapabilityVolumeReporting);
}
void command_print_now_playing(void) {
char title[MUSIC_BUFFER_LENGTH];
char artist[MUSIC_BUFFER_LENGTH];
char album[MUSIC_BUFFER_LENGTH];
music_get_now_playing(title, artist, album);
char buffer[128];
dbgserial_putstr_fmt(buffer, 128, "title=%s; artist=%s; album=%s", title, artist, album);
}