diff --git a/src/menu/menu.c b/src/menu/menu.c index d2c37ac8..3e683056 100644 --- a/src/menu/menu.c +++ b/src/menu/menu.c @@ -5,6 +5,7 @@ #include "actions.h" #include "menu_state.h" #include "menu.h" +#include "mp3player.h" #include "settings.h" #include "utils/fs.h" #include "views/views.h" @@ -24,6 +25,8 @@ static void menu_init (settings_t *settings) { rspq_init(); rdpq_init(); + mp3player_mixer_init(); + boot_pending = false; menu = calloc(1, sizeof(menu_t)); diff --git a/src/menu/mp3player.c b/src/menu/mp3player.c index 07816ba2..e4a57880 100644 --- a/src/menu/mp3player.c +++ b/src/menu/mp3player.c @@ -9,7 +9,6 @@ #include "libs/minimp3/minimp3_ex.h" -#define BUFFER_SIZE (16 * 1024) #define MIXER_CHANNEL (0) @@ -17,19 +16,18 @@ typedef struct { bool loaded; bool io_error; - mp3dec_t dec; - mp3dec_frame_info_t info; - FIL fil; FSIZE_t data_start; - uint8_t buffer[BUFFER_SIZE]; + mp3dec_t dec; + mp3dec_frame_info_t info; + + uint8_t buffer[24 * 1024]; uint8_t *buffer_ptr; size_t buffer_left; - short samples[MINIMP3_MAX_SAMPLES_PER_FRAME]; - short *samples_ptr; - int samples_left; + float duration; + float bitrate; waveform_t wave; } mp3player_t; @@ -41,8 +39,6 @@ static void mp3player_reset_decoder (void) { mp3dec_init(&p->dec); p->buffer_ptr = p->buffer; p->buffer_left = 0; - p->samples_ptr = p->samples; - p->samples_left = 0; } static void mp3player_fill_buffer (void) { @@ -61,59 +57,64 @@ static void mp3player_fill_buffer (void) { p->buffer_ptr = p->buffer; } - if (f_read(&p->fil, p->buffer + p->buffer_left, BUFFER_SIZE - p->buffer_left, &bytes_read) == FR_OK) { + if (f_read(&p->fil, p->buffer + p->buffer_left, sizeof(p->buffer) - p->buffer_left, &bytes_read) == FR_OK) { p->buffer_left += bytes_read; } else { p->io_error = true; } } -static void mp3player_decode_samples (short *buffer, int buffer_samples) { - if (p->samples_left > 0) { - int samples_to_copy = MIN(p->samples_left, buffer_samples); - - memcpy(buffer, p->samples_ptr, samples_to_copy * sizeof(short) * p->info.channels); - - p->samples_ptr += samples_to_copy * p->info.channels; - p->samples_left -= samples_to_copy; - - buffer += samples_to_copy * p->info.channels; - buffer_samples -= samples_to_copy; - } - - while (buffer_samples > 0) { +static void mp3player_wave_read (void *ctx, samplebuffer_t *sbuf, int wpos, int wlen, bool seeking) { + while (wlen > 0) { mp3player_fill_buffer(); - int samples = mp3dec_decode_frame(&p->dec, p->buffer_ptr, p->buffer_left, p->samples, &p->info); + int samples = mp3dec_decode_frame(&p->dec, p->buffer_ptr, p->buffer_left, NULL, &p->info); + + if (samples > 0) { + short *buffer = (short *) (samplebuffer_append(sbuf, samples)); + + p->buffer_ptr += p->info.frame_offset; + p->buffer_left -= p->info.frame_offset; + + mp3dec_decode_frame(&p->dec, p->buffer_ptr, p->buffer_left, buffer, &p->info); + + wlen -= samples; + } p->buffer_ptr += p->info.frame_bytes; p->buffer_left -= p->info.frame_bytes; - if (samples > 0) { - int samples_to_copy = MIN(samples, buffer_samples); - - memcpy(buffer, p->samples, samples_to_copy * sizeof(short) * p->info.channels); - - p->samples_ptr = p->samples + samples_to_copy * p->info.channels; - p->samples_left = samples - samples_to_copy; - - buffer += samples_to_copy * p->info.channels; - buffer_samples -= samples_to_copy; - } - if (p->info.frame_bytes == 0) { - memset(buffer, 0, buffer_samples * sizeof(short) * p->info.channels); - buffer_samples = 0; + short *buffer = (short *) (samplebuffer_append(sbuf, wlen)); + + memset(buffer, 0, wlen * sizeof(short) * p->info.channels); + + wlen = 0; } } } -static void mp3player_wave_read (void *ctx, samplebuffer_t *sbuf, int wpos, int wlen, bool seeking) { - short *buf = (short *) (samplebuffer_append(sbuf, wlen)); - mp3player_decode_samples(buf, wlen); +static void mp3player_calculate_duration (int samples) { + uint32_t frames; + int delay, padding; + + long data_size = (f_size(&p->fil) - p->data_start); + if (mp3dec_check_vbrtag((const uint8_t *) (p->buffer_ptr), p->info.frame_bytes, &frames, &delay, &padding) > 0) { + p->duration = (frames * samples) / (float) (p->info.hz); + p->bitrate = (data_size * 8) / p->duration; + } else { + p->bitrate = p->info.bitrate_kbps * 1000; + p->duration = data_size / (p->bitrate / 8); + } } +void mp3player_mixer_init (void) { + // NOTE: Deliberately setting max_frequency to twice of actual maximum samplerate of mp3 file. + // It's tricking mixer into creating buffer long enough for appending data created by mp3dec_decode_frame. + mixer_ch_set_limits(MIXER_CHANNEL, 16, 96000, 0); +} + mp3player_err_t mp3player_init (void) { p = calloc(1, sizeof(mp3player_t)); @@ -166,20 +167,26 @@ mp3player_err_t mp3player_load (char *path) { size_t id3v2_skip = mp3dec_skip_id3v2((const uint8_t *) (p->buffer_ptr), p->buffer_left); if (id3v2_skip > 0) { - f_lseek(&p->fil, f_tell(&p->fil) - p->buffer_left + id3v2_skip); + if (f_lseek(&p->fil, f_tell(&p->fil) - p->buffer_left + id3v2_skip) != FR_OK) { + return MP3PLAYER_ERR_IO; + } mp3player_reset_decoder(); continue; } - if (mp3dec_decode_frame(&p->dec, p->buffer_ptr, p->buffer_left, NULL, &p->info) > 0) { - mp3dec_init(&p->dec); - + int samples = mp3dec_decode_frame(&p->dec, p->buffer_ptr, p->buffer_left, NULL, &p->info); + if (samples > 0) { p->loaded = true; p->data_start = f_tell(&p->fil) - p->buffer_left + p->info.frame_offset; + p->buffer_ptr += p->info.frame_offset; + p->buffer_left -= p->info.frame_offset; + p->wave.channels = p->info.channels; p->wave.frequency = p->info.hz; + mp3player_calculate_duration(samples); + return MP3PLAYER_OK; } @@ -220,7 +227,7 @@ bool mp3player_is_playing (void) { } bool mp3player_is_finished (void) { - return f_eof(&p->fil) && p->buffer_left == 0 && p->samples_left == 0; + return p->loaded && f_eof(&p->fil) && p->buffer_left == 0; } mp3player_err_t mp3player_play (void) { @@ -255,15 +262,20 @@ mp3player_err_t mp3player_toggle (void) { return MP3PLAYER_OK; } +void mp3player_mute (bool mute) { + float volume = mute ? 0.f : 1.f; + mixer_ch_set_vol(MIXER_CHANNEL, volume, volume); +} + mp3player_err_t mp3player_seek (int seconds) { - // NOTE: Rough approximation using last frame bitrate to calculate number of bytes to be skipped. + // NOTE: Rough approximation using average bitrate to calculate number of bytes to be skipped. // Good enough but not very accurate for variable bitrate files. if (!p->loaded) { return MP3PLAYER_ERR_NO_FILE; } - long bytes_to_move = (long) (((p->info.bitrate_kbps * 1024) * seconds) / 8); + long bytes_to_move = (long) ((p->bitrate * seconds) / 8); if (bytes_to_move == 0) { return MP3PLAYER_OK; } @@ -279,14 +291,47 @@ mp3player_err_t mp3player_seek (int seconds) { } mp3player_reset_decoder(); + mp3player_fill_buffer(); + + if (p->io_error) { + return MP3PLAYER_ERR_IO; + } return MP3PLAYER_OK; } +float mp3player_get_duration (void) { + if (!p->loaded) { + return 0.f; + } + + return p->duration; +} + +float mp3player_get_bitrate (void) { + if (!p->loaded) { + return 0.f; + } + + return p->bitrate; +} + +int mp3player_get_samplerate (void) { + if (!p->loaded) { + return 0; + } + + return p->info.hz; +} + float mp3player_get_progress (void) { // NOTE: Rough approximation using file pointer instead of processed samples. // Good enough but not very accurate for variable bitrate files. + if (!p->loaded) { + return 0.f; + } + FSIZE_t data_size = f_size(&p->fil) - p->data_start; FSIZE_t data_consumed = f_tell(&p->fil) - p->buffer_left; FSIZE_t data_position = (data_consumed > p->data_start) ? (data_consumed - p->data_start) : 0; diff --git a/src/menu/mp3player.h b/src/menu/mp3player.h index e5ca938d..85d078ed 100644 --- a/src/menu/mp3player.h +++ b/src/menu/mp3player.h @@ -14,6 +14,7 @@ typedef enum { } mp3player_err_t; +void mp3player_mixer_init (void); mp3player_err_t mp3player_init (void); void mp3player_deinit (void); mp3player_err_t mp3player_load (char *path); @@ -24,7 +25,11 @@ bool mp3player_is_finished (void); mp3player_err_t mp3player_play (void); void mp3player_stop (void); mp3player_err_t mp3player_toggle (void); +void mp3player_mute (bool mute); mp3player_err_t mp3player_seek (int seconds); +float mp3player_get_duration (void); +float mp3player_get_bitrate (void); +int mp3player_get_samplerate (void); float mp3player_get_progress (void); diff --git a/src/menu/views/player.c b/src/menu/views/player.c index ca048b8e..ddc7a22c 100644 --- a/src/menu/views/player.c +++ b/src/menu/views/player.c @@ -6,6 +6,13 @@ #include "../mp3player.h" +#define SEEK_SECONDS 10 +#define SEEK_UNMUTE_TIMEOUT 17 + + +static int unmute_counter; + + static void format_name (char *buffer, char *name) { int cutoff_length = 57; int name_length = strlen(name); @@ -18,8 +25,31 @@ static void format_name (char *buffer, char *name) { } } +static void format_elapsed_duration (char *buffer, float elapsed, float duration) { + strcpy(buffer, " "); + + if (duration >= 3600) { + sprintf(buffer + strlen(buffer), "%02d:", (int) (elapsed) / 3600); + } + sprintf(buffer + strlen(buffer), "%02d:%02d", ((int) (elapsed) % 3600) / 60, (int) (elapsed) % 60); + + strcat(buffer, " / "); + + if (duration >= 3600) { + sprintf(buffer + strlen(buffer), "%02d:", (int) (duration) / 3600); + } + sprintf(buffer + strlen(buffer), "%02d:%02d", ((int) (duration) % 3600) / 60, (int) (duration) % 60); +} + static void process (menu_t *menu) { + if (unmute_counter > 0) { + unmute_counter -= 1; + if (unmute_counter == 0) { + mp3player_mute(false); + } + } + if (mp3player_process() != MP3PLAYER_OK) { menu->next_mode = MENU_MODE_ERROR; } else if (menu->actions.back) { @@ -29,7 +59,9 @@ static void process (menu_t *menu) { menu->next_mode = MENU_MODE_ERROR; } } else if (menu->actions.go_left || menu->actions.go_right) { - int seconds = ((menu->actions.go_left ? -1 : 1) * 10); + mp3player_mute(true); + unmute_counter = SEEK_UNMUTE_TIMEOUT; + int seconds = (menu->actions.go_left ? -SEEK_SECONDS : SEEK_SECONDS); if (mp3player_seek(seconds) != MP3PLAYER_OK) { menu->next_mode = MENU_MODE_ERROR; } @@ -65,6 +97,31 @@ static void draw (menu_t *menu, surface_t *d) { format_name(buffer, menu->browser.list[menu->browser.selected].name); rdpq_font_print(menu->assets.font, buffer); + text_y += layout->line_height * 2; + rdpq_font_begin(text_color); + rdpq_font_position(text_x, text_y + menu->assets.font_height); + rdpq_font_print(menu->assets.font, "Track elapsed / length:"); + text_y += layout->line_height; + rdpq_font_position(text_x, text_y + menu->assets.font_height); + format_elapsed_duration(buffer, mp3player_get_duration() * mp3player_get_progress(), mp3player_get_duration()); + rdpq_font_print(menu->assets.font, buffer); + + text_y += layout->line_height * 2; + rdpq_font_begin(text_color); + rdpq_font_position(text_x, text_y + menu->assets.font_height); + rdpq_font_print(menu->assets.font, "Average bitrate:"); + text_y += layout->line_height; + rdpq_font_position(text_x, text_y + menu->assets.font_height); + rdpq_font_printf(menu->assets.font, " %.0f kbps", mp3player_get_bitrate() / 1000); + + text_y += layout->line_height * 2; + rdpq_font_begin(text_color); + rdpq_font_position(text_x, text_y + menu->assets.font_height); + rdpq_font_print(menu->assets.font, "Samplerate:"); + text_y += layout->line_height; + rdpq_font_position(text_x, text_y + menu->assets.font_height); + rdpq_font_printf(menu->assets.font, " %d Hz", mp3player_get_samplerate()); + // Actions bar text_y = layout->actions_y + layout->offset_text_y; rdpq_font_position(text_x, text_y + menu->assets.font_height); @@ -77,7 +134,7 @@ static void draw (menu_t *menu, surface_t *d) { } text_y += layout->line_height; rdpq_font_position(text_x, text_y + menu->assets.font_height); - rdpq_font_print(menu->assets.font, "B: Exit | Left/Right: Rewind/Fast forward"); + rdpq_font_print(menu->assets.font, "B: Exit | Left / Right: Rewind / Fast forward"); rdpq_font_end(); rdpq_detach_show(); @@ -87,6 +144,8 @@ static void draw (menu_t *menu, surface_t *d) { void view_player_init (menu_t *menu) { mp3player_err_t error; + unmute_counter = 0; + error = mp3player_init(); if (error != MP3PLAYER_OK) { menu->next_mode = MENU_MODE_ERROR; @@ -102,6 +161,7 @@ void view_player_init (menu_t *menu) { menu->next_mode = MENU_MODE_ERROR; mp3player_deinit(); } else { + mp3player_mute(false); mp3player_play(); }