mirror of
https://github.com/google/pebble.git
synced 2025-05-05 09:21:40 -04:00
461 lines
15 KiB
C
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);
|
|
}
|
|
|