From b53bbf7daed1d57acd18afe2c691420ab4f7bfdd Mon Sep 17 00:00:00 2001 From: Mateusz Faderewski Date: Sun, 9 Jul 2023 00:01:41 +0200 Subject: [PATCH] New browser view using rdpq acceleration + MP3 player (#10) ## Description This change replaces deprecated graphics libdragon api with rdpq hardware accelerated drawing calls. New view has been added: MP3 player ## Motivation and Context Use newest and supported features of libdragon api ## How Has This Been Tested? On hardware with SC64 flashcart ## Screenshots ![Screenshot 2023-07-08 23-57-56](https://github.com/Polprzewodnikowy/N64FlashcartMenu/assets/3756990/3f791246-5f70-43d1-8c54-aeac62513ff3) ![Screenshot 2023-07-08 23-58-16](https://github.com/Polprzewodnikowy/N64FlashcartMenu/assets/3756990/fdf436bf-6201-4b43-bebc-70be993ebc50) ## Types of changes - [ ] Improvement (non-breaking change that adds a new feature) - [ ] Bug fix (fixes an issue) - [x] Breaking change (breaking change) - [ ] Config and build (change in the configuration and build system, has no impact on code or features) ## Checklist: - [x] My code follows the code style of this project. - [x] My change requires a change to the documentation. - [ ] I have updated the documentation accordingly. - [ ] I have added tests to cover my changes. - [ ] All new and existing tests passed. Signed-off-by: GITHUB_USER --- Makefile | 25 +- assets/.gitkeep | 0 assets/FiraMono-Bold.ttf | Bin 0 -> 201708 bytes libdragon | 2 +- src/flashcart/flashcart.c | 8 +- src/flashcart/flashcart.h | 4 +- src/flashcart/sc64/sc64.c | 46 +- src/flashcart/sc64/sc64_internal.c | 26 + src/flashcart/sc64/sc64_internal.h | 3 + src/libs/minimp3/minimp3.h | 1865 ++++++++++++++++++++++++++ src/libs/minimp3/minimp3_ex.h | 1397 +++++++++++++++++++ src/main.c | 33 +- src/menu/actions.c | 51 +- src/menu/actions.h | 2 +- src/menu/assets.c | 37 + src/menu/menu.c | 83 +- src/menu/menu.h | 51 - src/menu/menu_state.h | 79 ++ src/menu/mp3player.c | 295 ++++ src/menu/mp3player.h | 31 + src/menu/path.c | 10 +- src/menu/path.h | 4 + src/menu/settings.c | 3 +- src/menu/settings.h | 2 +- src/menu/views/browser.c | 300 +++-- src/menu/views/credits.c | 4 +- src/menu/views/error.c | 3 +- src/menu/views/file_info.c | 16 +- src/menu/views/fragments/fragments.c | 66 + src/menu/views/fragments/fragments.h | 41 + src/menu/views/fragments/widgets.c | 55 + src/menu/views/init.c | 13 - src/menu/views/load.c | 50 +- src/menu/views/player.c | 117 ++ src/menu/views/startup.c | 23 + src/menu/{ => views}/views.h | 8 +- src/utils/fs.c | 20 + src/utils/fs.h | 1 + 38 files changed, 4544 insertions(+), 230 deletions(-) delete mode 100644 assets/.gitkeep create mode 100644 assets/FiraMono-Bold.ttf create mode 100644 src/libs/minimp3/minimp3.h create mode 100644 src/libs/minimp3/minimp3_ex.h create mode 100644 src/menu/assets.c create mode 100644 src/menu/menu_state.h create mode 100644 src/menu/mp3player.c create mode 100644 src/menu/mp3player.h create mode 100644 src/menu/views/fragments/fragments.c create mode 100644 src/menu/views/fragments/fragments.h create mode 100644 src/menu/views/fragments/widgets.c delete mode 100644 src/menu/views/init.c create mode 100644 src/menu/views/player.c create mode 100644 src/menu/views/startup.c rename src/menu/{ => views}/views.h (71%) diff --git a/Makefile b/Makefile index ef025402..80dd82dd 100644 --- a/Makefile +++ b/Makefile @@ -1,15 +1,16 @@ - PROJECT_NAME = N64FlashcartMenu .DEFAULT_GOAL := $(PROJECT_NAME) SOURCE_DIR = src +ASSETS_DIR = assets BUILD_DIR = build OUTPUT_DIR = output include $(N64_INST)/include/n64.mk N64_CFLAGS += -iquote $(SOURCE_DIR) +N64_LDFLAGS += --wrap asset_load SRCS = \ main.c \ @@ -21,7 +22,9 @@ SRCS = \ flashcart/sc64/sc64.c \ libs/toml/toml.c \ menu/actions.c \ + menu/assets.c \ menu/menu.c \ + menu/mp3player.c \ menu/path.c \ menu/rom_database.c \ menu/settings.c \ @@ -29,11 +32,25 @@ SRCS = \ menu/views/credits.c \ menu/views/error.c \ menu/views/file_info.c \ - menu/views/init.c \ + menu/views/fragments/fragments.c \ + menu/views/fragments/widgets.c \ menu/views/load.c \ + menu/views/player.c \ + menu/views/startup.c \ utils/fs.c -OBJS = $(addprefix $(BUILD_DIR)/, $(addsuffix .o,$(basename $(SRCS)))) +ASSETS = \ + FiraMono-Bold.ttf + +$(BUILD_DIR)/FiraMono-Bold.o: MKFONT_FLAGS+=--size 16 -r 20-7F -r 2000-206F -r 2190-21FF + +$(BUILD_DIR)/%.o: $(ASSETS_DIR)/%.ttf + @echo " [FONT] $@" + @$(N64_MKFONT) $(MKFONT_FLAGS) -o $(ASSETS_DIR) "$<" + @$(N64_OBJCOPY) -I binary -O elf32-bigmips -B mips4300 $(basename $<).font64 $@ + @rm $(basename $<).font64 + +OBJS = $(addprefix $(BUILD_DIR)/, $(addsuffix .o,$(basename $(SRCS) $(ASSETS)))) $(BUILD_DIR)/$(PROJECT_NAME).elf: $(OBJS) @@ -41,7 +58,7 @@ $(PROJECT_NAME).z64: N64_ROM_TITLE=$(PROJECT_NAME) $(PROJECT_NAME): $(PROJECT_NAME).z64 $(shell mkdir -p $(OUTPUT_DIR)) - $(shell mv $(PROJECT_NAME).z64 $(OUTPUT_DIR)) + $(shell mv $(PROJECT_NAME).z64 $(OUTPUT_DIR)) sc64_minify: $(PROJECT_NAME) $(shell python3 ./tools/sc64/minify.py $(BUILD_DIR)/$(PROJECT_NAME).elf $(OUTPUT_DIR)/$(PROJECT_NAME).z64 $(OUTPUT_DIR)/sc64menu.n64) diff --git a/assets/.gitkeep b/assets/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/assets/FiraMono-Bold.ttf b/assets/FiraMono-Bold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..23bc30f6c467b764fcdd9bc02f346921dc0e13f4 GIT binary patch literal 201708 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 Qc5ojydeinit(); } -flashcart_error_t flashcart_load_rom (char *rom_path) { +flashcart_error_t flashcart_load_rom (char *rom_path, bool byte_swap) { if ((rom_path == NULL) || (!file_exists(rom_path)) || (file_get_size(rom_path) < KiB(4))) { return FLASHCART_ERROR_ARGS; } - return flashcart->load_rom(rom_path); + return flashcart->load_rom(rom_path, byte_swap); } flashcart_error_t flashcart_load_save (char *save_path, flashcart_save_type_t save_type, bool save_writeback) { @@ -57,7 +57,9 @@ flashcart_error_t flashcart_load_save (char *save_path, flashcart_save_type_t sa return FLASHCART_ERROR_ARGS; } - flashcart->set_save_type(save_type); + if ((error = flashcart->set_save_type(save_type)) != FLASHCART_OK) { + return error; + } if ((save_path == NULL) || (save_type == FLASHCART_SAVE_TYPE_NONE)) { return FLASHCART_OK; diff --git a/src/flashcart/flashcart.h b/src/flashcart/flashcart.h index 5997fe94..6edc3e44 100644 --- a/src/flashcart/flashcart.h +++ b/src/flashcart/flashcart.h @@ -29,7 +29,7 @@ typedef enum { typedef struct { flashcart_error_t (*init) (void); flashcart_error_t (*deinit) (void); - flashcart_error_t (*load_rom) (char *rom_path); + flashcart_error_t (*load_rom) (char *rom_path, bool byte_swap); flashcart_error_t (*load_save) (char *save_path); flashcart_error_t (*set_save_type) (flashcart_save_type_t save_type); flashcart_error_t (*set_save_writeback) (uint32_t *sectors); @@ -38,7 +38,7 @@ typedef struct { flashcart_error_t flashcart_init (void); flashcart_error_t flashcart_deinit (void); -flashcart_error_t flashcart_load_rom (char *rom_path); +flashcart_error_t flashcart_load_rom (char *rom_path, bool byte_swap); flashcart_error_t flashcart_load_save (char *save_path, flashcart_save_type_t save_type, bool save_writeback); diff --git a/src/flashcart/sc64/sc64.c b/src/flashcart/sc64/sc64.c index f18b231d..291567d0 100644 --- a/src/flashcart/sc64/sc64.c +++ b/src/flashcart/sc64/sc64.c @@ -19,7 +19,7 @@ #define EEPROM_ADDRESS (0x1FFE2000) #define SUPPORTED_MAJOR_VERSION (2) -#define SUPPORTED_MINOR_VERSION (12) +#define SUPPORTED_MINOR_VERSION (16) static flashcart_error_t load_to_flash (FIL *fil, void *address, size_t size, UINT *br) { @@ -51,6 +51,11 @@ static flashcart_error_t load_to_flash (FIL *fil, void *address, size_t size, UI return FLASHCART_OK; } +static void load_cleanup (FIL *fil) { + sc64_sd_set_byte_swap(false); + f_close(fil); +} + static flashcart_error_t sc64_init (void) { uint16_t major; @@ -71,6 +76,13 @@ static flashcart_error_t sc64_init (void) { return FLASHCART_ERROR_OUTDATED; } + bool writeback_pending; + do { + if (sc64_writeback_pending(&writeback_pending) != SC64_OK) { + return FLASHCART_ERROR_INT; + } + } while (writeback_pending); + return FLASHCART_OK; } @@ -79,7 +91,7 @@ static flashcart_error_t sc64_deinit (void) { return FLASHCART_OK; } -static flashcart_error_t sc64_load_rom (char *rom_path) { +static flashcart_error_t sc64_load_rom (char *rom_path, bool byte_swap) { FIL fil; UINT br; @@ -99,6 +111,11 @@ static flashcart_error_t sc64_load_rom (char *rom_path) { return FLASHCART_ERROR_LOAD; } + if (sc64_sd_set_byte_swap(byte_swap) != SC64_OK) { + load_cleanup(&fil); + return FLASHCART_ERROR_INT; + } + bool shadow_enabled = (rom_size > (MiB(64) - KiB(128))); bool extended_enabled = (rom_size > MiB(64)); @@ -107,48 +124,53 @@ static flashcart_error_t sc64_load_rom (char *rom_path) { size_t extended_size = extended_enabled ? rom_size - MiB(64) : 0; if (f_read(&fil, (void *) (ROM_ADDRESS), sdram_size, &br) != FR_OK) { - f_close(&fil); + load_cleanup(&fil); return FLASHCART_ERROR_LOAD; } if (br != sdram_size) { - f_close(&fil); + load_cleanup(&fil); return FLASHCART_ERROR_LOAD; } if (sc64_set_config(CFG_ROM_SHADOW_ENABLE, shadow_enabled) != SC64_OK) { - f_close(&fil); + load_cleanup(&fil); return FLASHCART_ERROR_INT; } if (shadow_enabled) { flashcart_error_t error = load_to_flash(&fil, (void *) (SHADOW_ADDRESS), shadow_size, &br); if (error != FLASHCART_OK) { - f_close(&fil); + load_cleanup(&fil); return error; } if (br != shadow_size) { - f_close(&fil); + load_cleanup(&fil); return FLASHCART_ERROR_LOAD; } } if (sc64_set_config(CFG_ROM_EXTENDED_ENABLE, extended_enabled) != SC64_OK) { - f_close(&fil); + load_cleanup(&fil); return FLASHCART_ERROR_INT; } if (extended_enabled) { flashcart_error_t error = load_to_flash(&fil, (void *) (EXTENDED_ADDRESS), extended_size, &br); if (error != FLASHCART_OK) { - f_close(&fil); + load_cleanup(&fil); return error; } if (br != extended_size) { - f_close(&fil); + load_cleanup(&fil); return FLASHCART_ERROR_LOAD; } } + if (sc64_sd_set_byte_swap(false) != SC64_OK) { + load_cleanup(&fil); + return FLASHCART_ERROR_INT; + } + if (f_close(&fil) != FR_OK) { return FLASHCART_ERROR_LOAD; } @@ -186,7 +208,7 @@ static flashcart_error_t sc64_load_save (char *save_path) { return FLASHCART_ERROR_LOAD; } - size_t save_size = ALIGN(f_size(&fil), FS_SECTOR_SIZE); + size_t save_size = f_size(&fil); if (f_read(&fil, address, save_size, &br) != FR_OK) { f_close(&fil); @@ -224,7 +246,7 @@ static flashcart_error_t sc64_set_save_type (flashcart_save_type_t save_type) { type = SAVE_TYPE_SRAM_BANKED; break; case FLASHCART_SAVE_TYPE_SRAM_128K: - type = SAVE_TYPE_SRAM; + type = SAVE_TYPE_SRAM_128K; break; case FLASHCART_SAVE_TYPE_FLASHRAM: type = SAVE_TYPE_FLASHRAM; diff --git a/src/flashcart/sc64/sc64_internal.c b/src/flashcart/sc64/sc64_internal.c index b381ec26..4ac69d39 100644 --- a/src/flashcart/sc64/sc64_internal.c +++ b/src/flashcart/sc64/sc64_internal.c @@ -29,12 +29,23 @@ typedef enum { CMD_ID_VERSION_GET = 'V', CMD_ID_CONFIG_GET = 'c', CMD_ID_CONFIG_SET = 'C', + CMD_ID_SD_CARD_OP = 'i', + CMD_ID_WRITEBACK_PENDING = 'w', CMD_ID_WRITEBACK_SD_INFO = 'W', CMD_ID_FLASH_PROGRAM = 'K', CMD_ID_FLASH_WAIT_BUSY = 'p', CMD_ID_FLASH_ERASE_BLOCK = 'P', } sc64_cmd_id_t; +typedef enum { + SD_CARD_OP_DEINIT = 0, + SD_CARD_OP_INIT = 1, + SD_CARD_OP_GET_STATUS = 2, + SD_CARD_OP_GET_INFO = 3, + SD_CARD_OP_BYTE_SWAP_ON = 4, + SD_CARD_OP_BYTE_SWAP_OFF = 5, +} sc64_sd_card_op_t; + typedef struct { sc64_cmd_id_t id; uint32_t arg[2]; @@ -110,6 +121,21 @@ sc64_error_t sc64_set_config (sc64_cfg_t id, uint32_t value) { return sc64_execute_cmd(&cmd); } +sc64_error_t sc64_sd_set_byte_swap (bool enabled) { + sc64_cmd_t cmd = { + .id = CMD_ID_SD_CARD_OP, + .arg = { 0, enabled ? SD_CARD_OP_BYTE_SWAP_ON : SD_CARD_OP_BYTE_SWAP_OFF } + }; + return sc64_execute_cmd(&cmd); +} + +sc64_error_t sc64_writeback_pending (bool *pending) { + sc64_cmd_t cmd = { .id = CMD_ID_WRITEBACK_PENDING }; + sc64_error_t error = sc64_execute_cmd(&cmd); + *pending = (cmd.rsp[0] != 0); + return error; +} + sc64_error_t sc64_writeback_enable (void *address) { sc64_cmd_t cmd = { .id = CMD_ID_WRITEBACK_SD_INFO, .arg = { (uint32_t) (address) } }; return sc64_execute_cmd(&cmd); diff --git a/src/flashcart/sc64/sc64_internal.h b/src/flashcart/sc64/sc64_internal.h index a24918a9..182bbe37 100644 --- a/src/flashcart/sc64/sc64_internal.h +++ b/src/flashcart/sc64/sc64_internal.h @@ -52,6 +52,7 @@ typedef enum { SAVE_TYPE_SRAM, SAVE_TYPE_FLASHRAM, SAVE_TYPE_SRAM_BANKED, + SAVE_TYPE_SRAM_128K, } sc64_save_type_t; @@ -63,6 +64,8 @@ void sc64_write_data (void *src, void *dst, size_t length); sc64_error_t sc64_get_version (uint16_t *major, uint16_t *minor); sc64_error_t sc64_get_config (sc64_cfg_t cfg, void *value); sc64_error_t sc64_set_config (sc64_cfg_t cfg, uint32_t value); +sc64_error_t sc64_sd_set_byte_swap (bool enabled); +sc64_error_t sc64_writeback_pending (bool *pending); sc64_error_t sc64_writeback_enable (void *address); sc64_error_t sc64_flash_program (void *address, size_t length); sc64_error_t sc64_flash_wait_busy (void); diff --git a/src/libs/minimp3/minimp3.h b/src/libs/minimp3/minimp3.h new file mode 100644 index 00000000..3220ae1a --- /dev/null +++ b/src/libs/minimp3/minimp3.h @@ -0,0 +1,1865 @@ +#ifndef MINIMP3_H +#define MINIMP3_H +/* + https://github.com/lieff/minimp3 + To the extent possible under law, the author(s) have dedicated all copyright and related and neighboring rights to this software to the public domain worldwide. + This software is distributed without any warranty. + See . +*/ +#include + +#define MINIMP3_MAX_SAMPLES_PER_FRAME (1152*2) + +typedef struct +{ + int frame_bytes, frame_offset, channels, hz, layer, bitrate_kbps; +} mp3dec_frame_info_t; + +typedef struct +{ + float mdct_overlap[2][9*32], qmf_state[15*2*32]; + int reserv, free_format_bytes; + unsigned char header[4], reserv_buf[511]; +} mp3dec_t; + +#ifdef __cplusplus +extern "C" { +#endif /* __cplusplus */ + +void mp3dec_init(mp3dec_t *dec); +#ifndef MINIMP3_FLOAT_OUTPUT +typedef int16_t mp3d_sample_t; +#else /* MINIMP3_FLOAT_OUTPUT */ +typedef float mp3d_sample_t; +void mp3dec_f32_to_s16(const float *in, int16_t *out, int num_samples); +#endif /* MINIMP3_FLOAT_OUTPUT */ +int mp3dec_decode_frame(mp3dec_t *dec, const uint8_t *mp3, int mp3_bytes, mp3d_sample_t *pcm, mp3dec_frame_info_t *info); + +#ifdef __cplusplus +} +#endif /* __cplusplus */ + +#endif /* MINIMP3_H */ +#if defined(MINIMP3_IMPLEMENTATION) && !defined(_MINIMP3_IMPLEMENTATION_GUARD) +#define _MINIMP3_IMPLEMENTATION_GUARD + +#include +#include + +#define MAX_FREE_FORMAT_FRAME_SIZE 2304 /* more than ISO spec's */ +#ifndef MAX_FRAME_SYNC_MATCHES +#define MAX_FRAME_SYNC_MATCHES 10 +#endif /* MAX_FRAME_SYNC_MATCHES */ + +#define MAX_L3_FRAME_PAYLOAD_BYTES MAX_FREE_FORMAT_FRAME_SIZE /* MUST be >= 320000/8/32000*1152 = 1440 */ + +#define MAX_BITRESERVOIR_BYTES 511 +#define SHORT_BLOCK_TYPE 2 +#define STOP_BLOCK_TYPE 3 +#define MODE_MONO 3 +#define MODE_JOINT_STEREO 1 +#define HDR_SIZE 4 +#define HDR_IS_MONO(h) (((h[3]) & 0xC0) == 0xC0) +#define HDR_IS_MS_STEREO(h) (((h[3]) & 0xE0) == 0x60) +#define HDR_IS_FREE_FORMAT(h) (((h[2]) & 0xF0) == 0) +#define HDR_IS_CRC(h) (!((h[1]) & 1)) +#define HDR_TEST_PADDING(h) ((h[2]) & 0x2) +#define HDR_TEST_MPEG1(h) ((h[1]) & 0x8) +#define HDR_TEST_NOT_MPEG25(h) ((h[1]) & 0x10) +#define HDR_TEST_I_STEREO(h) ((h[3]) & 0x10) +#define HDR_TEST_MS_STEREO(h) ((h[3]) & 0x20) +#define HDR_GET_STEREO_MODE(h) (((h[3]) >> 6) & 3) +#define HDR_GET_STEREO_MODE_EXT(h) (((h[3]) >> 4) & 3) +#define HDR_GET_LAYER(h) (((h[1]) >> 1) & 3) +#define HDR_GET_BITRATE(h) ((h[2]) >> 4) +#define HDR_GET_SAMPLE_RATE(h) (((h[2]) >> 2) & 3) +#define HDR_GET_MY_SAMPLE_RATE(h) (HDR_GET_SAMPLE_RATE(h) + (((h[1] >> 3) & 1) + ((h[1] >> 4) & 1))*3) +#define HDR_IS_FRAME_576(h) ((h[1] & 14) == 2) +#define HDR_IS_LAYER_1(h) ((h[1] & 6) == 6) + +#define BITS_DEQUANTIZER_OUT -1 +#define MAX_SCF (255 + BITS_DEQUANTIZER_OUT*4 - 210) +#define MAX_SCFI ((MAX_SCF + 3) & ~3) + +#define MINIMP3_MIN(a, b) ((a) > (b) ? (b) : (a)) +#define MINIMP3_MAX(a, b) ((a) < (b) ? (b) : (a)) + +#if !defined(MINIMP3_NO_SIMD) + +#if !defined(MINIMP3_ONLY_SIMD) && (defined(_M_X64) || defined(__x86_64__) || defined(__aarch64__) || defined(_M_ARM64)) +/* x64 always have SSE2, arm64 always have neon, no need for generic code */ +#define MINIMP3_ONLY_SIMD +#endif /* SIMD checks... */ + +#if (defined(_MSC_VER) && (defined(_M_IX86) || defined(_M_X64))) || ((defined(__i386__) || defined(__x86_64__)) && defined(__SSE2__)) +#if defined(_MSC_VER) +#include +#endif /* defined(_MSC_VER) */ +#include +#define HAVE_SSE 1 +#define HAVE_SIMD 1 +#define VSTORE _mm_storeu_ps +#define VLD _mm_loadu_ps +#define VSET _mm_set1_ps +#define VADD _mm_add_ps +#define VSUB _mm_sub_ps +#define VMUL _mm_mul_ps +#define VMAC(a, x, y) _mm_add_ps(a, _mm_mul_ps(x, y)) +#define VMSB(a, x, y) _mm_sub_ps(a, _mm_mul_ps(x, y)) +#define VMUL_S(x, s) _mm_mul_ps(x, _mm_set1_ps(s)) +#define VREV(x) _mm_shuffle_ps(x, x, _MM_SHUFFLE(0, 1, 2, 3)) +typedef __m128 f4; +#if defined(_MSC_VER) || defined(MINIMP3_ONLY_SIMD) +#define minimp3_cpuid __cpuid +#else /* defined(_MSC_VER) || defined(MINIMP3_ONLY_SIMD) */ +static __inline__ __attribute__((always_inline)) void minimp3_cpuid(int CPUInfo[], const int InfoType) +{ +#if defined(__PIC__) + __asm__ __volatile__( +#if defined(__x86_64__) + "push %%rbx\n" + "cpuid\n" + "xchgl %%ebx, %1\n" + "pop %%rbx\n" +#else /* defined(__x86_64__) */ + "xchgl %%ebx, %1\n" + "cpuid\n" + "xchgl %%ebx, %1\n" +#endif /* defined(__x86_64__) */ + : "=a" (CPUInfo[0]), "=r" (CPUInfo[1]), "=c" (CPUInfo[2]), "=d" (CPUInfo[3]) + : "a" (InfoType)); +#else /* defined(__PIC__) */ + __asm__ __volatile__( + "cpuid" + : "=a" (CPUInfo[0]), "=b" (CPUInfo[1]), "=c" (CPUInfo[2]), "=d" (CPUInfo[3]) + : "a" (InfoType)); +#endif /* defined(__PIC__)*/ +} +#endif /* defined(_MSC_VER) || defined(MINIMP3_ONLY_SIMD) */ +static int have_simd(void) +{ +#ifdef MINIMP3_ONLY_SIMD + return 1; +#else /* MINIMP3_ONLY_SIMD */ + static int g_have_simd; + int CPUInfo[4]; +#ifdef MINIMP3_TEST + static int g_counter; + if (g_counter++ > 100) + return 0; +#endif /* MINIMP3_TEST */ + if (g_have_simd) + goto end; + minimp3_cpuid(CPUInfo, 0); + g_have_simd = 1; + if (CPUInfo[0] > 0) + { + minimp3_cpuid(CPUInfo, 1); + g_have_simd = (CPUInfo[3] & (1 << 26)) + 1; /* SSE2 */ + } +end: + return g_have_simd - 1; +#endif /* MINIMP3_ONLY_SIMD */ +} +#elif defined(__ARM_NEON) || defined(__aarch64__) || defined(_M_ARM64) +#include +#define HAVE_SSE 0 +#define HAVE_SIMD 1 +#define VSTORE vst1q_f32 +#define VLD vld1q_f32 +#define VSET vmovq_n_f32 +#define VADD vaddq_f32 +#define VSUB vsubq_f32 +#define VMUL vmulq_f32 +#define VMAC(a, x, y) vmlaq_f32(a, x, y) +#define VMSB(a, x, y) vmlsq_f32(a, x, y) +#define VMUL_S(x, s) vmulq_f32(x, vmovq_n_f32(s)) +#define VREV(x) vcombine_f32(vget_high_f32(vrev64q_f32(x)), vget_low_f32(vrev64q_f32(x))) +typedef float32x4_t f4; +static int have_simd() +{ /* TODO: detect neon for !MINIMP3_ONLY_SIMD */ + return 1; +} +#else /* SIMD checks... */ +#define HAVE_SSE 0 +#define HAVE_SIMD 0 +#ifdef MINIMP3_ONLY_SIMD +#error MINIMP3_ONLY_SIMD used, but SSE/NEON not enabled +#endif /* MINIMP3_ONLY_SIMD */ +#endif /* SIMD checks... */ +#else /* !defined(MINIMP3_NO_SIMD) */ +#define HAVE_SIMD 0 +#endif /* !defined(MINIMP3_NO_SIMD) */ + +#if defined(__ARM_ARCH) && (__ARM_ARCH >= 6) && !defined(__aarch64__) && !defined(_M_ARM64) +#define HAVE_ARMV6 1 +static __inline__ __attribute__((always_inline)) int32_t minimp3_clip_int16_arm(int32_t a) +{ + int32_t x = 0; + __asm__ ("ssat %0, #16, %1" : "=r"(x) : "r"(a)); + return x; +} +#else +#define HAVE_ARMV6 0 +#endif + +typedef struct +{ + const uint8_t *buf; + int pos, limit; +} bs_t; + +typedef struct +{ + float scf[3*64]; + uint8_t total_bands, stereo_bands, bitalloc[64], scfcod[64]; +} L12_scale_info; + +typedef struct +{ + uint8_t tab_offset, code_tab_width, band_count; +} L12_subband_alloc_t; + +typedef struct +{ + const uint8_t *sfbtab; + uint16_t part_23_length, big_values, scalefac_compress; + uint8_t global_gain, block_type, mixed_block_flag, n_long_sfb, n_short_sfb; + uint8_t table_select[3], region_count[3], subblock_gain[3]; + uint8_t preflag, scalefac_scale, count1_table, scfsi; +} L3_gr_info_t; + +typedef struct +{ + bs_t bs; + uint8_t maindata[MAX_BITRESERVOIR_BYTES + MAX_L3_FRAME_PAYLOAD_BYTES]; + L3_gr_info_t gr_info[4]; + float grbuf[2][576], scf[40], syn[18 + 15][2*32]; + uint8_t ist_pos[2][39]; +} mp3dec_scratch_t; + +static void bs_init(bs_t *bs, const uint8_t *data, int bytes) +{ + bs->buf = data; + bs->pos = 0; + bs->limit = bytes*8; +} + +static uint32_t get_bits(bs_t *bs, int n) +{ + uint32_t next, cache = 0, s = bs->pos & 7; + int shl = n + s; + const uint8_t *p = bs->buf + (bs->pos >> 3); + if ((bs->pos += n) > bs->limit) + return 0; + next = *p++ & (255 >> s); + while ((shl -= 8) > 0) + { + cache |= next << shl; + next = *p++; + } + return cache | (next >> -shl); +} + +static int hdr_valid(const uint8_t *h) +{ + return h[0] == 0xff && + ((h[1] & 0xF0) == 0xf0 || (h[1] & 0xFE) == 0xe2) && + (HDR_GET_LAYER(h) != 0) && + (HDR_GET_BITRATE(h) != 15) && + (HDR_GET_SAMPLE_RATE(h) != 3); +} + +static int hdr_compare(const uint8_t *h1, const uint8_t *h2) +{ + return hdr_valid(h2) && + ((h1[1] ^ h2[1]) & 0xFE) == 0 && + ((h1[2] ^ h2[2]) & 0x0C) == 0 && + !(HDR_IS_FREE_FORMAT(h1) ^ HDR_IS_FREE_FORMAT(h2)); +} + +static unsigned hdr_bitrate_kbps(const uint8_t *h) +{ + static const uint8_t halfrate[2][3][15] = { + { { 0,4,8,12,16,20,24,28,32,40,48,56,64,72,80 }, { 0,4,8,12,16,20,24,28,32,40,48,56,64,72,80 }, { 0,16,24,28,32,40,48,56,64,72,80,88,96,112,128 } }, + { { 0,16,20,24,28,32,40,48,56,64,80,96,112,128,160 }, { 0,16,24,28,32,40,48,56,64,80,96,112,128,160,192 }, { 0,16,32,48,64,80,96,112,128,144,160,176,192,208,224 } }, + }; + return 2*halfrate[!!HDR_TEST_MPEG1(h)][HDR_GET_LAYER(h) - 1][HDR_GET_BITRATE(h)]; +} + +static unsigned hdr_sample_rate_hz(const uint8_t *h) +{ + static const unsigned g_hz[3] = { 44100, 48000, 32000 }; + return g_hz[HDR_GET_SAMPLE_RATE(h)] >> (int)!HDR_TEST_MPEG1(h) >> (int)!HDR_TEST_NOT_MPEG25(h); +} + +static unsigned hdr_frame_samples(const uint8_t *h) +{ + return HDR_IS_LAYER_1(h) ? 384 : (1152 >> (int)HDR_IS_FRAME_576(h)); +} + +static int hdr_frame_bytes(const uint8_t *h, int free_format_size) +{ + int frame_bytes = hdr_frame_samples(h)*hdr_bitrate_kbps(h)*125/hdr_sample_rate_hz(h); + if (HDR_IS_LAYER_1(h)) + { + frame_bytes &= ~3; /* slot align */ + } + return frame_bytes ? frame_bytes : free_format_size; +} + +static int hdr_padding(const uint8_t *h) +{ + return HDR_TEST_PADDING(h) ? (HDR_IS_LAYER_1(h) ? 4 : 1) : 0; +} + +#ifndef MINIMP3_ONLY_MP3 +static const L12_subband_alloc_t *L12_subband_alloc_table(const uint8_t *hdr, L12_scale_info *sci) +{ + const L12_subband_alloc_t *alloc; + int mode = HDR_GET_STEREO_MODE(hdr); + int nbands, stereo_bands = (mode == MODE_MONO) ? 0 : (mode == MODE_JOINT_STEREO) ? (HDR_GET_STEREO_MODE_EXT(hdr) << 2) + 4 : 32; + + if (HDR_IS_LAYER_1(hdr)) + { + static const L12_subband_alloc_t g_alloc_L1[] = { { 76, 4, 32 } }; + alloc = g_alloc_L1; + nbands = 32; + } else if (!HDR_TEST_MPEG1(hdr)) + { + static const L12_subband_alloc_t g_alloc_L2M2[] = { { 60, 4, 4 }, { 44, 3, 7 }, { 44, 2, 19 } }; + alloc = g_alloc_L2M2; + nbands = 30; + } else + { + static const L12_subband_alloc_t g_alloc_L2M1[] = { { 0, 4, 3 }, { 16, 4, 8 }, { 32, 3, 12 }, { 40, 2, 7 } }; + int sample_rate_idx = HDR_GET_SAMPLE_RATE(hdr); + unsigned kbps = hdr_bitrate_kbps(hdr) >> (int)(mode != MODE_MONO); + if (!kbps) /* free-format */ + { + kbps = 192; + } + + alloc = g_alloc_L2M1; + nbands = 27; + if (kbps < 56) + { + static const L12_subband_alloc_t g_alloc_L2M1_lowrate[] = { { 44, 4, 2 }, { 44, 3, 10 } }; + alloc = g_alloc_L2M1_lowrate; + nbands = sample_rate_idx == 2 ? 12 : 8; + } else if (kbps >= 96 && sample_rate_idx != 1) + { + nbands = 30; + } + } + + sci->total_bands = (uint8_t)nbands; + sci->stereo_bands = (uint8_t)MINIMP3_MIN(stereo_bands, nbands); + + return alloc; +} + +static void L12_read_scalefactors(bs_t *bs, uint8_t *pba, uint8_t *scfcod, int bands, float *scf) +{ + static const float g_deq_L12[18*3] = { +#define DQ(x) 9.53674316e-07f/x, 7.56931807e-07f/x, 6.00777173e-07f/x + DQ(3),DQ(7),DQ(15),DQ(31),DQ(63),DQ(127),DQ(255),DQ(511),DQ(1023),DQ(2047),DQ(4095),DQ(8191),DQ(16383),DQ(32767),DQ(65535),DQ(3),DQ(5),DQ(9) + }; + int i, m; + for (i = 0; i < bands; i++) + { + float s = 0; + int ba = *pba++; + int mask = ba ? 4 + ((19 >> scfcod[i]) & 3) : 0; + for (m = 4; m; m >>= 1) + { + if (mask & m) + { + int b = get_bits(bs, 6); + s = g_deq_L12[ba*3 - 6 + b % 3]*(1 << 21 >> b/3); + } + *scf++ = s; + } + } +} + +static void L12_read_scale_info(const uint8_t *hdr, bs_t *bs, L12_scale_info *sci) +{ + static const uint8_t g_bitalloc_code_tab[] = { + 0,17, 3, 4, 5,6,7, 8,9,10,11,12,13,14,15,16, + 0,17,18, 3,19,4,5, 6,7, 8, 9,10,11,12,13,16, + 0,17,18, 3,19,4,5,16, + 0,17,18,16, + 0,17,18,19, 4,5,6, 7,8, 9,10,11,12,13,14,15, + 0,17,18, 3,19,4,5, 6,7, 8, 9,10,11,12,13,14, + 0, 2, 3, 4, 5,6,7, 8,9,10,11,12,13,14,15,16 + }; + const L12_subband_alloc_t *subband_alloc = L12_subband_alloc_table(hdr, sci); + + int i, k = 0, ba_bits = 0; + const uint8_t *ba_code_tab = g_bitalloc_code_tab; + + for (i = 0; i < sci->total_bands; i++) + { + uint8_t ba; + if (i == k) + { + k += subband_alloc->band_count; + ba_bits = subband_alloc->code_tab_width; + ba_code_tab = g_bitalloc_code_tab + subband_alloc->tab_offset; + subband_alloc++; + } + ba = ba_code_tab[get_bits(bs, ba_bits)]; + sci->bitalloc[2*i] = ba; + if (i < sci->stereo_bands) + { + ba = ba_code_tab[get_bits(bs, ba_bits)]; + } + sci->bitalloc[2*i + 1] = sci->stereo_bands ? ba : 0; + } + + for (i = 0; i < 2*sci->total_bands; i++) + { + sci->scfcod[i] = sci->bitalloc[i] ? HDR_IS_LAYER_1(hdr) ? 2 : get_bits(bs, 2) : 6; + } + + L12_read_scalefactors(bs, sci->bitalloc, sci->scfcod, sci->total_bands*2, sci->scf); + + for (i = sci->stereo_bands; i < sci->total_bands; i++) + { + sci->bitalloc[2*i + 1] = 0; + } +} + +static int L12_dequantize_granule(float *grbuf, bs_t *bs, L12_scale_info *sci, int group_size) +{ + int i, j, k, choff = 576; + for (j = 0; j < 4; j++) + { + float *dst = grbuf + group_size*j; + for (i = 0; i < 2*sci->total_bands; i++) + { + int ba = sci->bitalloc[i]; + if (ba != 0) + { + if (ba < 17) + { + int half = (1 << (ba - 1)) - 1; + for (k = 0; k < group_size; k++) + { + dst[k] = (float)((int)get_bits(bs, ba) - half); + } + } else + { + unsigned mod = (2 << (ba - 17)) + 1; /* 3, 5, 9 */ + unsigned code = get_bits(bs, mod + 2 - (mod >> 3)); /* 5, 7, 10 */ + for (k = 0; k < group_size; k++, code /= mod) + { + dst[k] = (float)((int)(code % mod - mod/2)); + } + } + } + dst += choff; + choff = 18 - choff; + } + } + return group_size*4; +} + +static void L12_apply_scf_384(L12_scale_info *sci, const float *scf, float *dst) +{ + int i, k; + memcpy(dst + 576 + sci->stereo_bands*18, dst + sci->stereo_bands*18, (sci->total_bands - sci->stereo_bands)*18*sizeof(float)); + for (i = 0; i < sci->total_bands; i++, dst += 18, scf += 6) + { + for (k = 0; k < 12; k++) + { + dst[k + 0] *= scf[0]; + dst[k + 576] *= scf[3]; + } + } +} +#endif /* MINIMP3_ONLY_MP3 */ + +static int L3_read_side_info(bs_t *bs, L3_gr_info_t *gr, const uint8_t *hdr) +{ + static const uint8_t g_scf_long[8][23] = { + { 6,6,6,6,6,6,8,10,12,14,16,20,24,28,32,38,46,52,60,68,58,54,0 }, + { 12,12,12,12,12,12,16,20,24,28,32,40,48,56,64,76,90,2,2,2,2,2,0 }, + { 6,6,6,6,6,6,8,10,12,14,16,20,24,28,32,38,46,52,60,68,58,54,0 }, + { 6,6,6,6,6,6,8,10,12,14,16,18,22,26,32,38,46,54,62,70,76,36,0 }, + { 6,6,6,6,6,6,8,10,12,14,16,20,24,28,32,38,46,52,60,68,58,54,0 }, + { 4,4,4,4,4,4,6,6,8,8,10,12,16,20,24,28,34,42,50,54,76,158,0 }, + { 4,4,4,4,4,4,6,6,6,8,10,12,16,18,22,28,34,40,46,54,54,192,0 }, + { 4,4,4,4,4,4,6,6,8,10,12,16,20,24,30,38,46,56,68,84,102,26,0 } + }; + static const uint8_t g_scf_short[8][40] = { + { 4,4,4,4,4,4,4,4,4,6,6,6,8,8,8,10,10,10,12,12,12,14,14,14,18,18,18,24,24,24,30,30,30,40,40,40,18,18,18,0 }, + { 8,8,8,8,8,8,8,8,8,12,12,12,16,16,16,20,20,20,24,24,24,28,28,28,36,36,36,2,2,2,2,2,2,2,2,2,26,26,26,0 }, + { 4,4,4,4,4,4,4,4,4,6,6,6,6,6,6,8,8,8,10,10,10,14,14,14,18,18,18,26,26,26,32,32,32,42,42,42,18,18,18,0 }, + { 4,4,4,4,4,4,4,4,4,6,6,6,8,8,8,10,10,10,12,12,12,14,14,14,18,18,18,24,24,24,32,32,32,44,44,44,12,12,12,0 }, + { 4,4,4,4,4,4,4,4,4,6,6,6,8,8,8,10,10,10,12,12,12,14,14,14,18,18,18,24,24,24,30,30,30,40,40,40,18,18,18,0 }, + { 4,4,4,4,4,4,4,4,4,4,4,4,6,6,6,8,8,8,10,10,10,12,12,12,14,14,14,18,18,18,22,22,22,30,30,30,56,56,56,0 }, + { 4,4,4,4,4,4,4,4,4,4,4,4,6,6,6,6,6,6,10,10,10,12,12,12,14,14,14,16,16,16,20,20,20,26,26,26,66,66,66,0 }, + { 4,4,4,4,4,4,4,4,4,4,4,4,6,6,6,8,8,8,12,12,12,16,16,16,20,20,20,26,26,26,34,34,34,42,42,42,12,12,12,0 } + }; + static const uint8_t g_scf_mixed[8][40] = { + { 6,6,6,6,6,6,6,6,6,8,8,8,10,10,10,12,12,12,14,14,14,18,18,18,24,24,24,30,30,30,40,40,40,18,18,18,0 }, + { 12,12,12,4,4,4,8,8,8,12,12,12,16,16,16,20,20,20,24,24,24,28,28,28,36,36,36,2,2,2,2,2,2,2,2,2,26,26,26,0 }, + { 6,6,6,6,6,6,6,6,6,6,6,6,8,8,8,10,10,10,14,14,14,18,18,18,26,26,26,32,32,32,42,42,42,18,18,18,0 }, + { 6,6,6,6,6,6,6,6,6,8,8,8,10,10,10,12,12,12,14,14,14,18,18,18,24,24,24,32,32,32,44,44,44,12,12,12,0 }, + { 6,6,6,6,6,6,6,6,6,8,8,8,10,10,10,12,12,12,14,14,14,18,18,18,24,24,24,30,30,30,40,40,40,18,18,18,0 }, + { 4,4,4,4,4,4,6,6,4,4,4,6,6,6,8,8,8,10,10,10,12,12,12,14,14,14,18,18,18,22,22,22,30,30,30,56,56,56,0 }, + { 4,4,4,4,4,4,6,6,4,4,4,6,6,6,6,6,6,10,10,10,12,12,12,14,14,14,16,16,16,20,20,20,26,26,26,66,66,66,0 }, + { 4,4,4,4,4,4,6,6,4,4,4,6,6,6,8,8,8,12,12,12,16,16,16,20,20,20,26,26,26,34,34,34,42,42,42,12,12,12,0 } + }; + + unsigned tables, scfsi = 0; + int main_data_begin, part_23_sum = 0; + int sr_idx = HDR_GET_MY_SAMPLE_RATE(hdr); sr_idx -= (sr_idx != 0); + int gr_count = HDR_IS_MONO(hdr) ? 1 : 2; + + if (HDR_TEST_MPEG1(hdr)) + { + gr_count *= 2; + main_data_begin = get_bits(bs, 9); + scfsi = get_bits(bs, 7 + gr_count); + } else + { + main_data_begin = get_bits(bs, 8 + gr_count) >> gr_count; + } + + do + { + if (HDR_IS_MONO(hdr)) + { + scfsi <<= 4; + } + gr->part_23_length = (uint16_t)get_bits(bs, 12); + part_23_sum += gr->part_23_length; + gr->big_values = (uint16_t)get_bits(bs, 9); + if (gr->big_values > 288) + { + return -1; + } + gr->global_gain = (uint8_t)get_bits(bs, 8); + gr->scalefac_compress = (uint16_t)get_bits(bs, HDR_TEST_MPEG1(hdr) ? 4 : 9); + gr->sfbtab = g_scf_long[sr_idx]; + gr->n_long_sfb = 22; + gr->n_short_sfb = 0; + if (get_bits(bs, 1)) + { + gr->block_type = (uint8_t)get_bits(bs, 2); + if (!gr->block_type) + { + return -1; + } + gr->mixed_block_flag = (uint8_t)get_bits(bs, 1); + gr->region_count[0] = 7; + gr->region_count[1] = 255; + if (gr->block_type == SHORT_BLOCK_TYPE) + { + scfsi &= 0x0F0F; + if (!gr->mixed_block_flag) + { + gr->region_count[0] = 8; + gr->sfbtab = g_scf_short[sr_idx]; + gr->n_long_sfb = 0; + gr->n_short_sfb = 39; + } else + { + gr->sfbtab = g_scf_mixed[sr_idx]; + gr->n_long_sfb = HDR_TEST_MPEG1(hdr) ? 8 : 6; + gr->n_short_sfb = 30; + } + } + tables = get_bits(bs, 10); + tables <<= 5; + gr->subblock_gain[0] = (uint8_t)get_bits(bs, 3); + gr->subblock_gain[1] = (uint8_t)get_bits(bs, 3); + gr->subblock_gain[2] = (uint8_t)get_bits(bs, 3); + } else + { + gr->block_type = 0; + gr->mixed_block_flag = 0; + tables = get_bits(bs, 15); + gr->region_count[0] = (uint8_t)get_bits(bs, 4); + gr->region_count[1] = (uint8_t)get_bits(bs, 3); + gr->region_count[2] = 255; + } + gr->table_select[0] = (uint8_t)(tables >> 10); + gr->table_select[1] = (uint8_t)((tables >> 5) & 31); + gr->table_select[2] = (uint8_t)((tables) & 31); + gr->preflag = HDR_TEST_MPEG1(hdr) ? get_bits(bs, 1) : (gr->scalefac_compress >= 500); + gr->scalefac_scale = (uint8_t)get_bits(bs, 1); + gr->count1_table = (uint8_t)get_bits(bs, 1); + gr->scfsi = (uint8_t)((scfsi >> 12) & 15); + scfsi <<= 4; + gr++; + } while(--gr_count); + + if (part_23_sum + bs->pos > bs->limit + main_data_begin*8) + { + return -1; + } + + return main_data_begin; +} + +static void L3_read_scalefactors(uint8_t *scf, uint8_t *ist_pos, const uint8_t *scf_size, const uint8_t *scf_count, bs_t *bitbuf, int scfsi) +{ + int i, k; + for (i = 0; i < 4 && scf_count[i]; i++, scfsi *= 2) + { + int cnt = scf_count[i]; + if (scfsi & 8) + { + memcpy(scf, ist_pos, cnt); + } else + { + int bits = scf_size[i]; + if (!bits) + { + memset(scf, 0, cnt); + memset(ist_pos, 0, cnt); + } else + { + int max_scf = (scfsi < 0) ? (1 << bits) - 1 : -1; + for (k = 0; k < cnt; k++) + { + int s = get_bits(bitbuf, bits); + ist_pos[k] = (s == max_scf ? -1 : s); + scf[k] = s; + } + } + } + ist_pos += cnt; + scf += cnt; + } + scf[0] = scf[1] = scf[2] = 0; +} + +static float L3_ldexp_q2(float y, int exp_q2) +{ + static const float g_expfrac[4] = { 9.31322575e-10f,7.83145814e-10f,6.58544508e-10f,5.53767716e-10f }; + int e; + do + { + e = MINIMP3_MIN(30*4, exp_q2); + y *= g_expfrac[e & 3]*(1 << 30 >> (e >> 2)); + } while ((exp_q2 -= e) > 0); + return y; +} + +static void L3_decode_scalefactors(const uint8_t *hdr, uint8_t *ist_pos, bs_t *bs, const L3_gr_info_t *gr, float *scf, int ch) +{ + static const uint8_t g_scf_partitions[3][28] = { + { 6,5,5, 5,6,5,5,5,6,5, 7,3,11,10,0,0, 7, 7, 7,0, 6, 6,6,3, 8, 8,5,0 }, + { 8,9,6,12,6,9,9,9,6,9,12,6,15,18,0,0, 6,15,12,0, 6,12,9,6, 6,18,9,0 }, + { 9,9,6,12,9,9,9,9,9,9,12,6,18,18,0,0,12,12,12,0,12, 9,9,6,15,12,9,0 } + }; + const uint8_t *scf_partition = g_scf_partitions[!!gr->n_short_sfb + !gr->n_long_sfb]; + uint8_t scf_size[4], iscf[40]; + int i, scf_shift = gr->scalefac_scale + 1, gain_exp, scfsi = gr->scfsi; + float gain; + + if (HDR_TEST_MPEG1(hdr)) + { + static const uint8_t g_scfc_decode[16] = { 0,1,2,3, 12,5,6,7, 9,10,11,13, 14,15,18,19 }; + int part = g_scfc_decode[gr->scalefac_compress]; + scf_size[1] = scf_size[0] = (uint8_t)(part >> 2); + scf_size[3] = scf_size[2] = (uint8_t)(part & 3); + } else + { + static const uint8_t g_mod[6*4] = { 5,5,4,4,5,5,4,1,4,3,1,1,5,6,6,1,4,4,4,1,4,3,1,1 }; + int k, modprod, sfc, ist = HDR_TEST_I_STEREO(hdr) && ch; + sfc = gr->scalefac_compress >> ist; + for (k = ist*3*4; sfc >= 0; sfc -= modprod, k += 4) + { + for (modprod = 1, i = 3; i >= 0; i--) + { + scf_size[i] = (uint8_t)(sfc / modprod % g_mod[k + i]); + modprod *= g_mod[k + i]; + } + } + scf_partition += k; + scfsi = -16; + } + L3_read_scalefactors(iscf, ist_pos, scf_size, scf_partition, bs, scfsi); + + if (gr->n_short_sfb) + { + int sh = 3 - scf_shift; + for (i = 0; i < gr->n_short_sfb; i += 3) + { + iscf[gr->n_long_sfb + i + 0] += gr->subblock_gain[0] << sh; + iscf[gr->n_long_sfb + i + 1] += gr->subblock_gain[1] << sh; + iscf[gr->n_long_sfb + i + 2] += gr->subblock_gain[2] << sh; + } + } else if (gr->preflag) + { + static const uint8_t g_preamp[10] = { 1,1,1,1,2,2,3,3,3,2 }; + for (i = 0; i < 10; i++) + { + iscf[11 + i] += g_preamp[i]; + } + } + + gain_exp = gr->global_gain + BITS_DEQUANTIZER_OUT*4 - 210 - (HDR_IS_MS_STEREO(hdr) ? 2 : 0); + gain = L3_ldexp_q2(1 << (MAX_SCFI/4), MAX_SCFI - gain_exp); + for (i = 0; i < (int)(gr->n_long_sfb + gr->n_short_sfb); i++) + { + scf[i] = L3_ldexp_q2(gain, iscf[i] << scf_shift); + } +} + +static const float g_pow43[129 + 16] = { + 0,-1,-2.519842f,-4.326749f,-6.349604f,-8.549880f,-10.902724f,-13.390518f,-16.000000f,-18.720754f,-21.544347f,-24.463781f,-27.473142f,-30.567351f,-33.741992f,-36.993181f, + 0,1,2.519842f,4.326749f,6.349604f,8.549880f,10.902724f,13.390518f,16.000000f,18.720754f,21.544347f,24.463781f,27.473142f,30.567351f,33.741992f,36.993181f,40.317474f,43.711787f,47.173345f,50.699631f,54.288352f,57.937408f,61.644865f,65.408941f,69.227979f,73.100443f,77.024898f,81.000000f,85.024491f,89.097188f,93.216975f,97.382800f,101.593667f,105.848633f,110.146801f,114.487321f,118.869381f,123.292209f,127.755065f,132.257246f,136.798076f,141.376907f,145.993119f,150.646117f,155.335327f,160.060199f,164.820202f,169.614826f,174.443577f,179.305980f,184.201575f,189.129918f,194.090580f,199.083145f,204.107210f,209.162385f,214.248292f,219.364564f,224.510845f,229.686789f,234.892058f,240.126328f,245.389280f,250.680604f,256.000000f,261.347174f,266.721841f,272.123723f,277.552547f,283.008049f,288.489971f,293.998060f,299.532071f,305.091761f,310.676898f,316.287249f,321.922592f,327.582707f,333.267377f,338.976394f,344.709550f,350.466646f,356.247482f,362.051866f,367.879608f,373.730522f,379.604427f,385.501143f,391.420496f,397.362314f,403.326427f,409.312672f,415.320884f,421.350905f,427.402579f,433.475750f,439.570269f,445.685987f,451.822757f,457.980436f,464.158883f,470.357960f,476.577530f,482.817459f,489.077615f,495.357868f,501.658090f,507.978156f,514.317941f,520.677324f,527.056184f,533.454404f,539.871867f,546.308458f,552.764065f,559.238575f,565.731879f,572.243870f,578.774440f,585.323483f,591.890898f,598.476581f,605.080431f,611.702349f,618.342238f,625.000000f,631.675540f,638.368763f,645.079578f +}; + +static float L3_pow_43(int x) +{ + float frac; + int sign, mult = 256; + + if (x < 129) + { + return g_pow43[16 + x]; + } + + if (x < 1024) + { + mult = 16; + x <<= 3; + } + + sign = 2*x & 64; + frac = (float)((x & 63) - sign) / ((x & ~63) + sign); + return g_pow43[16 + ((x + sign) >> 6)]*(1.f + frac*((4.f/3) + frac*(2.f/9)))*mult; +} + +static void L3_huffman(float *dst, bs_t *bs, const L3_gr_info_t *gr_info, const float *scf, int layer3gr_limit) +{ + static const int16_t tabs[] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 785,785,785,785,784,784,784,784,513,513,513,513,513,513,513,513,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256, + -255,1313,1298,1282,785,785,785,785,784,784,784,784,769,769,769,769,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,290,288, + -255,1313,1298,1282,769,769,769,769,529,529,529,529,529,529,529,529,528,528,528,528,528,528,528,528,512,512,512,512,512,512,512,512,290,288, + -253,-318,-351,-367,785,785,785,785,784,784,784,784,769,769,769,769,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,819,818,547,547,275,275,275,275,561,560,515,546,289,274,288,258, + -254,-287,1329,1299,1314,1312,1057,1057,1042,1042,1026,1026,784,784,784,784,529,529,529,529,529,529,529,529,769,769,769,769,768,768,768,768,563,560,306,306,291,259, + -252,-413,-477,-542,1298,-575,1041,1041,784,784,784,784,769,769,769,769,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,-383,-399,1107,1092,1106,1061,849,849,789,789,1104,1091,773,773,1076,1075,341,340,325,309,834,804,577,577,532,532,516,516,832,818,803,816,561,561,531,531,515,546,289,289,288,258, + -252,-429,-493,-559,1057,1057,1042,1042,529,529,529,529,529,529,529,529,784,784,784,784,769,769,769,769,512,512,512,512,512,512,512,512,-382,1077,-415,1106,1061,1104,849,849,789,789,1091,1076,1029,1075,834,834,597,581,340,340,339,324,804,833,532,532,832,772,818,803,817,787,816,771,290,290,290,290,288,258, + -253,-349,-414,-447,-463,1329,1299,-479,1314,1312,1057,1057,1042,1042,1026,1026,785,785,785,785,784,784,784,784,769,769,769,769,768,768,768,768,-319,851,821,-335,836,850,805,849,341,340,325,336,533,533,579,579,564,564,773,832,578,548,563,516,321,276,306,291,304,259, + -251,-572,-733,-830,-863,-879,1041,1041,784,784,784,784,769,769,769,769,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,-511,-527,-543,1396,1351,1381,1366,1395,1335,1380,-559,1334,1138,1138,1063,1063,1350,1392,1031,1031,1062,1062,1364,1363,1120,1120,1333,1348,881,881,881,881,375,374,359,373,343,358,341,325,791,791,1123,1122,-703,1105,1045,-719,865,865,790,790,774,774,1104,1029,338,293,323,308,-799,-815,833,788,772,818,803,816,322,292,307,320,561,531,515,546,289,274,288,258, + -251,-525,-605,-685,-765,-831,-846,1298,1057,1057,1312,1282,785,785,785,785,784,784,784,784,769,769,769,769,512,512,512,512,512,512,512,512,1399,1398,1383,1367,1382,1396,1351,-511,1381,1366,1139,1139,1079,1079,1124,1124,1364,1349,1363,1333,882,882,882,882,807,807,807,807,1094,1094,1136,1136,373,341,535,535,881,775,867,822,774,-591,324,338,-671,849,550,550,866,864,609,609,293,336,534,534,789,835,773,-751,834,804,308,307,833,788,832,772,562,562,547,547,305,275,560,515,290,290, + -252,-397,-477,-557,-622,-653,-719,-735,-750,1329,1299,1314,1057,1057,1042,1042,1312,1282,1024,1024,785,785,785,785,784,784,784,784,769,769,769,769,-383,1127,1141,1111,1126,1140,1095,1110,869,869,883,883,1079,1109,882,882,375,374,807,868,838,881,791,-463,867,822,368,263,852,837,836,-543,610,610,550,550,352,336,534,534,865,774,851,821,850,805,593,533,579,564,773,832,578,578,548,548,577,577,307,276,306,291,516,560,259,259, + -250,-2107,-2507,-2764,-2909,-2974,-3007,-3023,1041,1041,1040,1040,769,769,769,769,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,-767,-1052,-1213,-1277,-1358,-1405,-1469,-1535,-1550,-1582,-1614,-1647,-1662,-1694,-1726,-1759,-1774,-1807,-1822,-1854,-1886,1565,-1919,-1935,-1951,-1967,1731,1730,1580,1717,-1983,1729,1564,-1999,1548,-2015,-2031,1715,1595,-2047,1714,-2063,1610,-2079,1609,-2095,1323,1323,1457,1457,1307,1307,1712,1547,1641,1700,1699,1594,1685,1625,1442,1442,1322,1322,-780,-973,-910,1279,1278,1277,1262,1276,1261,1275,1215,1260,1229,-959,974,974,989,989,-943,735,478,478,495,463,506,414,-1039,1003,958,1017,927,942,987,957,431,476,1272,1167,1228,-1183,1256,-1199,895,895,941,941,1242,1227,1212,1135,1014,1014,490,489,503,487,910,1013,985,925,863,894,970,955,1012,847,-1343,831,755,755,984,909,428,366,754,559,-1391,752,486,457,924,997,698,698,983,893,740,740,908,877,739,739,667,667,953,938,497,287,271,271,683,606,590,712,726,574,302,302,738,736,481,286,526,725,605,711,636,724,696,651,589,681,666,710,364,467,573,695,466,466,301,465,379,379,709,604,665,679,316,316,634,633,436,436,464,269,424,394,452,332,438,363,347,408,393,448,331,422,362,407,392,421,346,406,391,376,375,359,1441,1306,-2367,1290,-2383,1337,-2399,-2415,1426,1321,-2431,1411,1336,-2447,-2463,-2479,1169,1169,1049,1049,1424,1289,1412,1352,1319,-2495,1154,1154,1064,1064,1153,1153,416,390,360,404,403,389,344,374,373,343,358,372,327,357,342,311,356,326,1395,1394,1137,1137,1047,1047,1365,1392,1287,1379,1334,1364,1349,1378,1318,1363,792,792,792,792,1152,1152,1032,1032,1121,1121,1046,1046,1120,1120,1030,1030,-2895,1106,1061,1104,849,849,789,789,1091,1076,1029,1090,1060,1075,833,833,309,324,532,532,832,772,818,803,561,561,531,560,515,546,289,274,288,258, + -250,-1179,-1579,-1836,-1996,-2124,-2253,-2333,-2413,-2477,-2542,-2574,-2607,-2622,-2655,1314,1313,1298,1312,1282,785,785,785,785,1040,1040,1025,1025,768,768,768,768,-766,-798,-830,-862,-895,-911,-927,-943,-959,-975,-991,-1007,-1023,-1039,-1055,-1070,1724,1647,-1103,-1119,1631,1767,1662,1738,1708,1723,-1135,1780,1615,1779,1599,1677,1646,1778,1583,-1151,1777,1567,1737,1692,1765,1722,1707,1630,1751,1661,1764,1614,1736,1676,1763,1750,1645,1598,1721,1691,1762,1706,1582,1761,1566,-1167,1749,1629,767,766,751,765,494,494,735,764,719,749,734,763,447,447,748,718,477,506,431,491,446,476,461,505,415,430,475,445,504,399,460,489,414,503,383,474,429,459,502,502,746,752,488,398,501,473,413,472,486,271,480,270,-1439,-1455,1357,-1471,-1487,-1503,1341,1325,-1519,1489,1463,1403,1309,-1535,1372,1448,1418,1476,1356,1462,1387,-1551,1475,1340,1447,1402,1386,-1567,1068,1068,1474,1461,455,380,468,440,395,425,410,454,364,467,466,464,453,269,409,448,268,432,1371,1473,1432,1417,1308,1460,1355,1446,1459,1431,1083,1083,1401,1416,1458,1445,1067,1067,1370,1457,1051,1051,1291,1430,1385,1444,1354,1415,1400,1443,1082,1082,1173,1113,1186,1066,1185,1050,-1967,1158,1128,1172,1097,1171,1081,-1983,1157,1112,416,266,375,400,1170,1142,1127,1065,793,793,1169,1033,1156,1096,1141,1111,1155,1080,1126,1140,898,898,808,808,897,897,792,792,1095,1152,1032,1125,1110,1139,1079,1124,882,807,838,881,853,791,-2319,867,368,263,822,852,837,866,806,865,-2399,851,352,262,534,534,821,836,594,594,549,549,593,593,533,533,848,773,579,579,564,578,548,563,276,276,577,576,306,291,516,560,305,305,275,259, + -251,-892,-2058,-2620,-2828,-2957,-3023,-3039,1041,1041,1040,1040,769,769,769,769,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,-511,-527,-543,-559,1530,-575,-591,1528,1527,1407,1526,1391,1023,1023,1023,1023,1525,1375,1268,1268,1103,1103,1087,1087,1039,1039,1523,-604,815,815,815,815,510,495,509,479,508,463,507,447,431,505,415,399,-734,-782,1262,-815,1259,1244,-831,1258,1228,-847,-863,1196,-879,1253,987,987,748,-767,493,493,462,477,414,414,686,669,478,446,461,445,474,429,487,458,412,471,1266,1264,1009,1009,799,799,-1019,-1276,-1452,-1581,-1677,-1757,-1821,-1886,-1933,-1997,1257,1257,1483,1468,1512,1422,1497,1406,1467,1496,1421,1510,1134,1134,1225,1225,1466,1451,1374,1405,1252,1252,1358,1480,1164,1164,1251,1251,1238,1238,1389,1465,-1407,1054,1101,-1423,1207,-1439,830,830,1248,1038,1237,1117,1223,1148,1236,1208,411,426,395,410,379,269,1193,1222,1132,1235,1221,1116,976,976,1192,1162,1177,1220,1131,1191,963,963,-1647,961,780,-1663,558,558,994,993,437,408,393,407,829,978,813,797,947,-1743,721,721,377,392,844,950,828,890,706,706,812,859,796,960,948,843,934,874,571,571,-1919,690,555,689,421,346,539,539,944,779,918,873,932,842,903,888,570,570,931,917,674,674,-2575,1562,-2591,1609,-2607,1654,1322,1322,1441,1441,1696,1546,1683,1593,1669,1624,1426,1426,1321,1321,1639,1680,1425,1425,1305,1305,1545,1668,1608,1623,1667,1592,1638,1666,1320,1320,1652,1607,1409,1409,1304,1304,1288,1288,1664,1637,1395,1395,1335,1335,1622,1636,1394,1394,1319,1319,1606,1621,1392,1392,1137,1137,1137,1137,345,390,360,375,404,373,1047,-2751,-2767,-2783,1062,1121,1046,-2799,1077,-2815,1106,1061,789,789,1105,1104,263,355,310,340,325,354,352,262,339,324,1091,1076,1029,1090,1060,1075,833,833,788,788,1088,1028,818,818,803,803,561,561,531,531,816,771,546,546,289,274,288,258, + -253,-317,-381,-446,-478,-509,1279,1279,-811,-1179,-1451,-1756,-1900,-2028,-2189,-2253,-2333,-2414,-2445,-2511,-2526,1313,1298,-2559,1041,1041,1040,1040,1025,1025,1024,1024,1022,1007,1021,991,1020,975,1019,959,687,687,1018,1017,671,671,655,655,1016,1015,639,639,758,758,623,623,757,607,756,591,755,575,754,559,543,543,1009,783,-575,-621,-685,-749,496,-590,750,749,734,748,974,989,1003,958,988,973,1002,942,987,957,972,1001,926,986,941,971,956,1000,910,985,925,999,894,970,-1071,-1087,-1102,1390,-1135,1436,1509,1451,1374,-1151,1405,1358,1480,1420,-1167,1507,1494,1389,1342,1465,1435,1450,1326,1505,1310,1493,1373,1479,1404,1492,1464,1419,428,443,472,397,736,526,464,464,486,457,442,471,484,482,1357,1449,1434,1478,1388,1491,1341,1490,1325,1489,1463,1403,1309,1477,1372,1448,1418,1433,1476,1356,1462,1387,-1439,1475,1340,1447,1402,1474,1324,1461,1371,1473,269,448,1432,1417,1308,1460,-1711,1459,-1727,1441,1099,1099,1446,1386,1431,1401,-1743,1289,1083,1083,1160,1160,1458,1445,1067,1067,1370,1457,1307,1430,1129,1129,1098,1098,268,432,267,416,266,400,-1887,1144,1187,1082,1173,1113,1186,1066,1050,1158,1128,1143,1172,1097,1171,1081,420,391,1157,1112,1170,1142,1127,1065,1169,1049,1156,1096,1141,1111,1155,1080,1126,1154,1064,1153,1140,1095,1048,-2159,1125,1110,1137,-2175,823,823,1139,1138,807,807,384,264,368,263,868,838,853,791,867,822,852,837,866,806,865,790,-2319,851,821,836,352,262,850,805,849,-2399,533,533,835,820,336,261,578,548,563,577,532,532,832,772,562,562,547,547,305,275,560,515,290,290,288,258 }; + static const uint8_t tab32[] = { 130,162,193,209,44,28,76,140,9,9,9,9,9,9,9,9,190,254,222,238,126,94,157,157,109,61,173,205 }; + static const uint8_t tab33[] = { 252,236,220,204,188,172,156,140,124,108,92,76,60,44,28,12 }; + static const int16_t tabindex[2*16] = { 0,32,64,98,0,132,180,218,292,364,426,538,648,746,0,1126,1460,1460,1460,1460,1460,1460,1460,1460,1842,1842,1842,1842,1842,1842,1842,1842 }; + static const uint8_t g_linbits[] = { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,2,3,4,6,8,10,13,4,5,6,7,8,9,11,13 }; + +#define PEEK_BITS(n) (bs_cache >> (32 - n)) +#define FLUSH_BITS(n) { bs_cache <<= (n); bs_sh += (n); } +#define CHECK_BITS while (bs_sh >= 0) { bs_cache |= (uint32_t)*bs_next_ptr++ << bs_sh; bs_sh -= 8; } +#define BSPOS ((bs_next_ptr - bs->buf)*8 - 24 + bs_sh) + + float one = 0.0f; + int ireg = 0, big_val_cnt = gr_info->big_values; + const uint8_t *sfb = gr_info->sfbtab; + const uint8_t *bs_next_ptr = bs->buf + bs->pos/8; + uint32_t bs_cache = (((bs_next_ptr[0]*256u + bs_next_ptr[1])*256u + bs_next_ptr[2])*256u + bs_next_ptr[3]) << (bs->pos & 7); + int pairs_to_decode, np, bs_sh = (bs->pos & 7) - 8; + bs_next_ptr += 4; + + while (big_val_cnt > 0) + { + int tab_num = gr_info->table_select[ireg]; + int sfb_cnt = gr_info->region_count[ireg++]; + const int16_t *codebook = tabs + tabindex[tab_num]; + int linbits = g_linbits[tab_num]; + if (linbits) + { + do + { + np = *sfb++ / 2; + pairs_to_decode = MINIMP3_MIN(big_val_cnt, np); + one = *scf++; + do + { + int j, w = 5; + int leaf = codebook[PEEK_BITS(w)]; + while (leaf < 0) + { + FLUSH_BITS(w); + w = leaf & 7; + leaf = codebook[PEEK_BITS(w) - (leaf >> 3)]; + } + FLUSH_BITS(leaf >> 8); + + for (j = 0; j < 2; j++, dst++, leaf >>= 4) + { + int lsb = leaf & 0x0F; + if (lsb == 15) + { + lsb += PEEK_BITS(linbits); + FLUSH_BITS(linbits); + CHECK_BITS; + *dst = one*L3_pow_43(lsb)*((int32_t)bs_cache < 0 ? -1: 1); + } else + { + *dst = g_pow43[16 + lsb - 16*(bs_cache >> 31)]*one; + } + FLUSH_BITS(lsb ? 1 : 0); + } + CHECK_BITS; + } while (--pairs_to_decode); + } while ((big_val_cnt -= np) > 0 && --sfb_cnt >= 0); + } else + { + do + { + np = *sfb++ / 2; + pairs_to_decode = MINIMP3_MIN(big_val_cnt, np); + one = *scf++; + do + { + int j, w = 5; + int leaf = codebook[PEEK_BITS(w)]; + while (leaf < 0) + { + FLUSH_BITS(w); + w = leaf & 7; + leaf = codebook[PEEK_BITS(w) - (leaf >> 3)]; + } + FLUSH_BITS(leaf >> 8); + + for (j = 0; j < 2; j++, dst++, leaf >>= 4) + { + int lsb = leaf & 0x0F; + *dst = g_pow43[16 + lsb - 16*(bs_cache >> 31)]*one; + FLUSH_BITS(lsb ? 1 : 0); + } + CHECK_BITS; + } while (--pairs_to_decode); + } while ((big_val_cnt -= np) > 0 && --sfb_cnt >= 0); + } + } + + for (np = 1 - big_val_cnt;; dst += 4) + { + const uint8_t *codebook_count1 = (gr_info->count1_table) ? tab33 : tab32; + int leaf = codebook_count1[PEEK_BITS(4)]; + if (!(leaf & 8)) + { + leaf = codebook_count1[(leaf >> 3) + (bs_cache << 4 >> (32 - (leaf & 3)))]; + } + FLUSH_BITS(leaf & 7); + if (BSPOS > layer3gr_limit) + { + break; + } +#define RELOAD_SCALEFACTOR if (!--np) { np = *sfb++/2; if (!np) break; one = *scf++; } +#define DEQ_COUNT1(s) if (leaf & (128 >> s)) { dst[s] = ((int32_t)bs_cache < 0) ? -one : one; FLUSH_BITS(1) } + RELOAD_SCALEFACTOR; + DEQ_COUNT1(0); + DEQ_COUNT1(1); + RELOAD_SCALEFACTOR; + DEQ_COUNT1(2); + DEQ_COUNT1(3); + CHECK_BITS; + } + + bs->pos = layer3gr_limit; +} + +static void L3_midside_stereo(float *left, int n) +{ + int i = 0; + float *right = left + 576; +#if HAVE_SIMD + if (have_simd()) + { + for (; i < n - 3; i += 4) + { + f4 vl = VLD(left + i); + f4 vr = VLD(right + i); + VSTORE(left + i, VADD(vl, vr)); + VSTORE(right + i, VSUB(vl, vr)); + } +#ifdef __GNUC__ + /* Workaround for spurious -Waggressive-loop-optimizations warning from gcc. + * For more info see: https://github.com/lieff/minimp3/issues/88 + */ + if (__builtin_constant_p(n % 4 == 0) && n % 4 == 0) + return; +#endif + } +#endif /* HAVE_SIMD */ + for (; i < n; i++) + { + float a = left[i]; + float b = right[i]; + left[i] = a + b; + right[i] = a - b; + } +} + +static void L3_intensity_stereo_band(float *left, int n, float kl, float kr) +{ + int i; + for (i = 0; i < n; i++) + { + left[i + 576] = left[i]*kr; + left[i] = left[i]*kl; + } +} + +static void L3_stereo_top_band(const float *right, const uint8_t *sfb, int nbands, int max_band[3]) +{ + int i, k; + + max_band[0] = max_band[1] = max_band[2] = -1; + + for (i = 0; i < nbands; i++) + { + for (k = 0; k < sfb[i]; k += 2) + { + if (right[k] != 0 || right[k + 1] != 0) + { + max_band[i % 3] = i; + break; + } + } + right += sfb[i]; + } +} + +static void L3_stereo_process(float *left, const uint8_t *ist_pos, const uint8_t *sfb, const uint8_t *hdr, int max_band[3], int mpeg2_sh) +{ + static const float g_pan[7*2] = { 0,1,0.21132487f,0.78867513f,0.36602540f,0.63397460f,0.5f,0.5f,0.63397460f,0.36602540f,0.78867513f,0.21132487f,1,0 }; + unsigned i, max_pos = HDR_TEST_MPEG1(hdr) ? 7 : 64; + + for (i = 0; sfb[i]; i++) + { + unsigned ipos = ist_pos[i]; + if ((int)i > max_band[i % 3] && ipos < max_pos) + { + float kl, kr, s = HDR_TEST_MS_STEREO(hdr) ? 1.41421356f : 1; + if (HDR_TEST_MPEG1(hdr)) + { + kl = g_pan[2*ipos]; + kr = g_pan[2*ipos + 1]; + } else + { + kl = 1; + kr = L3_ldexp_q2(1, (ipos + 1) >> 1 << mpeg2_sh); + if (ipos & 1) + { + kl = kr; + kr = 1; + } + } + L3_intensity_stereo_band(left, sfb[i], kl*s, kr*s); + } else if (HDR_TEST_MS_STEREO(hdr)) + { + L3_midside_stereo(left, sfb[i]); + } + left += sfb[i]; + } +} + +static void L3_intensity_stereo(float *left, uint8_t *ist_pos, const L3_gr_info_t *gr, const uint8_t *hdr) +{ + int max_band[3], n_sfb = gr->n_long_sfb + gr->n_short_sfb; + int i, max_blocks = gr->n_short_sfb ? 3 : 1; + + L3_stereo_top_band(left + 576, gr->sfbtab, n_sfb, max_band); + if (gr->n_long_sfb) + { + max_band[0] = max_band[1] = max_band[2] = MINIMP3_MAX(MINIMP3_MAX(max_band[0], max_band[1]), max_band[2]); + } + for (i = 0; i < max_blocks; i++) + { + int default_pos = HDR_TEST_MPEG1(hdr) ? 3 : 0; + int itop = n_sfb - max_blocks + i; + int prev = itop - max_blocks; + ist_pos[itop] = max_band[i] >= prev ? default_pos : ist_pos[prev]; + } + L3_stereo_process(left, ist_pos, gr->sfbtab, hdr, max_band, gr[1].scalefac_compress & 1); +} + +static void L3_reorder(float *grbuf, float *scratch, const uint8_t *sfb) +{ + int i, len; + float *src = grbuf, *dst = scratch; + + for (;0 != (len = *sfb); sfb += 3, src += 2*len) + { + for (i = 0; i < len; i++, src++) + { + *dst++ = src[0*len]; + *dst++ = src[1*len]; + *dst++ = src[2*len]; + } + } + memcpy(grbuf, scratch, (dst - scratch)*sizeof(float)); +} + +static void L3_antialias(float *grbuf, int nbands) +{ + static const float g_aa[2][8] = { + {0.85749293f,0.88174200f,0.94962865f,0.98331459f,0.99551782f,0.99916056f,0.99989920f,0.99999316f}, + {0.51449576f,0.47173197f,0.31337745f,0.18191320f,0.09457419f,0.04096558f,0.01419856f,0.00369997f} + }; + + for (; nbands > 0; nbands--, grbuf += 18) + { + int i = 0; +#if HAVE_SIMD + if (have_simd()) for (; i < 8; i += 4) + { + f4 vu = VLD(grbuf + 18 + i); + f4 vd = VLD(grbuf + 14 - i); + f4 vc0 = VLD(g_aa[0] + i); + f4 vc1 = VLD(g_aa[1] + i); + vd = VREV(vd); + VSTORE(grbuf + 18 + i, VSUB(VMUL(vu, vc0), VMUL(vd, vc1))); + vd = VADD(VMUL(vu, vc1), VMUL(vd, vc0)); + VSTORE(grbuf + 14 - i, VREV(vd)); + } +#endif /* HAVE_SIMD */ +#ifndef MINIMP3_ONLY_SIMD + for(; i < 8; i++) + { + float u = grbuf[18 + i]; + float d = grbuf[17 - i]; + grbuf[18 + i] = u*g_aa[0][i] - d*g_aa[1][i]; + grbuf[17 - i] = u*g_aa[1][i] + d*g_aa[0][i]; + } +#endif /* MINIMP3_ONLY_SIMD */ + } +} + +static void L3_dct3_9(float *y) +{ + float s0, s1, s2, s3, s4, s5, s6, s7, s8, t0, t2, t4; + + s0 = y[0]; s2 = y[2]; s4 = y[4]; s6 = y[6]; s8 = y[8]; + t0 = s0 + s6*0.5f; + s0 -= s6; + t4 = (s4 + s2)*0.93969262f; + t2 = (s8 + s2)*0.76604444f; + s6 = (s4 - s8)*0.17364818f; + s4 += s8 - s2; + + s2 = s0 - s4*0.5f; + y[4] = s4 + s0; + s8 = t0 - t2 + s6; + s0 = t0 - t4 + t2; + s4 = t0 + t4 - s6; + + s1 = y[1]; s3 = y[3]; s5 = y[5]; s7 = y[7]; + + s3 *= 0.86602540f; + t0 = (s5 + s1)*0.98480775f; + t4 = (s5 - s7)*0.34202014f; + t2 = (s1 + s7)*0.64278761f; + s1 = (s1 - s5 - s7)*0.86602540f; + + s5 = t0 - s3 - t2; + s7 = t4 - s3 - t0; + s3 = t4 + s3 - t2; + + y[0] = s4 - s7; + y[1] = s2 + s1; + y[2] = s0 - s3; + y[3] = s8 + s5; + y[5] = s8 - s5; + y[6] = s0 + s3; + y[7] = s2 - s1; + y[8] = s4 + s7; +} + +static void L3_imdct36(float *grbuf, float *overlap, const float *window, int nbands) +{ + int i, j; + static const float g_twid9[18] = { + 0.73727734f,0.79335334f,0.84339145f,0.88701083f,0.92387953f,0.95371695f,0.97629601f,0.99144486f,0.99904822f,0.67559021f,0.60876143f,0.53729961f,0.46174861f,0.38268343f,0.30070580f,0.21643961f,0.13052619f,0.04361938f + }; + + for (j = 0; j < nbands; j++, grbuf += 18, overlap += 9) + { + float co[9], si[9]; + co[0] = -grbuf[0]; + si[0] = grbuf[17]; + for (i = 0; i < 4; i++) + { + si[8 - 2*i] = grbuf[4*i + 1] - grbuf[4*i + 2]; + co[1 + 2*i] = grbuf[4*i + 1] + grbuf[4*i + 2]; + si[7 - 2*i] = grbuf[4*i + 4] - grbuf[4*i + 3]; + co[2 + 2*i] = -(grbuf[4*i + 3] + grbuf[4*i + 4]); + } + L3_dct3_9(co); + L3_dct3_9(si); + + si[1] = -si[1]; + si[3] = -si[3]; + si[5] = -si[5]; + si[7] = -si[7]; + + i = 0; + +#if HAVE_SIMD + if (have_simd()) for (; i < 8; i += 4) + { + f4 vovl = VLD(overlap + i); + f4 vc = VLD(co + i); + f4 vs = VLD(si + i); + f4 vr0 = VLD(g_twid9 + i); + f4 vr1 = VLD(g_twid9 + 9 + i); + f4 vw0 = VLD(window + i); + f4 vw1 = VLD(window + 9 + i); + f4 vsum = VADD(VMUL(vc, vr1), VMUL(vs, vr0)); + VSTORE(overlap + i, VSUB(VMUL(vc, vr0), VMUL(vs, vr1))); + VSTORE(grbuf + i, VSUB(VMUL(vovl, vw0), VMUL(vsum, vw1))); + vsum = VADD(VMUL(vovl, vw1), VMUL(vsum, vw0)); + VSTORE(grbuf + 14 - i, VREV(vsum)); + } +#endif /* HAVE_SIMD */ + for (; i < 9; i++) + { + float ovl = overlap[i]; + float sum = co[i]*g_twid9[9 + i] + si[i]*g_twid9[0 + i]; + overlap[i] = co[i]*g_twid9[0 + i] - si[i]*g_twid9[9 + i]; + grbuf[i] = ovl*window[0 + i] - sum*window[9 + i]; + grbuf[17 - i] = ovl*window[9 + i] + sum*window[0 + i]; + } + } +} + +static void L3_idct3(float x0, float x1, float x2, float *dst) +{ + float m1 = x1*0.86602540f; + float a1 = x0 - x2*0.5f; + dst[1] = x0 + x2; + dst[0] = a1 + m1; + dst[2] = a1 - m1; +} + +static void L3_imdct12(float *x, float *dst, float *overlap) +{ + static const float g_twid3[6] = { 0.79335334f,0.92387953f,0.99144486f, 0.60876143f,0.38268343f,0.13052619f }; + float co[3], si[3]; + int i; + + L3_idct3(-x[0], x[6] + x[3], x[12] + x[9], co); + L3_idct3(x[15], x[12] - x[9], x[6] - x[3], si); + si[1] = -si[1]; + + for (i = 0; i < 3; i++) + { + float ovl = overlap[i]; + float sum = co[i]*g_twid3[3 + i] + si[i]*g_twid3[0 + i]; + overlap[i] = co[i]*g_twid3[0 + i] - si[i]*g_twid3[3 + i]; + dst[i] = ovl*g_twid3[2 - i] - sum*g_twid3[5 - i]; + dst[5 - i] = ovl*g_twid3[5 - i] + sum*g_twid3[2 - i]; + } +} + +static void L3_imdct_short(float *grbuf, float *overlap, int nbands) +{ + for (;nbands > 0; nbands--, overlap += 9, grbuf += 18) + { + float tmp[18]; + memcpy(tmp, grbuf, sizeof(tmp)); + memcpy(grbuf, overlap, 6*sizeof(float)); + L3_imdct12(tmp, grbuf + 6, overlap + 6); + L3_imdct12(tmp + 1, grbuf + 12, overlap + 6); + L3_imdct12(tmp + 2, overlap, overlap + 6); + } +} + +static void L3_change_sign(float *grbuf) +{ + int b, i; + for (b = 0, grbuf += 18; b < 32; b += 2, grbuf += 36) + for (i = 1; i < 18; i += 2) + grbuf[i] = -grbuf[i]; +} + +static void L3_imdct_gr(float *grbuf, float *overlap, unsigned block_type, unsigned n_long_bands) +{ + static const float g_mdct_window[2][18] = { + { 0.99904822f,0.99144486f,0.97629601f,0.95371695f,0.92387953f,0.88701083f,0.84339145f,0.79335334f,0.73727734f,0.04361938f,0.13052619f,0.21643961f,0.30070580f,0.38268343f,0.46174861f,0.53729961f,0.60876143f,0.67559021f }, + { 1,1,1,1,1,1,0.99144486f,0.92387953f,0.79335334f,0,0,0,0,0,0,0.13052619f,0.38268343f,0.60876143f } + }; + if (n_long_bands) + { + L3_imdct36(grbuf, overlap, g_mdct_window[0], n_long_bands); + grbuf += 18*n_long_bands; + overlap += 9*n_long_bands; + } + if (block_type == SHORT_BLOCK_TYPE) + L3_imdct_short(grbuf, overlap, 32 - n_long_bands); + else + L3_imdct36(grbuf, overlap, g_mdct_window[block_type == STOP_BLOCK_TYPE], 32 - n_long_bands); +} + +static void L3_save_reservoir(mp3dec_t *h, mp3dec_scratch_t *s) +{ + int pos = (s->bs.pos + 7)/8u; + int remains = s->bs.limit/8u - pos; + if (remains > MAX_BITRESERVOIR_BYTES) + { + pos += remains - MAX_BITRESERVOIR_BYTES; + remains = MAX_BITRESERVOIR_BYTES; + } + if (remains > 0) + { + memmove(h->reserv_buf, s->maindata + pos, remains); + } + h->reserv = remains; +} + +static int L3_restore_reservoir(mp3dec_t *h, bs_t *bs, mp3dec_scratch_t *s, int main_data_begin) +{ + int frame_bytes = (bs->limit - bs->pos)/8; + int bytes_have = MINIMP3_MIN(h->reserv, main_data_begin); + memcpy(s->maindata, h->reserv_buf + MINIMP3_MAX(0, h->reserv - main_data_begin), MINIMP3_MIN(h->reserv, main_data_begin)); + memcpy(s->maindata + bytes_have, bs->buf + bs->pos/8, frame_bytes); + bs_init(&s->bs, s->maindata, bytes_have + frame_bytes); + return h->reserv >= main_data_begin; +} + +static void L3_decode(mp3dec_t *h, mp3dec_scratch_t *s, L3_gr_info_t *gr_info, int nch) +{ + int ch; + + for (ch = 0; ch < nch; ch++) + { + int layer3gr_limit = s->bs.pos + gr_info[ch].part_23_length; + L3_decode_scalefactors(h->header, s->ist_pos[ch], &s->bs, gr_info + ch, s->scf, ch); + L3_huffman(s->grbuf[ch], &s->bs, gr_info + ch, s->scf, layer3gr_limit); + } + + if (HDR_TEST_I_STEREO(h->header)) + { + L3_intensity_stereo(s->grbuf[0], s->ist_pos[1], gr_info, h->header); + } else if (HDR_IS_MS_STEREO(h->header)) + { + L3_midside_stereo(s->grbuf[0], 576); + } + + for (ch = 0; ch < nch; ch++, gr_info++) + { + int aa_bands = 31; + int n_long_bands = (gr_info->mixed_block_flag ? 2 : 0) << (int)(HDR_GET_MY_SAMPLE_RATE(h->header) == 2); + + if (gr_info->n_short_sfb) + { + aa_bands = n_long_bands - 1; + L3_reorder(s->grbuf[ch] + n_long_bands*18, s->syn[0], gr_info->sfbtab + gr_info->n_long_sfb); + } + + L3_antialias(s->grbuf[ch], aa_bands); + L3_imdct_gr(s->grbuf[ch], h->mdct_overlap[ch], gr_info->block_type, n_long_bands); + L3_change_sign(s->grbuf[ch]); + } +} + +static void mp3d_DCT_II(float *grbuf, int n) +{ + static const float g_sec[24] = { + 10.19000816f,0.50060302f,0.50241929f,3.40760851f,0.50547093f,0.52249861f,2.05778098f,0.51544732f,0.56694406f,1.48416460f,0.53104258f,0.64682180f,1.16943991f,0.55310392f,0.78815460f,0.97256821f,0.58293498f,1.06067765f,0.83934963f,0.62250412f,1.72244716f,0.74453628f,0.67480832f,5.10114861f + }; + int i, k = 0; +#if HAVE_SIMD + if (have_simd()) for (; k < n; k += 4) + { + f4 t[4][8], *x; + float *y = grbuf + k; + + for (x = t[0], i = 0; i < 8; i++, x++) + { + f4 x0 = VLD(&y[i*18]); + f4 x1 = VLD(&y[(15 - i)*18]); + f4 x2 = VLD(&y[(16 + i)*18]); + f4 x3 = VLD(&y[(31 - i)*18]); + f4 t0 = VADD(x0, x3); + f4 t1 = VADD(x1, x2); + f4 t2 = VMUL_S(VSUB(x1, x2), g_sec[3*i + 0]); + f4 t3 = VMUL_S(VSUB(x0, x3), g_sec[3*i + 1]); + x[0] = VADD(t0, t1); + x[8] = VMUL_S(VSUB(t0, t1), g_sec[3*i + 2]); + x[16] = VADD(t3, t2); + x[24] = VMUL_S(VSUB(t3, t2), g_sec[3*i + 2]); + } + for (x = t[0], i = 0; i < 4; i++, x += 8) + { + f4 x0 = x[0], x1 = x[1], x2 = x[2], x3 = x[3], x4 = x[4], x5 = x[5], x6 = x[6], x7 = x[7], xt; + xt = VSUB(x0, x7); x0 = VADD(x0, x7); + x7 = VSUB(x1, x6); x1 = VADD(x1, x6); + x6 = VSUB(x2, x5); x2 = VADD(x2, x5); + x5 = VSUB(x3, x4); x3 = VADD(x3, x4); + x4 = VSUB(x0, x3); x0 = VADD(x0, x3); + x3 = VSUB(x1, x2); x1 = VADD(x1, x2); + x[0] = VADD(x0, x1); + x[4] = VMUL_S(VSUB(x0, x1), 0.70710677f); + x5 = VADD(x5, x6); + x6 = VMUL_S(VADD(x6, x7), 0.70710677f); + x7 = VADD(x7, xt); + x3 = VMUL_S(VADD(x3, x4), 0.70710677f); + x5 = VSUB(x5, VMUL_S(x7, 0.198912367f)); /* rotate by PI/8 */ + x7 = VADD(x7, VMUL_S(x5, 0.382683432f)); + x5 = VSUB(x5, VMUL_S(x7, 0.198912367f)); + x0 = VSUB(xt, x6); xt = VADD(xt, x6); + x[1] = VMUL_S(VADD(xt, x7), 0.50979561f); + x[2] = VMUL_S(VADD(x4, x3), 0.54119611f); + x[3] = VMUL_S(VSUB(x0, x5), 0.60134488f); + x[5] = VMUL_S(VADD(x0, x5), 0.89997619f); + x[6] = VMUL_S(VSUB(x4, x3), 1.30656302f); + x[7] = VMUL_S(VSUB(xt, x7), 2.56291556f); + } + + if (k > n - 3) + { +#if HAVE_SSE +#define VSAVE2(i, v) _mm_storel_pi((__m64 *)(void*)&y[i*18], v) +#else /* HAVE_SSE */ +#define VSAVE2(i, v) vst1_f32((float32_t *)&y[i*18], vget_low_f32(v)) +#endif /* HAVE_SSE */ + for (i = 0; i < 7; i++, y += 4*18) + { + f4 s = VADD(t[3][i], t[3][i + 1]); + VSAVE2(0, t[0][i]); + VSAVE2(1, VADD(t[2][i], s)); + VSAVE2(2, VADD(t[1][i], t[1][i + 1])); + VSAVE2(3, VADD(t[2][1 + i], s)); + } + VSAVE2(0, t[0][7]); + VSAVE2(1, VADD(t[2][7], t[3][7])); + VSAVE2(2, t[1][7]); + VSAVE2(3, t[3][7]); + } else + { +#define VSAVE4(i, v) VSTORE(&y[i*18], v) + for (i = 0; i < 7; i++, y += 4*18) + { + f4 s = VADD(t[3][i], t[3][i + 1]); + VSAVE4(0, t[0][i]); + VSAVE4(1, VADD(t[2][i], s)); + VSAVE4(2, VADD(t[1][i], t[1][i + 1])); + VSAVE4(3, VADD(t[2][1 + i], s)); + } + VSAVE4(0, t[0][7]); + VSAVE4(1, VADD(t[2][7], t[3][7])); + VSAVE4(2, t[1][7]); + VSAVE4(3, t[3][7]); + } + } else +#endif /* HAVE_SIMD */ +#ifdef MINIMP3_ONLY_SIMD + {} /* for HAVE_SIMD=1, MINIMP3_ONLY_SIMD=1 case we do not need non-intrinsic "else" branch */ +#else /* MINIMP3_ONLY_SIMD */ + for (; k < n; k++) + { + float t[4][8], *x, *y = grbuf + k; + + for (x = t[0], i = 0; i < 8; i++, x++) + { + float x0 = y[i*18]; + float x1 = y[(15 - i)*18]; + float x2 = y[(16 + i)*18]; + float x3 = y[(31 - i)*18]; + float t0 = x0 + x3; + float t1 = x1 + x2; + float t2 = (x1 - x2)*g_sec[3*i + 0]; + float t3 = (x0 - x3)*g_sec[3*i + 1]; + x[0] = t0 + t1; + x[8] = (t0 - t1)*g_sec[3*i + 2]; + x[16] = t3 + t2; + x[24] = (t3 - t2)*g_sec[3*i + 2]; + } + for (x = t[0], i = 0; i < 4; i++, x += 8) + { + float x0 = x[0], x1 = x[1], x2 = x[2], x3 = x[3], x4 = x[4], x5 = x[5], x6 = x[6], x7 = x[7], xt; + xt = x0 - x7; x0 += x7; + x7 = x1 - x6; x1 += x6; + x6 = x2 - x5; x2 += x5; + x5 = x3 - x4; x3 += x4; + x4 = x0 - x3; x0 += x3; + x3 = x1 - x2; x1 += x2; + x[0] = x0 + x1; + x[4] = (x0 - x1)*0.70710677f; + x5 = x5 + x6; + x6 = (x6 + x7)*0.70710677f; + x7 = x7 + xt; + x3 = (x3 + x4)*0.70710677f; + x5 -= x7*0.198912367f; /* rotate by PI/8 */ + x7 += x5*0.382683432f; + x5 -= x7*0.198912367f; + x0 = xt - x6; xt += x6; + x[1] = (xt + x7)*0.50979561f; + x[2] = (x4 + x3)*0.54119611f; + x[3] = (x0 - x5)*0.60134488f; + x[5] = (x0 + x5)*0.89997619f; + x[6] = (x4 - x3)*1.30656302f; + x[7] = (xt - x7)*2.56291556f; + + } + for (i = 0; i < 7; i++, y += 4*18) + { + y[0*18] = t[0][i]; + y[1*18] = t[2][i] + t[3][i] + t[3][i + 1]; + y[2*18] = t[1][i] + t[1][i + 1]; + y[3*18] = t[2][i + 1] + t[3][i] + t[3][i + 1]; + } + y[0*18] = t[0][7]; + y[1*18] = t[2][7] + t[3][7]; + y[2*18] = t[1][7]; + y[3*18] = t[3][7]; + } +#endif /* MINIMP3_ONLY_SIMD */ +} + +#ifndef MINIMP3_FLOAT_OUTPUT +static int16_t mp3d_scale_pcm(float sample) +{ +#if HAVE_ARMV6 + int32_t s32 = (int32_t)(sample + .5f); + s32 -= (s32 < 0); + int16_t s = (int16_t)minimp3_clip_int16_arm(s32); +#else + if (sample >= 32766.5) return (int16_t) 32767; + if (sample <= -32767.5) return (int16_t)-32768; + int16_t s = (int16_t)(sample + .5f); + s -= (s < 0); /* away from zero, to be compliant */ +#endif + return s; +} +#else /* MINIMP3_FLOAT_OUTPUT */ +static float mp3d_scale_pcm(float sample) +{ + return sample*(1.f/32768.f); +} +#endif /* MINIMP3_FLOAT_OUTPUT */ + +static void mp3d_synth_pair(mp3d_sample_t *pcm, int nch, const float *z) +{ + float a; + a = (z[14*64] - z[ 0]) * 29; + a += (z[ 1*64] + z[13*64]) * 213; + a += (z[12*64] - z[ 2*64]) * 459; + a += (z[ 3*64] + z[11*64]) * 2037; + a += (z[10*64] - z[ 4*64]) * 5153; + a += (z[ 5*64] + z[ 9*64]) * 6574; + a += (z[ 8*64] - z[ 6*64]) * 37489; + a += z[ 7*64] * 75038; + pcm[0] = mp3d_scale_pcm(a); + + z += 2; + a = z[14*64] * 104; + a += z[12*64] * 1567; + a += z[10*64] * 9727; + a += z[ 8*64] * 64019; + a += z[ 6*64] * -9975; + a += z[ 4*64] * -45; + a += z[ 2*64] * 146; + a += z[ 0*64] * -5; + pcm[16*nch] = mp3d_scale_pcm(a); +} + +static void mp3d_synth(float *xl, mp3d_sample_t *dstl, int nch, float *lins) +{ + int i; + float *xr = xl + 576*(nch - 1); + mp3d_sample_t *dstr = dstl + (nch - 1); + + static const float g_win[] = { + -1,26,-31,208,218,401,-519,2063,2000,4788,-5517,7134,5959,35640,-39336,74992, + -1,24,-35,202,222,347,-581,2080,1952,4425,-5879,7640,5288,33791,-41176,74856, + -1,21,-38,196,225,294,-645,2087,1893,4063,-6237,8092,4561,31947,-43006,74630, + -1,19,-41,190,227,244,-711,2085,1822,3705,-6589,8492,3776,30112,-44821,74313, + -1,17,-45,183,228,197,-779,2075,1739,3351,-6935,8840,2935,28289,-46617,73908, + -1,16,-49,176,228,153,-848,2057,1644,3004,-7271,9139,2037,26482,-48390,73415, + -2,14,-53,169,227,111,-919,2032,1535,2663,-7597,9389,1082,24694,-50137,72835, + -2,13,-58,161,224,72,-991,2001,1414,2330,-7910,9592,70,22929,-51853,72169, + -2,11,-63,154,221,36,-1064,1962,1280,2006,-8209,9750,-998,21189,-53534,71420, + -2,10,-68,147,215,2,-1137,1919,1131,1692,-8491,9863,-2122,19478,-55178,70590, + -3,9,-73,139,208,-29,-1210,1870,970,1388,-8755,9935,-3300,17799,-56778,69679, + -3,8,-79,132,200,-57,-1283,1817,794,1095,-8998,9966,-4533,16155,-58333,68692, + -4,7,-85,125,189,-83,-1356,1759,605,814,-9219,9959,-5818,14548,-59838,67629, + -4,7,-91,117,177,-106,-1428,1698,402,545,-9416,9916,-7154,12980,-61289,66494, + -5,6,-97,111,163,-127,-1498,1634,185,288,-9585,9838,-8540,11455,-62684,65290 + }; + float *zlin = lins + 15*64; + const float *w = g_win; + + zlin[4*15] = xl[18*16]; + zlin[4*15 + 1] = xr[18*16]; + zlin[4*15 + 2] = xl[0]; + zlin[4*15 + 3] = xr[0]; + + zlin[4*31] = xl[1 + 18*16]; + zlin[4*31 + 1] = xr[1 + 18*16]; + zlin[4*31 + 2] = xl[1]; + zlin[4*31 + 3] = xr[1]; + + mp3d_synth_pair(dstr, nch, lins + 4*15 + 1); + mp3d_synth_pair(dstr + 32*nch, nch, lins + 4*15 + 64 + 1); + mp3d_synth_pair(dstl, nch, lins + 4*15); + mp3d_synth_pair(dstl + 32*nch, nch, lins + 4*15 + 64); + +#if HAVE_SIMD + if (have_simd()) for (i = 14; i >= 0; i--) + { +#define VLOAD(k) f4 w0 = VSET(*w++); f4 w1 = VSET(*w++); f4 vz = VLD(&zlin[4*i - 64*k]); f4 vy = VLD(&zlin[4*i - 64*(15 - k)]); +#define V0(k) { VLOAD(k) b = VADD(VMUL(vz, w1), VMUL(vy, w0)) ; a = VSUB(VMUL(vz, w0), VMUL(vy, w1)); } +#define V1(k) { VLOAD(k) b = VADD(b, VADD(VMUL(vz, w1), VMUL(vy, w0))); a = VADD(a, VSUB(VMUL(vz, w0), VMUL(vy, w1))); } +#define V2(k) { VLOAD(k) b = VADD(b, VADD(VMUL(vz, w1), VMUL(vy, w0))); a = VADD(a, VSUB(VMUL(vy, w1), VMUL(vz, w0))); } + f4 a, b; + zlin[4*i] = xl[18*(31 - i)]; + zlin[4*i + 1] = xr[18*(31 - i)]; + zlin[4*i + 2] = xl[1 + 18*(31 - i)]; + zlin[4*i + 3] = xr[1 + 18*(31 - i)]; + zlin[4*i + 64] = xl[1 + 18*(1 + i)]; + zlin[4*i + 64 + 1] = xr[1 + 18*(1 + i)]; + zlin[4*i - 64 + 2] = xl[18*(1 + i)]; + zlin[4*i - 64 + 3] = xr[18*(1 + i)]; + + V0(0) V2(1) V1(2) V2(3) V1(4) V2(5) V1(6) V2(7) + + { +#ifndef MINIMP3_FLOAT_OUTPUT +#if HAVE_SSE + static const f4 g_max = { 32767.0f, 32767.0f, 32767.0f, 32767.0f }; + static const f4 g_min = { -32768.0f, -32768.0f, -32768.0f, -32768.0f }; + __m128i pcm8 = _mm_packs_epi32(_mm_cvtps_epi32(_mm_max_ps(_mm_min_ps(a, g_max), g_min)), + _mm_cvtps_epi32(_mm_max_ps(_mm_min_ps(b, g_max), g_min))); + dstr[(15 - i)*nch] = _mm_extract_epi16(pcm8, 1); + dstr[(17 + i)*nch] = _mm_extract_epi16(pcm8, 5); + dstl[(15 - i)*nch] = _mm_extract_epi16(pcm8, 0); + dstl[(17 + i)*nch] = _mm_extract_epi16(pcm8, 4); + dstr[(47 - i)*nch] = _mm_extract_epi16(pcm8, 3); + dstr[(49 + i)*nch] = _mm_extract_epi16(pcm8, 7); + dstl[(47 - i)*nch] = _mm_extract_epi16(pcm8, 2); + dstl[(49 + i)*nch] = _mm_extract_epi16(pcm8, 6); +#else /* HAVE_SSE */ + int16x4_t pcma, pcmb; + a = VADD(a, VSET(0.5f)); + b = VADD(b, VSET(0.5f)); + pcma = vqmovn_s32(vqaddq_s32(vcvtq_s32_f32(a), vreinterpretq_s32_u32(vcltq_f32(a, VSET(0))))); + pcmb = vqmovn_s32(vqaddq_s32(vcvtq_s32_f32(b), vreinterpretq_s32_u32(vcltq_f32(b, VSET(0))))); + vst1_lane_s16(dstr + (15 - i)*nch, pcma, 1); + vst1_lane_s16(dstr + (17 + i)*nch, pcmb, 1); + vst1_lane_s16(dstl + (15 - i)*nch, pcma, 0); + vst1_lane_s16(dstl + (17 + i)*nch, pcmb, 0); + vst1_lane_s16(dstr + (47 - i)*nch, pcma, 3); + vst1_lane_s16(dstr + (49 + i)*nch, pcmb, 3); + vst1_lane_s16(dstl + (47 - i)*nch, pcma, 2); + vst1_lane_s16(dstl + (49 + i)*nch, pcmb, 2); +#endif /* HAVE_SSE */ + +#else /* MINIMP3_FLOAT_OUTPUT */ + + static const f4 g_scale = { 1.0f/32768.0f, 1.0f/32768.0f, 1.0f/32768.0f, 1.0f/32768.0f }; + a = VMUL(a, g_scale); + b = VMUL(b, g_scale); +#if HAVE_SSE + _mm_store_ss(dstr + (15 - i)*nch, _mm_shuffle_ps(a, a, _MM_SHUFFLE(1, 1, 1, 1))); + _mm_store_ss(dstr + (17 + i)*nch, _mm_shuffle_ps(b, b, _MM_SHUFFLE(1, 1, 1, 1))); + _mm_store_ss(dstl + (15 - i)*nch, _mm_shuffle_ps(a, a, _MM_SHUFFLE(0, 0, 0, 0))); + _mm_store_ss(dstl + (17 + i)*nch, _mm_shuffle_ps(b, b, _MM_SHUFFLE(0, 0, 0, 0))); + _mm_store_ss(dstr + (47 - i)*nch, _mm_shuffle_ps(a, a, _MM_SHUFFLE(3, 3, 3, 3))); + _mm_store_ss(dstr + (49 + i)*nch, _mm_shuffle_ps(b, b, _MM_SHUFFLE(3, 3, 3, 3))); + _mm_store_ss(dstl + (47 - i)*nch, _mm_shuffle_ps(a, a, _MM_SHUFFLE(2, 2, 2, 2))); + _mm_store_ss(dstl + (49 + i)*nch, _mm_shuffle_ps(b, b, _MM_SHUFFLE(2, 2, 2, 2))); +#else /* HAVE_SSE */ + vst1q_lane_f32(dstr + (15 - i)*nch, a, 1); + vst1q_lane_f32(dstr + (17 + i)*nch, b, 1); + vst1q_lane_f32(dstl + (15 - i)*nch, a, 0); + vst1q_lane_f32(dstl + (17 + i)*nch, b, 0); + vst1q_lane_f32(dstr + (47 - i)*nch, a, 3); + vst1q_lane_f32(dstr + (49 + i)*nch, b, 3); + vst1q_lane_f32(dstl + (47 - i)*nch, a, 2); + vst1q_lane_f32(dstl + (49 + i)*nch, b, 2); +#endif /* HAVE_SSE */ +#endif /* MINIMP3_FLOAT_OUTPUT */ + } + } else +#endif /* HAVE_SIMD */ +#ifdef MINIMP3_ONLY_SIMD + {} /* for HAVE_SIMD=1, MINIMP3_ONLY_SIMD=1 case we do not need non-intrinsic "else" branch */ +#else /* MINIMP3_ONLY_SIMD */ + for (i = 14; i >= 0; i--) + { +#define LOAD(k) float w0 = *w++; float w1 = *w++; float *vz = &zlin[4*i - k*64]; float *vy = &zlin[4*i - (15 - k)*64]; +#define S0(k) { int j; LOAD(k); for (j = 0; j < 4; j++) b[j] = vz[j]*w1 + vy[j]*w0, a[j] = vz[j]*w0 - vy[j]*w1; } +#define S1(k) { int j; LOAD(k); for (j = 0; j < 4; j++) b[j] += vz[j]*w1 + vy[j]*w0, a[j] += vz[j]*w0 - vy[j]*w1; } +#define S2(k) { int j; LOAD(k); for (j = 0; j < 4; j++) b[j] += vz[j]*w1 + vy[j]*w0, a[j] += vy[j]*w1 - vz[j]*w0; } + float a[4], b[4]; + + zlin[4*i] = xl[18*(31 - i)]; + zlin[4*i + 1] = xr[18*(31 - i)]; + zlin[4*i + 2] = xl[1 + 18*(31 - i)]; + zlin[4*i + 3] = xr[1 + 18*(31 - i)]; + zlin[4*(i + 16)] = xl[1 + 18*(1 + i)]; + zlin[4*(i + 16) + 1] = xr[1 + 18*(1 + i)]; + zlin[4*(i - 16) + 2] = xl[18*(1 + i)]; + zlin[4*(i - 16) + 3] = xr[18*(1 + i)]; + + S0(0) S2(1) S1(2) S2(3) S1(4) S2(5) S1(6) S2(7) + + dstr[(15 - i)*nch] = mp3d_scale_pcm(a[1]); + dstr[(17 + i)*nch] = mp3d_scale_pcm(b[1]); + dstl[(15 - i)*nch] = mp3d_scale_pcm(a[0]); + dstl[(17 + i)*nch] = mp3d_scale_pcm(b[0]); + dstr[(47 - i)*nch] = mp3d_scale_pcm(a[3]); + dstr[(49 + i)*nch] = mp3d_scale_pcm(b[3]); + dstl[(47 - i)*nch] = mp3d_scale_pcm(a[2]); + dstl[(49 + i)*nch] = mp3d_scale_pcm(b[2]); + } +#endif /* MINIMP3_ONLY_SIMD */ +} + +static void mp3d_synth_granule(float *qmf_state, float *grbuf, int nbands, int nch, mp3d_sample_t *pcm, float *lins) +{ + int i; + for (i = 0; i < nch; i++) + { + mp3d_DCT_II(grbuf + 576*i, nbands); + } + + memcpy(lins, qmf_state, sizeof(float)*15*64); + + for (i = 0; i < nbands; i += 2) + { + mp3d_synth(grbuf + i, pcm + 32*nch*i, nch, lins + i*64); + } +#ifndef MINIMP3_NONSTANDARD_BUT_LOGICAL + if (nch == 1) + { + for (i = 0; i < 15*64; i += 2) + { + qmf_state[i] = lins[nbands*64 + i]; + } + } else +#endif /* MINIMP3_NONSTANDARD_BUT_LOGICAL */ + { + memcpy(qmf_state, lins + nbands*64, sizeof(float)*15*64); + } +} + +static int mp3d_match_frame(const uint8_t *hdr, int mp3_bytes, int frame_bytes) +{ + int i, nmatch; + for (i = 0, nmatch = 0; nmatch < MAX_FRAME_SYNC_MATCHES; nmatch++) + { + i += hdr_frame_bytes(hdr + i, frame_bytes) + hdr_padding(hdr + i); + if (i + HDR_SIZE > mp3_bytes) + return nmatch > 0; + if (!hdr_compare(hdr, hdr + i)) + return 0; + } + return 1; +} + +static int mp3d_find_frame(const uint8_t *mp3, int mp3_bytes, int *free_format_bytes, int *ptr_frame_bytes) +{ + int i, k; + for (i = 0; i < mp3_bytes - HDR_SIZE; i++, mp3++) + { + if (hdr_valid(mp3)) + { + int frame_bytes = hdr_frame_bytes(mp3, *free_format_bytes); + int frame_and_padding = frame_bytes + hdr_padding(mp3); + + for (k = HDR_SIZE; !frame_bytes && k < MAX_FREE_FORMAT_FRAME_SIZE && i + 2*k < mp3_bytes - HDR_SIZE; k++) + { + if (hdr_compare(mp3, mp3 + k)) + { + int fb = k - hdr_padding(mp3); + int nextfb = fb + hdr_padding(mp3 + k); + if (i + k + nextfb + HDR_SIZE > mp3_bytes || !hdr_compare(mp3, mp3 + k + nextfb)) + continue; + frame_and_padding = k; + frame_bytes = fb; + *free_format_bytes = fb; + } + } + if ((frame_bytes && i + frame_and_padding <= mp3_bytes && + mp3d_match_frame(mp3, mp3_bytes - i, frame_bytes)) || + (!i && frame_and_padding == mp3_bytes)) + { + *ptr_frame_bytes = frame_and_padding; + return i; + } + *free_format_bytes = 0; + } + } + *ptr_frame_bytes = 0; + return mp3_bytes; +} + +void mp3dec_init(mp3dec_t *dec) +{ + dec->header[0] = 0; +} + +int mp3dec_decode_frame(mp3dec_t *dec, const uint8_t *mp3, int mp3_bytes, mp3d_sample_t *pcm, mp3dec_frame_info_t *info) +{ + int i = 0, igr, frame_size = 0, success = 1; + const uint8_t *hdr; + bs_t bs_frame[1]; + mp3dec_scratch_t scratch; + + if (mp3_bytes > 4 && dec->header[0] == 0xff && hdr_compare(dec->header, mp3)) + { + frame_size = hdr_frame_bytes(mp3, dec->free_format_bytes) + hdr_padding(mp3); + if (frame_size != mp3_bytes && (frame_size + HDR_SIZE > mp3_bytes || !hdr_compare(mp3, mp3 + frame_size))) + { + frame_size = 0; + } + } + if (!frame_size) + { + memset(dec, 0, sizeof(mp3dec_t)); + i = mp3d_find_frame(mp3, mp3_bytes, &dec->free_format_bytes, &frame_size); + if (!frame_size || i + frame_size > mp3_bytes) + { + info->frame_bytes = i; + return 0; + } + } + + hdr = mp3 + i; + memcpy(dec->header, hdr, HDR_SIZE); + info->frame_bytes = i + frame_size; + info->frame_offset = i; + info->channels = HDR_IS_MONO(hdr) ? 1 : 2; + info->hz = hdr_sample_rate_hz(hdr); + info->layer = 4 - HDR_GET_LAYER(hdr); + info->bitrate_kbps = hdr_bitrate_kbps(hdr); + + if (!pcm) + { + return hdr_frame_samples(hdr); + } + + bs_init(bs_frame, hdr + HDR_SIZE, frame_size - HDR_SIZE); + if (HDR_IS_CRC(hdr)) + { + get_bits(bs_frame, 16); + } + + if (info->layer == 3) + { + int main_data_begin = L3_read_side_info(bs_frame, scratch.gr_info, hdr); + if (main_data_begin < 0 || bs_frame->pos > bs_frame->limit) + { + mp3dec_init(dec); + return 0; + } + success = L3_restore_reservoir(dec, bs_frame, &scratch, main_data_begin); + if (success) + { + for (igr = 0; igr < (HDR_TEST_MPEG1(hdr) ? 2 : 1); igr++, pcm += 576*info->channels) + { + memset(scratch.grbuf[0], 0, 576*2*sizeof(float)); + L3_decode(dec, &scratch, scratch.gr_info + igr*info->channels, info->channels); + mp3d_synth_granule(dec->qmf_state, scratch.grbuf[0], 18, info->channels, pcm, scratch.syn[0]); + } + } + L3_save_reservoir(dec, &scratch); + } else + { +#ifdef MINIMP3_ONLY_MP3 + return 0; +#else /* MINIMP3_ONLY_MP3 */ + L12_scale_info sci[1]; + L12_read_scale_info(hdr, bs_frame, sci); + + memset(scratch.grbuf[0], 0, 576*2*sizeof(float)); + for (i = 0, igr = 0; igr < 3; igr++) + { + if (12 == (i += L12_dequantize_granule(scratch.grbuf[0] + i, bs_frame, sci, info->layer | 1))) + { + i = 0; + L12_apply_scf_384(sci, sci->scf + igr, scratch.grbuf[0]); + mp3d_synth_granule(dec->qmf_state, scratch.grbuf[0], 12, info->channels, pcm, scratch.syn[0]); + memset(scratch.grbuf[0], 0, 576*2*sizeof(float)); + pcm += 384*info->channels; + } + if (bs_frame->pos > bs_frame->limit) + { + mp3dec_init(dec); + return 0; + } + } +#endif /* MINIMP3_ONLY_MP3 */ + } + return success*hdr_frame_samples(dec->header); +} + +#ifdef MINIMP3_FLOAT_OUTPUT +void mp3dec_f32_to_s16(const float *in, int16_t *out, int num_samples) +{ + int i = 0; +#if HAVE_SIMD + int aligned_count = num_samples & ~7; + for(; i < aligned_count; i += 8) + { + static const f4 g_scale = { 32768.0f, 32768.0f, 32768.0f, 32768.0f }; + f4 a = VMUL(VLD(&in[i ]), g_scale); + f4 b = VMUL(VLD(&in[i+4]), g_scale); +#if HAVE_SSE + static const f4 g_max = { 32767.0f, 32767.0f, 32767.0f, 32767.0f }; + static const f4 g_min = { -32768.0f, -32768.0f, -32768.0f, -32768.0f }; + __m128i pcm8 = _mm_packs_epi32(_mm_cvtps_epi32(_mm_max_ps(_mm_min_ps(a, g_max), g_min)), + _mm_cvtps_epi32(_mm_max_ps(_mm_min_ps(b, g_max), g_min))); + out[i ] = _mm_extract_epi16(pcm8, 0); + out[i+1] = _mm_extract_epi16(pcm8, 1); + out[i+2] = _mm_extract_epi16(pcm8, 2); + out[i+3] = _mm_extract_epi16(pcm8, 3); + out[i+4] = _mm_extract_epi16(pcm8, 4); + out[i+5] = _mm_extract_epi16(pcm8, 5); + out[i+6] = _mm_extract_epi16(pcm8, 6); + out[i+7] = _mm_extract_epi16(pcm8, 7); +#else /* HAVE_SSE */ + int16x4_t pcma, pcmb; + a = VADD(a, VSET(0.5f)); + b = VADD(b, VSET(0.5f)); + pcma = vqmovn_s32(vqaddq_s32(vcvtq_s32_f32(a), vreinterpretq_s32_u32(vcltq_f32(a, VSET(0))))); + pcmb = vqmovn_s32(vqaddq_s32(vcvtq_s32_f32(b), vreinterpretq_s32_u32(vcltq_f32(b, VSET(0))))); + vst1_lane_s16(out+i , pcma, 0); + vst1_lane_s16(out+i+1, pcma, 1); + vst1_lane_s16(out+i+2, pcma, 2); + vst1_lane_s16(out+i+3, pcma, 3); + vst1_lane_s16(out+i+4, pcmb, 0); + vst1_lane_s16(out+i+5, pcmb, 1); + vst1_lane_s16(out+i+6, pcmb, 2); + vst1_lane_s16(out+i+7, pcmb, 3); +#endif /* HAVE_SSE */ + } +#endif /* HAVE_SIMD */ + for(; i < num_samples; i++) + { + float sample = in[i] * 32768.0f; + if (sample >= 32766.5) + out[i] = (int16_t) 32767; + else if (sample <= -32767.5) + out[i] = (int16_t)-32768; + else + { + int16_t s = (int16_t)(sample + .5f); + s -= (s < 0); /* away from zero, to be compliant */ + out[i] = s; + } + } +} +#endif /* MINIMP3_FLOAT_OUTPUT */ +#endif /* MINIMP3_IMPLEMENTATION && !_MINIMP3_IMPLEMENTATION_GUARD */ diff --git a/src/libs/minimp3/minimp3_ex.h b/src/libs/minimp3/minimp3_ex.h new file mode 100644 index 00000000..2871705d --- /dev/null +++ b/src/libs/minimp3/minimp3_ex.h @@ -0,0 +1,1397 @@ +#ifndef MINIMP3_EXT_H +#define MINIMP3_EXT_H +/* + https://github.com/lieff/minimp3 + To the extent possible under law, the author(s) have dedicated all copyright and related and neighboring rights to this software to the public domain worldwide. + This software is distributed without any warranty. + See . +*/ +#include +#include "minimp3.h" + +/* flags for mp3dec_ex_open_* functions */ +#define MP3D_SEEK_TO_BYTE 0 /* mp3dec_ex_seek seeks to byte in stream */ +#define MP3D_SEEK_TO_SAMPLE 1 /* mp3dec_ex_seek precisely seeks to sample using index (created during duration calculation scan or when mp3dec_ex_seek called) */ +#define MP3D_DO_NOT_SCAN 2 /* do not scan whole stream for duration if vbrtag not found, mp3dec_ex_t::samples will be filled only if mp3dec_ex_t::vbr_tag_found == 1 */ +#ifdef MINIMP3_ALLOW_MONO_STEREO_TRANSITION +#define MP3D_ALLOW_MONO_STEREO_TRANSITION 4 +#define MP3D_FLAGS_MASK 7 +#else +#define MP3D_FLAGS_MASK 3 +#endif + +/* compile-time config */ +#define MINIMP3_PREDECODE_FRAMES 2 /* frames to pre-decode and skip after seek (to fill internal structures) */ +/*#define MINIMP3_SEEK_IDX_LINEAR_SEARCH*/ /* define to use linear index search instead of binary search on seek */ +#define MINIMP3_IO_SIZE (128*1024) /* io buffer size for streaming functions, must be greater than MINIMP3_BUF_SIZE */ +#define MINIMP3_BUF_SIZE (16*1024) /* buffer which can hold minimum 10 consecutive mp3 frames (~16KB) worst case */ +/*#define MINIMP3_SCAN_LIMIT (256*1024)*/ /* how many bytes will be scanned to search first valid mp3 frame, to prevent stall on large non-mp3 files */ +#define MINIMP3_ENABLE_RING 0 /* WIP enable hardware magic ring buffer if available, to make less input buffer memmove(s) in callback IO mode */ + +/* return error codes */ +#define MP3D_E_PARAM -1 +#define MP3D_E_MEMORY -2 +#define MP3D_E_IOERROR -3 +#define MP3D_E_USER -4 /* can be used to stop processing from callbacks without indicating specific error */ +#define MP3D_E_DECODE -5 /* decode error which can't be safely skipped, such as sample rate, layer and channels change */ + +typedef struct +{ + mp3d_sample_t *buffer; + size_t samples; /* channels included, byte size = samples*sizeof(mp3d_sample_t) */ + int channels, hz, layer, avg_bitrate_kbps; +} mp3dec_file_info_t; + +typedef struct +{ + const uint8_t *buffer; + size_t size; +} mp3dec_map_info_t; + +typedef struct +{ + uint64_t sample; + uint64_t offset; +} mp3dec_frame_t; + +typedef struct +{ + mp3dec_frame_t *frames; + size_t num_frames, capacity; +} mp3dec_index_t; + +typedef size_t (*MP3D_READ_CB)(void *buf, size_t size, void *user_data); +typedef int (*MP3D_SEEK_CB)(uint64_t position, void *user_data); + +typedef struct +{ + MP3D_READ_CB read; + void *read_data; + MP3D_SEEK_CB seek; + void *seek_data; +} mp3dec_io_t; + +typedef struct +{ + mp3dec_t mp3d; + mp3dec_map_info_t file; + mp3dec_io_t *io; + mp3dec_index_t index; + uint64_t offset, samples, detected_samples, cur_sample, start_offset, end_offset; + mp3dec_frame_info_t info; + mp3d_sample_t buffer[MINIMP3_MAX_SAMPLES_PER_FRAME]; + size_t input_consumed, input_filled; + int is_file, flags, vbr_tag_found, indexes_built; + int free_format_bytes; + int buffer_samples, buffer_consumed, to_skip, start_delay; + int last_error; +} mp3dec_ex_t; + +typedef int (*MP3D_ITERATE_CB)(void *user_data, const uint8_t *frame, int frame_size, int free_format_bytes, size_t buf_size, uint64_t offset, mp3dec_frame_info_t *info); +typedef int (*MP3D_PROGRESS_CB)(void *user_data, size_t file_size, uint64_t offset, mp3dec_frame_info_t *info); + +#ifdef __cplusplus +extern "C" { +#endif + +/* detect mp3/mpa format */ +int mp3dec_detect_buf(const uint8_t *buf, size_t buf_size); +int mp3dec_detect_cb(mp3dec_io_t *io, uint8_t *buf, size_t buf_size); +/* decode whole buffer block */ +int mp3dec_load_buf(mp3dec_t *dec, const uint8_t *buf, size_t buf_size, mp3dec_file_info_t *info, MP3D_PROGRESS_CB progress_cb, void *user_data); +int mp3dec_load_cb(mp3dec_t *dec, mp3dec_io_t *io, uint8_t *buf, size_t buf_size, mp3dec_file_info_t *info, MP3D_PROGRESS_CB progress_cb, void *user_data); +/* iterate through frames */ +int mp3dec_iterate_buf(const uint8_t *buf, size_t buf_size, MP3D_ITERATE_CB callback, void *user_data); +int mp3dec_iterate_cb(mp3dec_io_t *io, uint8_t *buf, size_t buf_size, MP3D_ITERATE_CB callback, void *user_data); +/* streaming decoder with seeking capability */ +int mp3dec_ex_open_buf(mp3dec_ex_t *dec, const uint8_t *buf, size_t buf_size, int flags); +int mp3dec_ex_open_cb(mp3dec_ex_t *dec, mp3dec_io_t *io, int flags); +void mp3dec_ex_close(mp3dec_ex_t *dec); +int mp3dec_ex_seek(mp3dec_ex_t *dec, uint64_t position); +size_t mp3dec_ex_read_frame(mp3dec_ex_t *dec, mp3d_sample_t **buf, mp3dec_frame_info_t *frame_info, size_t max_samples); +size_t mp3dec_ex_read(mp3dec_ex_t *dec, mp3d_sample_t *buf, size_t samples); +#ifndef MINIMP3_NO_STDIO +/* stdio versions of file detect, load, iterate and stream */ +int mp3dec_detect(const char *file_name); +int mp3dec_load(mp3dec_t *dec, const char *file_name, mp3dec_file_info_t *info, MP3D_PROGRESS_CB progress_cb, void *user_data); +int mp3dec_iterate(const char *file_name, MP3D_ITERATE_CB callback, void *user_data); +int mp3dec_ex_open(mp3dec_ex_t *dec, const char *file_name, int flags); +#ifdef _WIN32 +int mp3dec_detect_w(const wchar_t *file_name); +int mp3dec_load_w(mp3dec_t *dec, const wchar_t *file_name, mp3dec_file_info_t *info, MP3D_PROGRESS_CB progress_cb, void *user_data); +int mp3dec_iterate_w(const wchar_t *file_name, MP3D_ITERATE_CB callback, void *user_data); +int mp3dec_ex_open_w(mp3dec_ex_t *dec, const wchar_t *file_name, int flags); +#endif +#endif + +#ifdef __cplusplus +} +#endif +#endif /*MINIMP3_EXT_H*/ + +#if defined(MINIMP3_IMPLEMENTATION) && !defined(_MINIMP3_EX_IMPLEMENTATION_GUARD) +#define _MINIMP3_EX_IMPLEMENTATION_GUARD +#include +#include "minimp3.h" + +static void mp3dec_skip_id3v1(const uint8_t *buf, size_t *pbuf_size) +{ + size_t buf_size = *pbuf_size; +#ifndef MINIMP3_NOSKIP_ID3V1 + if (buf_size >= 128 && !memcmp(buf + buf_size - 128, "TAG", 3)) + { + buf_size -= 128; + if (buf_size >= 227 && !memcmp(buf + buf_size - 227, "TAG+", 4)) + buf_size -= 227; + } +#endif +#ifndef MINIMP3_NOSKIP_APEV2 + if (buf_size > 32 && !memcmp(buf + buf_size - 32, "APETAGEX", 8)) + { + buf_size -= 32; + const uint8_t *tag = buf + buf_size + 8 + 4; + uint32_t tag_size = (uint32_t)(tag[3] << 24) | (tag[2] << 16) | (tag[1] << 8) | tag[0]; + if (buf_size >= tag_size) + buf_size -= tag_size; + } +#endif + *pbuf_size = buf_size; +} + +static size_t mp3dec_skip_id3v2(const uint8_t *buf, size_t buf_size) +{ +#define MINIMP3_ID3_DETECT_SIZE 10 +#ifndef MINIMP3_NOSKIP_ID3V2 + if (buf_size >= MINIMP3_ID3_DETECT_SIZE && !memcmp(buf, "ID3", 3) && !((buf[5] & 15) || (buf[6] & 0x80) || (buf[7] & 0x80) || (buf[8] & 0x80) || (buf[9] & 0x80))) + { + size_t id3v2size = (((buf[6] & 0x7f) << 21) | ((buf[7] & 0x7f) << 14) | ((buf[8] & 0x7f) << 7) | (buf[9] & 0x7f)) + 10; + if ((buf[5] & 16)) + id3v2size += 10; /* footer */ + return id3v2size; + } +#endif + return 0; +} + +static void mp3dec_skip_id3(const uint8_t **pbuf, size_t *pbuf_size) +{ + uint8_t *buf = (uint8_t *)(*pbuf); + size_t buf_size = *pbuf_size; + size_t id3v2size = mp3dec_skip_id3v2(buf, buf_size); + if (id3v2size) + { + if (id3v2size >= buf_size) + id3v2size = buf_size; + buf += id3v2size; + buf_size -= id3v2size; + } + mp3dec_skip_id3v1(buf, &buf_size); + *pbuf = (const uint8_t *)buf; + *pbuf_size = buf_size; +} + +static int mp3dec_check_vbrtag(const uint8_t *frame, int frame_size, uint32_t *frames, int *delay, int *padding) +{ + static const char g_xing_tag[4] = { 'X', 'i', 'n', 'g' }; + static const char g_info_tag[4] = { 'I', 'n', 'f', 'o' }; +#define FRAMES_FLAG 1 +#define BYTES_FLAG 2 +#define TOC_FLAG 4 +#define VBR_SCALE_FLAG 8 + /* Side info offsets after header: + / Mono Stereo + / MPEG1 17 32 + / MPEG2 & 2.5 9 17*/ + bs_t bs[1]; + L3_gr_info_t gr_info[4]; + bs_init(bs, frame + HDR_SIZE, frame_size - HDR_SIZE); + if (HDR_IS_CRC(frame)) + get_bits(bs, 16); + if (L3_read_side_info(bs, gr_info, frame) < 0) + return 0; /* side info corrupted */ + + const uint8_t *tag = frame + HDR_SIZE + bs->pos/8; + if (memcmp(g_xing_tag, tag, 4) && memcmp(g_info_tag, tag, 4)) + return 0; + int flags = tag[7]; + if (!((flags & FRAMES_FLAG))) + return -1; + tag += 8; + *frames = (uint32_t)(tag[0] << 24) | (tag[1] << 16) | (tag[2] << 8) | tag[3]; + tag += 4; + if (flags & BYTES_FLAG) + tag += 4; + if (flags & TOC_FLAG) + tag += 100; + if (flags & VBR_SCALE_FLAG) + tag += 4; + *delay = *padding = 0; + if (*tag) + { /* extension, LAME, Lavc, etc. Should be the same structure. */ + tag += 21; + if (tag - frame + 14 >= frame_size) + return 0; + *delay = ((tag[0] << 4) | (tag[1] >> 4)) + (528 + 1); + *padding = (((tag[1] & 0xF) << 8) | tag[2]) - (528 + 1); + } + return 1; +} + +int mp3dec_detect_buf(const uint8_t *buf, size_t buf_size) +{ + return mp3dec_detect_cb(0, (uint8_t *)buf, buf_size); +} + +int mp3dec_detect_cb(mp3dec_io_t *io, uint8_t *buf, size_t buf_size) +{ + if (!buf || (size_t)-1 == buf_size || (io && buf_size < MINIMP3_BUF_SIZE)) + return MP3D_E_PARAM; + size_t filled = buf_size; + if (io) + { + if (io->seek(0, io->seek_data)) + return MP3D_E_IOERROR; + filled = io->read(buf, MINIMP3_ID3_DETECT_SIZE, io->read_data); + if (filled > MINIMP3_ID3_DETECT_SIZE) + return MP3D_E_IOERROR; + } + if (filled < MINIMP3_ID3_DETECT_SIZE) + return MP3D_E_USER; /* too small, can't be mp3/mpa */ + if (mp3dec_skip_id3v2(buf, filled)) + return 0; /* id3v2 tag is enough evidence */ + if (io) + { + size_t readed = io->read(buf + MINIMP3_ID3_DETECT_SIZE, buf_size - MINIMP3_ID3_DETECT_SIZE, io->read_data); + if (readed > (buf_size - MINIMP3_ID3_DETECT_SIZE)) + return MP3D_E_IOERROR; + filled += readed; + if (filled < MINIMP3_BUF_SIZE) + mp3dec_skip_id3v1(buf, &filled); + } else + { + mp3dec_skip_id3v1(buf, &filled); + if (filled > MINIMP3_BUF_SIZE) + filled = MINIMP3_BUF_SIZE; + } + int free_format_bytes, frame_size; + mp3d_find_frame(buf, filled, &free_format_bytes, &frame_size); + if (frame_size) + return 0; /* MAX_FRAME_SYNC_MATCHES consecutive frames found */ + return MP3D_E_USER; +} + +int mp3dec_load_buf(mp3dec_t *dec, const uint8_t *buf, size_t buf_size, mp3dec_file_info_t *info, MP3D_PROGRESS_CB progress_cb, void *user_data) +{ + return mp3dec_load_cb(dec, 0, (uint8_t *)buf, buf_size, info, progress_cb, user_data); +} + +int mp3dec_load_cb(mp3dec_t *dec, mp3dec_io_t *io, uint8_t *buf, size_t buf_size, mp3dec_file_info_t *info, MP3D_PROGRESS_CB progress_cb, void *user_data) +{ + if (!dec || !buf || !info || (size_t)-1 == buf_size || (io && buf_size < MINIMP3_BUF_SIZE)) + return MP3D_E_PARAM; + uint64_t detected_samples = 0; + size_t orig_buf_size = buf_size; + int to_skip = 0; + mp3dec_frame_info_t frame_info; + memset(info, 0, sizeof(*info)); + memset(&frame_info, 0, sizeof(frame_info)); + + /* skip id3 */ + size_t filled = 0, consumed = 0; + int eof = 0, ret = 0; + if (io) + { + if (io->seek(0, io->seek_data)) + return MP3D_E_IOERROR; + filled = io->read(buf, MINIMP3_ID3_DETECT_SIZE, io->read_data); + if (filled > MINIMP3_ID3_DETECT_SIZE) + return MP3D_E_IOERROR; + if (MINIMP3_ID3_DETECT_SIZE != filled) + return 0; + size_t id3v2size = mp3dec_skip_id3v2(buf, filled); + if (id3v2size) + { + if (io->seek(id3v2size, io->seek_data)) + return MP3D_E_IOERROR; + filled = io->read(buf, buf_size, io->read_data); + if (filled > buf_size) + return MP3D_E_IOERROR; + } else + { + size_t readed = io->read(buf + MINIMP3_ID3_DETECT_SIZE, buf_size - MINIMP3_ID3_DETECT_SIZE, io->read_data); + if (readed > (buf_size - MINIMP3_ID3_DETECT_SIZE)) + return MP3D_E_IOERROR; + filled += readed; + } + if (filled < MINIMP3_BUF_SIZE) + mp3dec_skip_id3v1(buf, &filled); + } else + { + mp3dec_skip_id3((const uint8_t **)&buf, &buf_size); + if (!buf_size) + return 0; + } + /* try to make allocation size assumption by first frame or vbr tag */ + mp3dec_init(dec); + int samples; + do + { + uint32_t frames; + int i, delay, padding, free_format_bytes = 0, frame_size = 0; + const uint8_t *hdr; + if (io) + { + if (!eof && filled - consumed < MINIMP3_BUF_SIZE) + { /* keep minimum 10 consecutive mp3 frames (~16KB) worst case */ + memmove(buf, buf + consumed, filled - consumed); + filled -= consumed; + consumed = 0; + size_t readed = io->read(buf + filled, buf_size - filled, io->read_data); + if (readed > (buf_size - filled)) + return MP3D_E_IOERROR; + if (readed != (buf_size - filled)) + eof = 1; + filled += readed; + if (eof) + mp3dec_skip_id3v1(buf, &filled); + } + i = mp3d_find_frame(buf + consumed, filled - consumed, &free_format_bytes, &frame_size); + consumed += i; + hdr = buf + consumed; + } else + { + i = mp3d_find_frame(buf, buf_size, &free_format_bytes, &frame_size); + buf += i; + buf_size -= i; + hdr = buf; + } + if (i && !frame_size) + continue; + if (!frame_size) + return 0; + frame_info.channels = HDR_IS_MONO(hdr) ? 1 : 2; + frame_info.hz = hdr_sample_rate_hz(hdr); + frame_info.layer = 4 - HDR_GET_LAYER(hdr); + frame_info.bitrate_kbps = hdr_bitrate_kbps(hdr); + frame_info.frame_bytes = frame_size; + samples = hdr_frame_samples(hdr)*frame_info.channels; + if (3 != frame_info.layer) + break; + int ret = mp3dec_check_vbrtag(hdr, frame_size, &frames, &delay, &padding); + if (ret > 0) + { + padding *= frame_info.channels; + to_skip = delay*frame_info.channels; + detected_samples = samples*(uint64_t)frames; + if (detected_samples >= (uint64_t)to_skip) + detected_samples -= to_skip; + if (padding > 0 && detected_samples >= (uint64_t)padding) + detected_samples -= padding; + if (!detected_samples) + return 0; + } + if (ret) + { + if (io) + { + consumed += frame_size; + } else + { + buf += frame_size; + buf_size -= frame_size; + } + } + break; + } while(1); + size_t allocated = MINIMP3_MAX_SAMPLES_PER_FRAME*sizeof(mp3d_sample_t); + if (detected_samples) + allocated += detected_samples*sizeof(mp3d_sample_t); + else + allocated += (buf_size/frame_info.frame_bytes)*samples*sizeof(mp3d_sample_t); + info->buffer = (mp3d_sample_t*)malloc(allocated); + if (!info->buffer) + return MP3D_E_MEMORY; + /* save info */ + info->channels = frame_info.channels; + info->hz = frame_info.hz; + info->layer = frame_info.layer; + /* decode all frames */ + size_t avg_bitrate_kbps = 0, frames = 0; + do + { + if ((allocated - info->samples*sizeof(mp3d_sample_t)) < MINIMP3_MAX_SAMPLES_PER_FRAME*sizeof(mp3d_sample_t)) + { + allocated *= 2; + mp3d_sample_t *alloc_buf = (mp3d_sample_t*)realloc(info->buffer, allocated); + if (!alloc_buf) + return MP3D_E_MEMORY; + info->buffer = alloc_buf; + } + if (io) + { + if (!eof && filled - consumed < MINIMP3_BUF_SIZE) + { /* keep minimum 10 consecutive mp3 frames (~16KB) worst case */ + memmove(buf, buf + consumed, filled - consumed); + filled -= consumed; + consumed = 0; + size_t readed = io->read(buf + filled, buf_size - filled, io->read_data); + if (readed != (buf_size - filled)) + eof = 1; + filled += readed; + if (eof) + mp3dec_skip_id3v1(buf, &filled); + } + samples = mp3dec_decode_frame(dec, buf + consumed, filled - consumed, info->buffer + info->samples, &frame_info); + consumed += frame_info.frame_bytes; + } else + { + samples = mp3dec_decode_frame(dec, buf, MINIMP3_MIN(buf_size, (size_t)INT_MAX), info->buffer + info->samples, &frame_info); + buf += frame_info.frame_bytes; + buf_size -= frame_info.frame_bytes; + } + if (samples) + { + if (info->hz != frame_info.hz || info->layer != frame_info.layer) + { + ret = MP3D_E_DECODE; + break; + } + if (info->channels && info->channels != frame_info.channels) + { +#ifdef MINIMP3_ALLOW_MONO_STEREO_TRANSITION + info->channels = 0; /* mark file with mono-stereo transition */ +#else + ret = MP3D_E_DECODE; + break; +#endif + } + samples *= frame_info.channels; + if (to_skip) + { + size_t skip = MINIMP3_MIN(samples, to_skip); + to_skip -= skip; + samples -= skip; + memmove(info->buffer, info->buffer + skip, samples*sizeof(mp3d_sample_t)); + } + info->samples += samples; + avg_bitrate_kbps += frame_info.bitrate_kbps; + frames++; + if (progress_cb) + { + ret = progress_cb(user_data, orig_buf_size, orig_buf_size - buf_size, &frame_info); + if (ret) + break; + } + } + } while (frame_info.frame_bytes); + if (detected_samples && info->samples > detected_samples) + info->samples = detected_samples; /* cut padding */ + /* reallocate to normal buffer size */ + if (allocated != info->samples*sizeof(mp3d_sample_t)) + { + mp3d_sample_t *alloc_buf = (mp3d_sample_t*)realloc(info->buffer, info->samples*sizeof(mp3d_sample_t)); + if (!alloc_buf && info->samples) + return MP3D_E_MEMORY; + info->buffer = alloc_buf; + } + if (frames) + info->avg_bitrate_kbps = avg_bitrate_kbps/frames; + return ret; +} + +int mp3dec_iterate_buf(const uint8_t *buf, size_t buf_size, MP3D_ITERATE_CB callback, void *user_data) +{ + const uint8_t *orig_buf = buf; + if (!buf || (size_t)-1 == buf_size || !callback) + return MP3D_E_PARAM; + /* skip id3 */ + mp3dec_skip_id3(&buf, &buf_size); + if (!buf_size) + return 0; + mp3dec_frame_info_t frame_info; + memset(&frame_info, 0, sizeof(frame_info)); + do + { + int free_format_bytes = 0, frame_size = 0, ret; + int i = mp3d_find_frame(buf, buf_size, &free_format_bytes, &frame_size); + buf += i; + buf_size -= i; + if (i && !frame_size) + continue; + if (!frame_size) + break; + const uint8_t *hdr = buf; + frame_info.channels = HDR_IS_MONO(hdr) ? 1 : 2; + frame_info.hz = hdr_sample_rate_hz(hdr); + frame_info.layer = 4 - HDR_GET_LAYER(hdr); + frame_info.bitrate_kbps = hdr_bitrate_kbps(hdr); + frame_info.frame_bytes = frame_size; + + if (callback) + { + if ((ret = callback(user_data, hdr, frame_size, free_format_bytes, buf_size, hdr - orig_buf, &frame_info))) + return ret; + } + buf += frame_size; + buf_size -= frame_size; + } while (1); + return 0; +} + +int mp3dec_iterate_cb(mp3dec_io_t *io, uint8_t *buf, size_t buf_size, MP3D_ITERATE_CB callback, void *user_data) +{ + if (!io || !buf || (size_t)-1 == buf_size || buf_size < MINIMP3_BUF_SIZE || !callback) + return MP3D_E_PARAM; + size_t filled = io->read(buf, MINIMP3_ID3_DETECT_SIZE, io->read_data), consumed = 0; + uint64_t readed = 0; + mp3dec_frame_info_t frame_info; + int eof = 0; + memset(&frame_info, 0, sizeof(frame_info)); + if (filled > MINIMP3_ID3_DETECT_SIZE) + return MP3D_E_IOERROR; + if (MINIMP3_ID3_DETECT_SIZE != filled) + return 0; + size_t id3v2size = mp3dec_skip_id3v2(buf, filled); + if (id3v2size) + { + if (io->seek(id3v2size, io->seek_data)) + return MP3D_E_IOERROR; + filled = io->read(buf, buf_size, io->read_data); + if (filled > buf_size) + return MP3D_E_IOERROR; + readed += id3v2size; + } else + { + size_t readed = io->read(buf + MINIMP3_ID3_DETECT_SIZE, buf_size - MINIMP3_ID3_DETECT_SIZE, io->read_data); + if (readed > (buf_size - MINIMP3_ID3_DETECT_SIZE)) + return MP3D_E_IOERROR; + filled += readed; + } + if (filled < MINIMP3_BUF_SIZE) + mp3dec_skip_id3v1(buf, &filled); + do + { + int free_format_bytes = 0, frame_size = 0, ret; + int i = mp3d_find_frame(buf + consumed, filled - consumed, &free_format_bytes, &frame_size); + if (i && !frame_size) + { + consumed += i; + continue; + } + if (!frame_size) + break; + const uint8_t *hdr = buf + consumed + i; + frame_info.channels = HDR_IS_MONO(hdr) ? 1 : 2; + frame_info.hz = hdr_sample_rate_hz(hdr); + frame_info.layer = 4 - HDR_GET_LAYER(hdr); + frame_info.bitrate_kbps = hdr_bitrate_kbps(hdr); + frame_info.frame_bytes = frame_size; + + readed += i; + if (callback) + { + if ((ret = callback(user_data, hdr, frame_size, free_format_bytes, filled - consumed, readed, &frame_info))) + return ret; + } + readed += frame_size; + consumed += i + frame_size; + if (!eof && filled - consumed < MINIMP3_BUF_SIZE) + { /* keep minimum 10 consecutive mp3 frames (~16KB) worst case */ + memmove(buf, buf + consumed, filled - consumed); + filled -= consumed; + consumed = 0; + size_t readed = io->read(buf + filled, buf_size - filled, io->read_data); + if (readed > (buf_size - filled)) + return MP3D_E_IOERROR; + if (readed != (buf_size - filled)) + eof = 1; + filled += readed; + if (eof) + mp3dec_skip_id3v1(buf, &filled); + } + } while (1); + return 0; +} + +static int mp3dec_load_index(void *user_data, const uint8_t *frame, int frame_size, int free_format_bytes, size_t buf_size, uint64_t offset, mp3dec_frame_info_t *info) +{ + mp3dec_frame_t *idx_frame; + mp3dec_ex_t *dec = (mp3dec_ex_t *)user_data; + if (!dec->index.frames && !dec->start_offset) + { /* detect VBR tag and try to avoid full scan */ + uint32_t frames; + int delay, padding; + dec->info = *info; + dec->start_offset = dec->offset = offset; + dec->end_offset = offset + buf_size; + dec->free_format_bytes = free_format_bytes; /* should not change */ + if (3 == dec->info.layer) + { + int ret = mp3dec_check_vbrtag(frame, frame_size, &frames, &delay, &padding); + if (ret) + dec->start_offset = dec->offset = offset + frame_size; + if (ret > 0) + { + padding *= info->channels; + dec->start_delay = dec->to_skip = delay*info->channels; + dec->samples = hdr_frame_samples(frame)*info->channels*(uint64_t)frames; + if (dec->samples >= (uint64_t)dec->start_delay) + dec->samples -= dec->start_delay; + if (padding > 0 && dec->samples >= (uint64_t)padding) + dec->samples -= padding; + dec->detected_samples = dec->samples; + dec->vbr_tag_found = 1; + return MP3D_E_USER; + } else if (ret < 0) + return 0; + } + } + if (dec->flags & MP3D_DO_NOT_SCAN) + return MP3D_E_USER; + if (dec->index.num_frames + 1 > dec->index.capacity) + { + if (!dec->index.capacity) + dec->index.capacity = 4096; + else + dec->index.capacity *= 2; + mp3dec_frame_t *alloc_buf = (mp3dec_frame_t *)realloc((void*)dec->index.frames, sizeof(mp3dec_frame_t)*dec->index.capacity); + if (!alloc_buf) + return MP3D_E_MEMORY; + dec->index.frames = alloc_buf; + } + idx_frame = &dec->index.frames[dec->index.num_frames++]; + idx_frame->offset = offset; + idx_frame->sample = dec->samples; + if (!dec->buffer_samples && dec->index.num_frames < 256) + { /* for some cutted mp3 frames, bit-reservoir not filled and decoding can't be started from first frames */ + /* try to decode up to 255 first frames till samples starts to decode */ + dec->buffer_samples = mp3dec_decode_frame(&dec->mp3d, frame, MINIMP3_MIN(buf_size, (size_t)INT_MAX), dec->buffer, info); + dec->samples += dec->buffer_samples*info->channels; + } else + dec->samples += hdr_frame_samples(frame)*info->channels; + return 0; +} + +int mp3dec_ex_open_buf(mp3dec_ex_t *dec, const uint8_t *buf, size_t buf_size, int flags) +{ + if (!dec || !buf || (size_t)-1 == buf_size || (flags & (~MP3D_FLAGS_MASK))) + return MP3D_E_PARAM; + memset(dec, 0, sizeof(*dec)); + dec->file.buffer = buf; + dec->file.size = buf_size; + dec->flags = flags; + mp3dec_init(&dec->mp3d); + int ret = mp3dec_iterate_buf(dec->file.buffer, dec->file.size, mp3dec_load_index, dec); + if (ret && MP3D_E_USER != ret) + return ret; + mp3dec_init(&dec->mp3d); + dec->buffer_samples = 0; + dec->indexes_built = !(dec->vbr_tag_found || (flags & MP3D_DO_NOT_SCAN)); + dec->flags &= (~MP3D_DO_NOT_SCAN); + return 0; +} + +#ifndef MINIMP3_SEEK_IDX_LINEAR_SEARCH +static size_t mp3dec_idx_binary_search(mp3dec_index_t *idx, uint64_t position) +{ + size_t end = idx->num_frames, start = 0, index = 0; + while (start <= end) + { + size_t mid = (start + end) / 2; + if (idx->frames[mid].sample >= position) + { /* move left side. */ + if (idx->frames[mid].sample == position) + return mid; + end = mid - 1; + } else + { /* move to right side */ + index = mid; + start = mid + 1; + if (start == idx->num_frames) + break; + } + } + return index; +} +#endif + +int mp3dec_ex_seek(mp3dec_ex_t *dec, uint64_t position) +{ + size_t i; + if (!dec) + return MP3D_E_PARAM; + if (!(dec->flags & MP3D_SEEK_TO_SAMPLE)) + { + if (dec->io) + { + dec->offset = position; + } else + { + dec->offset = MINIMP3_MIN(position, dec->file.size); + } + dec->cur_sample = 0; + goto do_exit; + } + dec->cur_sample = position; + position += dec->start_delay; + if (0 == position) + { /* optimize seek to zero, no index needed */ +seek_zero: + dec->offset = dec->start_offset; + dec->to_skip = 0; + goto do_exit; + } + if (!dec->indexes_built) + { /* no index created yet (vbr tag used to calculate track length or MP3D_DO_NOT_SCAN open flag used) */ + dec->indexes_built = 1; + dec->samples = 0; + dec->buffer_samples = 0; + if (dec->io) + { + if (dec->io->seek(dec->start_offset, dec->io->seek_data)) + return MP3D_E_IOERROR; + int ret = mp3dec_iterate_cb(dec->io, (uint8_t *)dec->file.buffer, dec->file.size, mp3dec_load_index, dec); + if (ret && MP3D_E_USER != ret) + return ret; + } else + { + int ret = mp3dec_iterate_buf(dec->file.buffer + dec->start_offset, dec->file.size - dec->start_offset, mp3dec_load_index, dec); + if (ret && MP3D_E_USER != ret) + return ret; + } + for (i = 0; i < dec->index.num_frames; i++) + dec->index.frames[i].offset += dec->start_offset; + dec->samples = dec->detected_samples; + } + if (!dec->index.frames) + goto seek_zero; /* no frames in file - seek to zero */ +#ifdef MINIMP3_SEEK_IDX_LINEAR_SEARCH + for (i = 0; i < dec->index.num_frames; i++) + { + if (dec->index.frames[i].sample >= position) + break; + } +#else + i = mp3dec_idx_binary_search(&dec->index, position); +#endif + if (i) + { + int to_fill_bytes = 511; + int skip_frames = MINIMP3_PREDECODE_FRAMES +#ifdef MINIMP3_SEEK_IDX_LINEAR_SEARCH + + ((dec->index.frames[i].sample == position) ? 0 : 1) +#endif + ; + i -= MINIMP3_MIN(i, (size_t)skip_frames); + if (3 == dec->info.layer) + { + while (i && to_fill_bytes) + { /* make sure bit-reservoir is filled when we start decoding */ + bs_t bs[1]; + L3_gr_info_t gr_info[4]; + int frame_bytes, frame_size; + const uint8_t *hdr; + if (dec->io) + { + hdr = dec->file.buffer; + if (dec->io->seek(dec->index.frames[i - 1].offset, dec->io->seek_data)) + return MP3D_E_IOERROR; + size_t readed = dec->io->read((uint8_t *)hdr, HDR_SIZE, dec->io->read_data); + if (readed != HDR_SIZE) + return MP3D_E_IOERROR; + frame_size = hdr_frame_bytes(hdr, dec->free_format_bytes) + hdr_padding(hdr); + readed = dec->io->read((uint8_t *)hdr + HDR_SIZE, frame_size - HDR_SIZE, dec->io->read_data); + if (readed != (size_t)(frame_size - HDR_SIZE)) + return MP3D_E_IOERROR; + bs_init(bs, hdr + HDR_SIZE, frame_size - HDR_SIZE); + } else + { + hdr = dec->file.buffer + dec->index.frames[i - 1].offset; + frame_size = hdr_frame_bytes(hdr, dec->free_format_bytes) + hdr_padding(hdr); + bs_init(bs, hdr + HDR_SIZE, frame_size - HDR_SIZE); + } + if (HDR_IS_CRC(hdr)) + get_bits(bs, 16); + i--; + if (L3_read_side_info(bs, gr_info, hdr) < 0) + break; /* frame not decodable, we can start from here */ + frame_bytes = (bs->limit - bs->pos)/8; + to_fill_bytes -= MINIMP3_MIN(to_fill_bytes, frame_bytes); + } + } + } + dec->offset = dec->index.frames[i].offset; + dec->to_skip = position - dec->index.frames[i].sample; + while ((i + 1) < dec->index.num_frames && !dec->index.frames[i].sample && !dec->index.frames[i + 1].sample) + { /* skip not decodable first frames */ + const uint8_t *hdr; + if (dec->io) + { + hdr = dec->file.buffer; + if (dec->io->seek(dec->index.frames[i].offset, dec->io->seek_data)) + return MP3D_E_IOERROR; + size_t readed = dec->io->read((uint8_t *)hdr, HDR_SIZE, dec->io->read_data); + if (readed != HDR_SIZE) + return MP3D_E_IOERROR; + } else + hdr = dec->file.buffer + dec->index.frames[i].offset; + dec->to_skip += hdr_frame_samples(hdr)*dec->info.channels; + i++; + } +do_exit: + if (dec->io) + { + if (dec->io->seek(dec->offset, dec->io->seek_data)) + return MP3D_E_IOERROR; + } + dec->buffer_samples = 0; + dec->buffer_consumed = 0; + dec->input_consumed = 0; + dec->input_filled = 0; + dec->last_error = 0; + mp3dec_init(&dec->mp3d); + return 0; +} + +size_t mp3dec_ex_read_frame(mp3dec_ex_t *dec, mp3d_sample_t **buf, mp3dec_frame_info_t *frame_info, size_t max_samples) +{ + if (!dec || !buf || !frame_info) + { + if (dec) + dec->last_error = MP3D_E_PARAM; + return 0; + } + if (dec->detected_samples && dec->cur_sample >= dec->detected_samples) + return 0; /* at end of stream */ + if (dec->last_error) + return 0; /* error eof state, seek can reset it */ + *buf = NULL; + uint64_t end_offset = dec->end_offset ? dec->end_offset : dec->file.size; + int eof = 0; + while (dec->buffer_consumed == dec->buffer_samples) + { + const uint8_t *dec_buf; + if (dec->io) + { + if (!eof && (dec->input_filled - dec->input_consumed) < MINIMP3_BUF_SIZE) + { /* keep minimum 10 consecutive mp3 frames (~16KB) worst case */ + memmove((uint8_t*)dec->file.buffer, (uint8_t*)dec->file.buffer + dec->input_consumed, dec->input_filled - dec->input_consumed); + dec->input_filled -= dec->input_consumed; + dec->input_consumed = 0; + size_t readed = dec->io->read((uint8_t*)dec->file.buffer + dec->input_filled, dec->file.size - dec->input_filled, dec->io->read_data); + if (readed > (dec->file.size - dec->input_filled)) + { + dec->last_error = MP3D_E_IOERROR; + readed = 0; + } + if (readed != (dec->file.size - dec->input_filled)) + eof = 1; + dec->input_filled += readed; + if (eof) + mp3dec_skip_id3v1((uint8_t*)dec->file.buffer, &dec->input_filled); + } + dec_buf = dec->file.buffer + dec->input_consumed; + if (!(dec->input_filled - dec->input_consumed)) + return 0; + dec->buffer_samples = mp3dec_decode_frame(&dec->mp3d, dec_buf, dec->input_filled - dec->input_consumed, dec->buffer, frame_info); + dec->input_consumed += frame_info->frame_bytes; + } else + { + dec_buf = dec->file.buffer + dec->offset; + uint64_t buf_size = end_offset - dec->offset; + if (!buf_size) + return 0; + dec->buffer_samples = mp3dec_decode_frame(&dec->mp3d, dec_buf, MINIMP3_MIN(buf_size, (uint64_t)INT_MAX), dec->buffer, frame_info); + } + dec->buffer_consumed = 0; + if (dec->info.hz != frame_info->hz || dec->info.layer != frame_info->layer) + { +return_e_decode: + dec->last_error = MP3D_E_DECODE; + return 0; + } + if (dec->buffer_samples) + { + dec->buffer_samples *= frame_info->channels; + if (dec->to_skip) + { + size_t skip = MINIMP3_MIN(dec->buffer_samples, dec->to_skip); + dec->buffer_consumed += skip; + dec->to_skip -= skip; + } + if ( +#ifdef MINIMP3_ALLOW_MONO_STEREO_TRANSITION + !(dec->flags & MP3D_ALLOW_MONO_STEREO_TRANSITION) && +#endif + dec->buffer_consumed != dec->buffer_samples && dec->info.channels != frame_info->channels) + { + goto return_e_decode; + } + } else if (dec->to_skip) + { /* In mp3 decoding not always can start decode from any frame because of bit reservoir, + count skip samples for such frames */ + int frame_samples = hdr_frame_samples(dec_buf)*frame_info->channels; + dec->to_skip -= MINIMP3_MIN(frame_samples, dec->to_skip); + } + dec->offset += frame_info->frame_bytes; + } + size_t out_samples = MINIMP3_MIN((size_t)(dec->buffer_samples - dec->buffer_consumed), max_samples); + if (dec->detected_samples) + { /* count decoded samples to properly cut padding */ + if (dec->cur_sample + out_samples >= dec->detected_samples) + out_samples = dec->detected_samples - dec->cur_sample; + } + dec->cur_sample += out_samples; + *buf = dec->buffer + dec->buffer_consumed; + dec->buffer_consumed += out_samples; + return out_samples; +} + +size_t mp3dec_ex_read(mp3dec_ex_t *dec, mp3d_sample_t *buf, size_t samples) +{ + if (!dec || !buf) + { + if (dec) + dec->last_error = MP3D_E_PARAM; + return 0; + } + mp3dec_frame_info_t frame_info; + memset(&frame_info, 0, sizeof(frame_info)); + size_t samples_requested = samples; + while (samples) + { + mp3d_sample_t *buf_frame = NULL; + size_t read_samples = mp3dec_ex_read_frame(dec, &buf_frame, &frame_info, samples); + if (!read_samples) + { + break; + } + memcpy(buf, buf_frame, read_samples * sizeof(mp3d_sample_t)); + buf += read_samples; + samples -= read_samples; + } + return samples_requested - samples; +} + +int mp3dec_ex_open_cb(mp3dec_ex_t *dec, mp3dec_io_t *io, int flags) +{ + if (!dec || !io || (flags & (~MP3D_FLAGS_MASK))) + return MP3D_E_PARAM; + memset(dec, 0, sizeof(*dec)); +#ifdef MINIMP3_HAVE_RING + int ret; + if (ret = mp3dec_open_ring(&dec->file, MINIMP3_IO_SIZE)) + return ret; +#else + dec->file.size = MINIMP3_IO_SIZE; + dec->file.buffer = (const uint8_t*)malloc(dec->file.size); + if (!dec->file.buffer) + return MP3D_E_MEMORY; +#endif + dec->flags = flags; + dec->io = io; + mp3dec_init(&dec->mp3d); + if (io->seek(0, io->seek_data)) + return MP3D_E_IOERROR; + int ret = mp3dec_iterate_cb(io, (uint8_t *)dec->file.buffer, dec->file.size, mp3dec_load_index, dec); + if (ret && MP3D_E_USER != ret) + return ret; + if (dec->io->seek(dec->start_offset, dec->io->seek_data)) + return MP3D_E_IOERROR; + mp3dec_init(&dec->mp3d); + dec->buffer_samples = 0; + dec->indexes_built = !(dec->vbr_tag_found || (flags & MP3D_DO_NOT_SCAN)); + dec->flags &= (~MP3D_DO_NOT_SCAN); + return 0; +} + + +#ifndef MINIMP3_NO_STDIO + +#if defined(__linux__) || defined(__FreeBSD__) +#include +#include +#include +#include +#include +#include +#if !defined(_GNU_SOURCE) +#include +#include +#endif +#if !defined(MAP_POPULATE) && defined(__linux__) +#define MAP_POPULATE 0x08000 +#elif !defined(MAP_POPULATE) +#define MAP_POPULATE 0 +#endif + +static void mp3dec_close_file(mp3dec_map_info_t *map_info) +{ + if (map_info->buffer && MAP_FAILED != map_info->buffer) + munmap((void *)map_info->buffer, map_info->size); + map_info->buffer = 0; + map_info->size = 0; +} + +static int mp3dec_open_file(const char *file_name, mp3dec_map_info_t *map_info) +{ + if (!file_name) + return MP3D_E_PARAM; + int file; + struct stat st; + memset(map_info, 0, sizeof(*map_info)); +retry_open: + file = open(file_name, O_RDONLY); + if (file < 0 && (errno == EAGAIN || errno == EINTR)) + goto retry_open; + if (file < 0 || fstat(file, &st) < 0) + { + close(file); + return MP3D_E_IOERROR; + } + + map_info->size = st.st_size; +retry_mmap: + map_info->buffer = (const uint8_t *)mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE | MAP_POPULATE, file, 0); + if (MAP_FAILED == map_info->buffer && (errno == EAGAIN || errno == EINTR)) + goto retry_mmap; + close(file); + if (MAP_FAILED == map_info->buffer) + return MP3D_E_IOERROR; + return 0; +} + +#if MINIMP3_ENABLE_RING && defined(__linux__) && defined(_GNU_SOURCE) +#define MINIMP3_HAVE_RING +static void mp3dec_close_ring(mp3dec_map_info_t *map_info) +{ +#if defined(__linux__) && defined(_GNU_SOURCE) + if (map_info->buffer && MAP_FAILED != map_info->buffer) + munmap((void *)map_info->buffer, map_info->size*2); +#else + if (map_info->buffer) + { + shmdt(map_info->buffer); + shmdt(map_info->buffer + map_info->size); + } +#endif + map_info->buffer = 0; + map_info->size = 0; +} + +static int mp3dec_open_ring(mp3dec_map_info_t *map_info, size_t size) +{ + int memfd, page_size; +#if defined(__linux__) && defined(_GNU_SOURCE) + void *buffer; + int res; +#endif + memset(map_info, 0, sizeof(*map_info)); + +#ifdef _SC_PAGESIZE + page_size = sysconf(_SC_PAGESIZE); +#else + page_size = getpagesize(); +#endif + map_info->size = (size + page_size - 1)/page_size*page_size; + +#if defined(__linux__) && defined(_GNU_SOURCE) + memfd = memfd_create("mp3_ring", 0); + if (memfd < 0) + return MP3D_E_MEMORY; + +retry_ftruncate: + res = ftruncate(memfd, map_info->size); + if (res && (errno == EAGAIN || errno == EINTR)) + goto retry_ftruncate; + if (res) + goto error; + +retry_mmap: + map_info->buffer = (const uint8_t *)mmap(NULL, map_info->size*2, PROT_NONE, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0); + if (MAP_FAILED == map_info->buffer && (errno == EAGAIN || errno == EINTR)) + goto retry_mmap; + if (MAP_FAILED == map_info->buffer || !map_info->buffer) + goto error; +retry_mmap2: + buffer = mmap((void *)map_info->buffer, map_info->size, PROT_READ | PROT_WRITE, MAP_FIXED | MAP_SHARED, memfd, 0); + if (MAP_FAILED == map_info->buffer && (errno == EAGAIN || errno == EINTR)) + goto retry_mmap2; + if (MAP_FAILED == map_info->buffer || buffer != (void *)map_info->buffer) + goto error; +retry_mmap3: + buffer = mmap((void *)map_info->buffer + map_info->size, map_info->size, PROT_READ | PROT_WRITE, MAP_FIXED | MAP_SHARED, memfd, 0); + if (MAP_FAILED == map_info->buffer && (errno == EAGAIN || errno == EINTR)) + goto retry_mmap3; + if (MAP_FAILED == map_info->buffer || buffer != (void *)(map_info->buffer + map_info->size)) + goto error; + + close(memfd); + return 0; +error: + close(memfd); + mp3dec_close_ring(map_info); + return MP3D_E_MEMORY; +#else + memfd = shmget(IPC_PRIVATE, map_info->size, IPC_CREAT | 0700); + if (memfd < 0) + return MP3D_E_MEMORY; +retry_mmap: + map_info->buffer = (const uint8_t *)mmap(NULL, map_info->size*2, PROT_NONE, MAP_PRIVATE, -1, 0); + if (MAP_FAILED == map_info->buffer && (errno == EAGAIN || errno == EINTR)) + goto retry_mmap; + if (MAP_FAILED == map_info->buffer) + goto error; + if (map_info->buffer != shmat(memfd, map_info->buffer, 0)) + goto error; + if ((map_info->buffer + map_info->size) != shmat(memfd, map_info->buffer + map_info->size, 0)) + goto error; + if (shmctl(memfd, IPC_RMID, NULL) < 0) + return MP3D_E_MEMORY; + return 0; +error: + shmctl(memfd, IPC_RMID, NULL); + mp3dec_close_ring(map_info); + return MP3D_E_MEMORY; +#endif +} +#endif /*MINIMP3_ENABLE_RING*/ +#elif defined(_WIN32) +#include + +static void mp3dec_close_file(mp3dec_map_info_t *map_info) +{ + if (map_info->buffer) + UnmapViewOfFile(map_info->buffer); + map_info->buffer = 0; + map_info->size = 0; +} + +static int mp3dec_open_file_h(HANDLE file, mp3dec_map_info_t *map_info) +{ + memset(map_info, 0, sizeof(*map_info)); + + HANDLE mapping = NULL; + LARGE_INTEGER s; + s.LowPart = GetFileSize(file, (DWORD*)&s.HighPart); + if (s.LowPart == INVALID_FILE_SIZE && GetLastError() != NO_ERROR) + goto error; + map_info->size = s.QuadPart; + + mapping = CreateFileMapping(file, NULL, PAGE_READONLY, 0, 0, NULL); + if (!mapping) + goto error; + map_info->buffer = (const uint8_t*)MapViewOfFile(mapping, FILE_MAP_READ, 0, 0, s.QuadPart); + CloseHandle(mapping); + if (!map_info->buffer) + goto error; + + CloseHandle(file); + return 0; +error: + mp3dec_close_file(map_info); + CloseHandle(file); + return MP3D_E_IOERROR; +} + +static int mp3dec_open_file(const char *file_name, mp3dec_map_info_t *map_info) +{ + if (!file_name) + return MP3D_E_PARAM; + HANDLE file = CreateFileA(file_name, GENERIC_READ, FILE_SHARE_READ, 0, OPEN_EXISTING, 0, 0); + if (INVALID_HANDLE_VALUE == file) + return MP3D_E_IOERROR; + return mp3dec_open_file_h(file, map_info); +} + +static int mp3dec_open_file_w(const wchar_t *file_name, mp3dec_map_info_t *map_info) +{ + if (!file_name) + return MP3D_E_PARAM; + HANDLE file = CreateFileW(file_name, GENERIC_READ, FILE_SHARE_READ, 0, OPEN_EXISTING, 0, 0); + if (INVALID_HANDLE_VALUE == file) + return MP3D_E_IOERROR; + return mp3dec_open_file_h(file, map_info); +} +#else +#include + +static void mp3dec_close_file(mp3dec_map_info_t *map_info) +{ + if (map_info->buffer) + free((void *)map_info->buffer); + map_info->buffer = 0; + map_info->size = 0; +} + +static int mp3dec_open_file(const char *file_name, mp3dec_map_info_t *map_info) +{ + if (!file_name) + return MP3D_E_PARAM; + memset(map_info, 0, sizeof(*map_info)); + FILE *file = fopen(file_name, "rb"); + if (!file) + return MP3D_E_IOERROR; + int res = MP3D_E_IOERROR; + long size = -1; + if (fseek(file, 0, SEEK_END)) + goto error; + size = ftell(file); + if (size < 0) + goto error; + map_info->size = (size_t)size; + if (fseek(file, 0, SEEK_SET)) + goto error; + map_info->buffer = (uint8_t *)malloc(map_info->size); + if (!map_info->buffer) + { + res = MP3D_E_MEMORY; + goto error; + } + if (fread((void *)map_info->buffer, 1, map_info->size, file) != map_info->size) + goto error; + fclose(file); + return 0; +error: + mp3dec_close_file(map_info); + fclose(file); + return res; +} +#endif + +static int mp3dec_detect_mapinfo(mp3dec_map_info_t *map_info) +{ + int ret = mp3dec_detect_buf(map_info->buffer, map_info->size); + mp3dec_close_file(map_info); + return ret; +} + +static int mp3dec_load_mapinfo(mp3dec_t *dec, mp3dec_map_info_t *map_info, mp3dec_file_info_t *info, MP3D_PROGRESS_CB progress_cb, void *user_data) +{ + int ret = mp3dec_load_buf(dec, map_info->buffer, map_info->size, info, progress_cb, user_data); + mp3dec_close_file(map_info); + return ret; +} + +static int mp3dec_iterate_mapinfo(mp3dec_map_info_t *map_info, MP3D_ITERATE_CB callback, void *user_data) +{ + int ret = mp3dec_iterate_buf(map_info->buffer, map_info->size, callback, user_data); + mp3dec_close_file(map_info); + return ret; +} + +static int mp3dec_ex_open_mapinfo(mp3dec_ex_t *dec, int flags) +{ + int ret = mp3dec_ex_open_buf(dec, dec->file.buffer, dec->file.size, flags); + dec->is_file = 1; + if (ret) + mp3dec_ex_close(dec); + return ret; +} + +int mp3dec_detect(const char *file_name) +{ + int ret; + mp3dec_map_info_t map_info; + if ((ret = mp3dec_open_file(file_name, &map_info))) + return ret; + return mp3dec_detect_mapinfo(&map_info); +} + +int mp3dec_load(mp3dec_t *dec, const char *file_name, mp3dec_file_info_t *info, MP3D_PROGRESS_CB progress_cb, void *user_data) +{ + int ret; + mp3dec_map_info_t map_info; + if ((ret = mp3dec_open_file(file_name, &map_info))) + return ret; + return mp3dec_load_mapinfo(dec, &map_info, info, progress_cb, user_data); +} + +int mp3dec_iterate(const char *file_name, MP3D_ITERATE_CB callback, void *user_data) +{ + int ret; + mp3dec_map_info_t map_info; + if ((ret = mp3dec_open_file(file_name, &map_info))) + return ret; + return mp3dec_iterate_mapinfo(&map_info, callback, user_data); +} + +int mp3dec_ex_open(mp3dec_ex_t *dec, const char *file_name, int flags) +{ + int ret; + if (!dec) + return MP3D_E_PARAM; + if ((ret = mp3dec_open_file(file_name, &dec->file))) + return ret; + return mp3dec_ex_open_mapinfo(dec, flags); +} + +void mp3dec_ex_close(mp3dec_ex_t *dec) +{ +#ifdef MINIMP3_HAVE_RING + if (dec->io) + mp3dec_close_ring(&dec->file); +#else + if (dec->io && dec->file.buffer) + free((void*)dec->file.buffer); +#endif + if (dec->is_file) + mp3dec_close_file(&dec->file); + if (dec->index.frames) + free(dec->index.frames); + memset(dec, 0, sizeof(*dec)); +} + +#ifdef _WIN32 +int mp3dec_detect_w(const wchar_t *file_name) +{ + int ret; + mp3dec_map_info_t map_info; + if ((ret = mp3dec_open_file_w(file_name, &map_info))) + return ret; + return mp3dec_detect_mapinfo(&map_info); +} + +int mp3dec_load_w(mp3dec_t *dec, const wchar_t *file_name, mp3dec_file_info_t *info, MP3D_PROGRESS_CB progress_cb, void *user_data) +{ + int ret; + mp3dec_map_info_t map_info; + if ((ret = mp3dec_open_file_w(file_name, &map_info))) + return ret; + return mp3dec_load_mapinfo(dec, &map_info, info, progress_cb, user_data); +} + +int mp3dec_iterate_w(const wchar_t *file_name, MP3D_ITERATE_CB callback, void *user_data) +{ + int ret; + mp3dec_map_info_t map_info; + if ((ret = mp3dec_open_file_w(file_name, &map_info))) + return ret; + return mp3dec_iterate_mapinfo(&map_info, callback, user_data); +} + +int mp3dec_ex_open_w(mp3dec_ex_t *dec, const wchar_t *file_name, int flags) +{ + int ret; + if ((ret = mp3dec_open_file_w(file_name, &dec->file))) + return ret; + return mp3dec_ex_open_mapinfo(dec, flags); +} +#endif +#else /* MINIMP3_NO_STDIO */ +void mp3dec_ex_close(mp3dec_ex_t *dec) +{ +#ifdef MINIMP3_HAVE_RING + if (dec->io) + mp3dec_close_ring(&dec->file); +#else + if (dec->io && dec->file.buffer) + free((void*)dec->file.buffer); +#endif + if (dec->index.frames) + free(dec->index.frames); + memset(dec, 0, sizeof(*dec)); +} +#endif + +#endif /* MINIMP3_IMPLEMENTATION && !_MINIMP3_EX_IMPLEMENTATION_GUARD */ diff --git a/src/main.c b/src/main.c index 7f50559d..c34515fb 100644 --- a/src/main.c +++ b/src/main.c @@ -9,45 +9,38 @@ #include "menu/settings.h" -static void init (void) { +static void hw_init (void) { assertf(usb_initialize() != CART_NONE, "No flashcart was detected"); - //debug_init_usblog(); - assertf(debug_init_sdfs("sd:/", -1), "Couldn't initialize SD card"); + flashcart_error_t error = flashcart_init(); assertf(error != FLASHCART_ERROR_OUTDATED, "Outdated flashcart firmware"); assertf(error != FLASHCART_ERROR_UNSUPPORTED, "Unsupported flashcart"); assertf(error == FLASHCART_OK, "Unknown error while initializing flashcart"); - controller_init(); - display_init(RESOLUTION_640x240, DEPTH_16_BPP, 2, GAMMA_NONE, ANTIALIAS_RESAMPLE); - graphics_set_color(0xFFFFFFFF, 0x00000000); - graphics_set_default_font(); + assertf(debug_init_sdfs("sd:/", -1), "Couldn't initialize SD card"); + +#ifdef DEBUG + debug_init_usblog(); +#endif } -static void deinit (void) { +static void hw_deinit (void) { flashcart_deinit(); - rdpq_close(); - rspq_close(); - audio_close(); - display_close(); disable_interrupts(); } int main (void) { - init(); - settings_t settings; - settings_load_default_state(&settings); - //settings_load_from_file(&settings); // FIXME: this needs a rethink. - // if (boot_is_warm()) { - // menu_restore(&settings); - // } + hw_init(); + + settings_load_default_state(&settings); + settings_load_from_file(&settings); menu_run(&settings); - deinit(); + hw_deinit(); boot(&settings.boot_params); diff --git a/src/menu/actions.c b/src/menu/actions.c index 2e509cd0..33a638e5 100644 --- a/src/menu/actions.c +++ b/src/menu/actions.c @@ -3,50 +3,59 @@ #include "actions.h" -#define ACTIONS_REPEAT_DELAY 20 +#define ACTIONS_REPEAT_DELAY 16 #define ACTIONS_REPEAT_RATE 2 -void actions_update (menu_t *menu) { +static void actions_clear (menu_t *menu) { menu->actions.go_up = false; menu->actions.go_down = false; + menu->actions.go_left = false; + menu->actions.go_right = false; menu->actions.fast = false; menu->actions.enter = false; menu->actions.back = false; menu->actions.info = false; menu->actions.settings = false; + menu->actions.override = false; +} + +void actions_update (menu_t *menu) { controller_scan(); + struct controller_data down = get_keys_down(); struct controller_data held = get_keys_held(); - if (down.c[0].err != 0) { + if (down.c[0].err != ERROR_NONE) { return; } + actions_clear(menu); + if (down.c[0].up || down.c[0].C_up) { menu->actions.go_up = true; - menu->actions.held_counter = 0; + menu->actions.vertical_held_counter = 0; if (down.c[0].C_up) { menu->actions.fast = true; } } else if (down.c[0].down || down.c[0].C_down) { menu->actions.go_down = true; - menu->actions.held_counter = 0; + menu->actions.vertical_held_counter = 0; if (down.c[0].C_down) { menu->actions.fast = true; } } else if (held.c[0].up || held.c[0].C_up) { - menu->actions.held_counter += 1; - if ((menu->actions.held_counter >= ACTIONS_REPEAT_DELAY) && (menu->actions.held_counter % ACTIONS_REPEAT_RATE)) { + menu->actions.vertical_held_counter += 1; + if ((menu->actions.vertical_held_counter >= ACTIONS_REPEAT_DELAY) && (menu->actions.vertical_held_counter % ACTIONS_REPEAT_RATE)) { menu->actions.go_up = true; if (held.c[0].C_up) { menu->actions.fast = true; } } } else if (held.c[0].down || held.c[0].C_down) { - menu->actions.held_counter += 1; - if ((menu->actions.held_counter >= ACTIONS_REPEAT_DELAY) && (menu->actions.held_counter % ACTIONS_REPEAT_RATE)) { + menu->actions.vertical_held_counter += 1; + if ((menu->actions.vertical_held_counter >= ACTIONS_REPEAT_DELAY) && (menu->actions.vertical_held_counter % ACTIONS_REPEAT_RATE)) { menu->actions.go_down = true; if (held.c[0].C_down) { menu->actions.fast = true; @@ -54,13 +63,35 @@ void actions_update (menu_t *menu) { } } + if (down.c[0].left) { + menu->actions.go_left = true; + menu->actions.horizontal_held_counter = 0; + } else if (down.c[0].right) { + menu->actions.go_right = true; + menu->actions.horizontal_held_counter = 0; + } else if (held.c[0].left) { + menu->actions.horizontal_held_counter += 1; + if ((menu->actions.horizontal_held_counter >= ACTIONS_REPEAT_DELAY) && (menu->actions.horizontal_held_counter % ACTIONS_REPEAT_RATE)) { + menu->actions.go_left = true; + } + } else if (held.c[0].right) { + menu->actions.horizontal_held_counter += 1; + if ((menu->actions.horizontal_held_counter >= ACTIONS_REPEAT_DELAY) && (menu->actions.horizontal_held_counter % ACTIONS_REPEAT_RATE)) { + menu->actions.go_right = true; + } + } + if (down.c[0].A) { menu->actions.enter = true; } else if (down.c[0].B) { menu->actions.back = true; } else if (down.c[0].Z) { menu->actions.info = true; - } else if (down.c[0].start) { + } else if (down.c[0].R) { menu->actions.settings = true; } + + if (down.c[0].B || held.c[0].B) { + menu->actions.override = true; + } } diff --git a/src/menu/actions.h b/src/menu/actions.h index 3f96be46..1695e93f 100644 --- a/src/menu/actions.h +++ b/src/menu/actions.h @@ -2,7 +2,7 @@ #define ACTIONS_H__ -#include "menu.h" +#include "menu_state.h" void actions_update (menu_t *menu); diff --git a/src/menu/assets.c b/src/menu/assets.c new file mode 100644 index 00000000..44511a95 --- /dev/null +++ b/src/menu/assets.c @@ -0,0 +1,37 @@ +#include +#include + + +typedef struct { + char *name; + uint8_t *data; + int size; +} asset_t; + + +#define ASSET_IMPORT(a) \ + extern uint8_t *_binary_assets_##a##_start __attribute__((section(".data"))); \ + extern int _binary_assets_##a##_size __attribute__((section(".data"))); +#define ASSET(n, a) { n, (uint8_t *) (&_binary_assets_##a##_start), (int) (&_binary_assets_##a##_size) } + + +ASSET_IMPORT(FiraMono_Bold_font64); + +static asset_t assets[] = { + ASSET("assets:/font", FiraMono_Bold_font64), +}; + + +extern void *__real_asset_load (char *fn, int *sz); + +void *__wrap_asset_load (char *fn, int *sz) { + for (int i = 0; i < sizeof(assets) / sizeof(assets[0]); i++) { + asset_t *asset = &assets[i]; + if (strcmp(asset->name, fn) == 0) { + *sz = asset->size; + return asset->data; + } + } + + return __real_asset_load(fn, sz); +} diff --git a/src/menu/menu.c b/src/menu/menu.c index 82d77439..4237a914 100644 --- a/src/menu/menu.c +++ b/src/menu/menu.c @@ -3,42 +3,73 @@ #include #include "actions.h" +#include "menu_state.h" #include "menu.h" -#include "views.h" +#include "settings.h" +#include "utils/fs.h" +#include "views/views.h" -static menu_t *menu_init (settings_t *settings) { - menu_t *menu = calloc(1, sizeof(menu_t)); +static menu_t *menu; +static bool boot_pending; - menu->mode = MENU_MODE_INIT; - menu->next_mode = MENU_MODE_BROWSER; + +static void menu_init (settings_t *settings) { + controller_init(); + audio_init(44100, 2); + mixer_init(2); + display_init(RESOLUTION_640x480, DEPTH_16_BPP, 2, GAMMA_NONE, ANTIALIAS_OFF); + rspq_init(); + rdpq_init(); + + boot_pending = false; + + menu = calloc(1, sizeof(menu_t)); + assert(menu != NULL); + + menu->mode = MENU_MODE_NONE; + menu->next_mode = MENU_MODE_STARTUP; + + menu->assets.font = rdpq_font_load("assets:/font"); + menu->assets.font_height = 16; menu->browser.valid = false; - menu->browser.directory = path_init(NULL); // TODO: load starting directory from settings - - return menu; + if (file_exists(settings->last_state.directory)) { + menu->browser.directory = path_init(settings->last_state.directory); + } else { + menu->browser.directory = path_init(NULL); + } + menu->browser.show_hidden = false; } static void menu_deinit (menu_t *menu) { path_free(menu->browser.directory); + // NOTE: font is not loaded dynamically due to hack in assets.c, so there's no need to free it + // rdpq_font_free(menu->assets.font); free(menu); + + rdpq_close(); + rspq_close(); + display_close(); + mixer_close(); + audio_close(); } void menu_run (settings_t *settings) { - menu_t *menu = menu_init(settings); + menu_init(settings); - bool running = true; + int audio_buffer_length = audio_get_buffer_length(); - while (running) { + while (!boot_pending && (exception_reset_time() == 0)) { surface_t *display = display_try_get(); if (display != NULL) { actions_update(menu); switch (menu->mode) { - case MENU_MODE_INIT: - view_init_display(menu, display); + case MENU_MODE_STARTUP: + view_startup_display(menu, display); break; case MENU_MODE_BROWSER: @@ -49,6 +80,10 @@ void menu_run (settings_t *settings) { view_file_info_display(menu, display); break; + case MENU_MODE_PLAYER: + view_player_display(menu, display); + break; + case MENU_MODE_CREDITS: view_credits_display(menu, display); break; @@ -62,6 +97,8 @@ void menu_run (settings_t *settings) { break; default: + rdpq_attach_clear(display, NULL); + rdpq_detach_show(); break; } @@ -69,6 +106,10 @@ void menu_run (settings_t *settings) { menu->mode = menu->next_mode; switch (menu->next_mode) { + case MENU_MODE_STARTUP: + view_startup_init(menu); + break; + case MENU_MODE_BROWSER: view_browser_init(menu); break; @@ -77,6 +118,10 @@ void menu_run (settings_t *settings) { view_file_info_init(menu); break; + case MENU_MODE_PLAYER: + view_player_init(menu); + break; + case MENU_MODE_CREDITS: view_credits_init(menu); break; @@ -90,7 +135,7 @@ void menu_run (settings_t *settings) { break; case MENU_MODE_BOOT: - running = false; + boot_pending = true; break; default: @@ -98,7 +143,17 @@ void menu_run (settings_t *settings) { } } } + + if (audio_can_write()) { + short *audio_buffer = audio_write_begin(); + mixer_poll(audio_buffer, audio_buffer_length); + audio_write_end(); + } } menu_deinit(menu); + + while (exception_reset_time() > 0) { + // Do nothing if reset button was pressed + } } diff --git a/src/menu/menu.h b/src/menu/menu.h index 2b33727a..06f6cba9 100644 --- a/src/menu/menu.h +++ b/src/menu/menu.h @@ -2,60 +2,9 @@ #define MENU_H__ -#include "path.h" #include "settings.h" -#define BROWSER_LIST_SIZE 4096 - - -typedef enum { - MENU_MODE_INIT, - MENU_MODE_BROWSER, - MENU_MODE_FILE_INFO, - MENU_MODE_CREDITS, - MENU_MODE_LOAD, - MENU_MODE_ERROR, - MENU_MODE_BOOT, -} menu_mode_t; - -typedef enum { - ENTRY_TYPE_DIR, - ENTRY_TYPE_ROM, - ENTRY_TYPE_SAVE, - ENTRY_TYPE_UNKNOWN, -} entry_type_t; - -typedef struct { - char *name; - entry_type_t type; -} entry_t; - -typedef struct { - menu_mode_t mode; - menu_mode_t next_mode; - - struct { - bool go_up; - bool go_down; - bool fast; - bool enter; - bool back; - bool info; - bool settings; - int held_counter; - } actions; - - struct { - bool valid; - path_t *directory; - entry_t list[BROWSER_LIST_SIZE]; - int entries; - int selected; - } browser; -} menu_t; - - void menu_run (settings_t *settings); diff --git a/src/menu/menu_state.h b/src/menu/menu_state.h new file mode 100644 index 00000000..6092ce0c --- /dev/null +++ b/src/menu/menu_state.h @@ -0,0 +1,79 @@ +#ifndef MENU_STRUCT_H__ +#define MENU_STRUCT_H__ + + +#include + +#include "path.h" + + +#define BROWSER_LIST_SIZE 10000 + + +typedef enum { + MENU_MODE_NONE, + MENU_MODE_STARTUP, + MENU_MODE_BROWSER, + MENU_MODE_FILE_INFO, + MENU_MODE_PLAYER, + MENU_MODE_CREDITS, + MENU_MODE_LOAD, + MENU_MODE_ERROR, + MENU_MODE_BOOT, +} menu_mode_t; + +typedef enum { + ENTRY_TYPE_DIR, + ENTRY_TYPE_ROM, + ENTRY_TYPE_SAVE, + ENTRY_TYPE_MUSIC, + ENTRY_TYPE_OTHER, +} entry_type_t; + +typedef struct { + char *name; + entry_type_t type; + int size; +} entry_t; + +typedef struct { + menu_mode_t mode; + menu_mode_t next_mode; + + struct { + rdpq_font_t *font; + int font_height; + } assets; + + struct { + bool go_up; + bool go_down; + bool go_left; + bool go_right; + bool fast; + int vertical_held_counter; + int horizontal_held_counter; + + bool enter; + bool back; + bool info; + bool settings; + bool override; + } actions; + + struct { + bool valid; + path_t *directory; + entry_t list[BROWSER_LIST_SIZE]; + int entries; + int selected; + bool show_hidden; + } browser; + + struct { + path_t *path; + } player; +} menu_t; + + +#endif diff --git a/src/menu/mp3player.c b/src/menu/mp3player.c new file mode 100644 index 00000000..07816ba2 --- /dev/null +++ b/src/menu/mp3player.c @@ -0,0 +1,295 @@ +#include +#include + +#include "mp3player.h" +#include "utils/utils.h" + +#define MINIMP3_IMPLEMENTATION +#include "libs/minimp3/minimp3.h" +#include "libs/minimp3/minimp3_ex.h" + + +#define BUFFER_SIZE (16 * 1024) +#define MIXER_CHANNEL (0) + + +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]; + uint8_t *buffer_ptr; + size_t buffer_left; + + short samples[MINIMP3_MAX_SAMPLES_PER_FRAME]; + short *samples_ptr; + int samples_left; + + waveform_t wave; +} mp3player_t; + +static mp3player_t *p = NULL; + + +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) { + UINT bytes_read; + + if (f_eof(&p->fil)) { + return; + } + + if (p->buffer_left >= (MAX_FREE_FORMAT_FRAME_SIZE * 3)) { + return; + } + + if ((p->buffer_ptr != p->buffer) && (p->buffer_left > 0)) { + memmove(p->buffer, p->buffer_ptr, p->buffer_left); + p->buffer_ptr = p->buffer; + } + + if (f_read(&p->fil, p->buffer + p->buffer_left, BUFFER_SIZE - 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) { + mp3player_fill_buffer(); + + int samples = mp3dec_decode_frame(&p->dec, p->buffer_ptr, p->buffer_left, p->samples, &p->info); + + 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; + } + } +} + +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); +} + + +mp3player_err_t mp3player_init (void) { + p = calloc(1, sizeof(mp3player_t)); + + if (p == NULL) { + return MP3PLAYER_ERR_MALLOC; + } + + mp3player_reset_decoder(); + + p->loaded = false; + p->io_error = false; + + p->wave = (waveform_t) { + .name = "mp3player", + .bits = 16, + .channels = 2, + .frequency = 44100, + .len = WAVEFORM_MAX_LEN - 1, + .loop_len = WAVEFORM_MAX_LEN - 1, + .read = mp3player_wave_read, + .ctx = p, + }; + + return MP3PLAYER_OK; +} + +void mp3player_deinit (void) { + mp3player_unload(); + free(p); + p = NULL; +} + +mp3player_err_t mp3player_load (char *path) { + if (p->loaded) { + mp3player_unload(); + } + + if (f_open(&p->fil, path, FA_READ) != FR_OK) { + return MP3PLAYER_ERR_IO; + } + + mp3player_reset_decoder(); + + while (!(f_eof(&p->fil) && p->buffer_left == 0)) { + mp3player_fill_buffer(); + + if (p->io_error) { + return MP3PLAYER_ERR_IO; + } + + 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); + mp3player_reset_decoder(); + continue; + } + + if (mp3dec_decode_frame(&p->dec, p->buffer_ptr, p->buffer_left, NULL, &p->info) > 0) { + mp3dec_init(&p->dec); + + p->loaded = true; + p->data_start = f_tell(&p->fil) - p->buffer_left + p->info.frame_offset; + + p->wave.channels = p->info.channels; + p->wave.frequency = p->info.hz; + + return MP3PLAYER_OK; + } + + p->buffer_ptr += p->info.frame_bytes; + p->buffer_left -= p->info.frame_bytes; + } + + if (f_close(&p->fil) != FR_OK) { + return MP3PLAYER_ERR_IO; + } + + return MP3PLAYER_ERR_INVALID_FILE; +} + +void mp3player_unload (void) { + mp3player_stop(); + if (p->loaded) { + p->loaded = false; + f_close(&p->fil); + } +} + +mp3player_err_t mp3player_process (void) { + if (p->io_error) { + mp3player_unload(); + return MP3PLAYER_ERR_IO; + } + + if (mp3player_is_finished()) { + mp3player_stop(); + } + + return MP3PLAYER_OK; +} + +bool mp3player_is_playing (void) { + return mixer_ch_playing(MIXER_CHANNEL); +} + +bool mp3player_is_finished (void) { + return f_eof(&p->fil) && p->buffer_left == 0 && p->samples_left == 0; +} + +mp3player_err_t mp3player_play (void) { + if (!p->loaded) { + return MP3PLAYER_ERR_NO_FILE; + } + if (!mp3player_is_playing()) { + if (mp3player_is_finished()) { + if (f_lseek(&p->fil, p->data_start) != FR_OK) { + p->io_error = true; + return MP3PLAYER_ERR_IO; + } + mp3player_reset_decoder(); + } + mixer_ch_play(MIXER_CHANNEL, &p->wave); + } + return MP3PLAYER_OK; +} + +void mp3player_stop (void) { + if (mp3player_is_playing()) { + mixer_ch_stop(MIXER_CHANNEL); + } +} + +mp3player_err_t mp3player_toggle (void) { + if (mp3player_is_playing()) { + mp3player_stop(); + } else { + return mp3player_play(); + } + return MP3PLAYER_OK; +} + +mp3player_err_t mp3player_seek (int seconds) { + // NOTE: Rough approximation using last frame 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); + if (bytes_to_move == 0) { + return MP3PLAYER_OK; + } + + long position = ((long) (f_tell(&p->fil)) - p->buffer_left + bytes_to_move); + if (position < (long) (p->data_start)) { + position = p->data_start; + } + + if (f_lseek(&p->fil, position) != FR_OK) { + p->io_error = true; + return MP3PLAYER_ERR_IO; + } + + mp3player_reset_decoder(); + + return MP3PLAYER_OK; +} + +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. + + 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; + + return data_position / (float) (data_size); +} diff --git a/src/menu/mp3player.h b/src/menu/mp3player.h new file mode 100644 index 00000000..e5ca938d --- /dev/null +++ b/src/menu/mp3player.h @@ -0,0 +1,31 @@ +#ifndef MP3PLAYER_H__ +#define MP3PLAYER_H__ + + +#include + + +typedef enum { + MP3PLAYER_OK, + MP3PLAYER_ERR_MALLOC, + MP3PLAYER_ERR_IO, + MP3PLAYER_ERR_NO_FILE, + MP3PLAYER_ERR_INVALID_FILE, +} mp3player_err_t; + + +mp3player_err_t mp3player_init (void); +void mp3player_deinit (void); +mp3player_err_t mp3player_load (char *path); +void mp3player_unload (void); +mp3player_err_t mp3player_process (void); +bool mp3player_is_playing (void); +bool mp3player_is_finished (void); +mp3player_err_t mp3player_play (void); +void mp3player_stop (void); +mp3player_err_t mp3player_toggle (void); +mp3player_err_t mp3player_seek (int seconds); +float mp3player_get_progress (void); + + +#endif diff --git a/src/menu/path.c b/src/menu/path.c index 65c3e460..108f04a7 100644 --- a/src/menu/path.c +++ b/src/menu/path.c @@ -1,7 +1,7 @@ -#include - +#include #include #include + #include "path.h" @@ -16,6 +16,7 @@ static void path_resize (path_t *path, size_t min_length) { path->capacity += PATH_CAPACITY_ALIGNMENT - alignment; } path->buffer = realloc(path->buffer, (path->capacity + 1) * sizeof(char)); + assert(path->buffer != NULL); } path_t *path_init (char *string) { @@ -23,6 +24,7 @@ path_t *path_init (char *string) { string = ""; } path_t *path = calloc(1, sizeof(path_t)); + assert(path != NULL); path_resize(path, strlen(string)); memset(path->buffer, 0, path->capacity + 1); strcpy(path->buffer, string); @@ -47,6 +49,10 @@ char *path_last_get (path_t *path) { return (last_slash == NULL) ? path->buffer : (last_slash + 1); } +bool path_is_root (path_t *path) { + return (strcmp(path->buffer, "") == 0) || (strcmp(path->buffer, "/") == 0); +} + void path_append (path_t *path, char *string) { size_t buffer_length = strlen(path->buffer); size_t string_length = strlen(string); diff --git a/src/menu/path.h b/src/menu/path.h index 196bbe19..3f947b19 100644 --- a/src/menu/path.h +++ b/src/menu/path.h @@ -2,6 +2,9 @@ #define PATH_H__ +#include + + typedef struct { char *buffer; size_t capacity; @@ -13,6 +16,7 @@ void path_free (path_t *path); path_t *path_clone (path_t *string); char *path_get (path_t *path); char *path_last_get (path_t *path); +bool path_is_root (path_t *path); void path_append (path_t *path, char *string); void path_concat (path_t *dst, path_t *str); void path_push (path_t *path, char *string); diff --git a/src/menu/settings.c b/src/menu/settings.c index 1666509c..51aa2065 100644 --- a/src/menu/settings.c +++ b/src/menu/settings.c @@ -13,6 +13,7 @@ void settings_load_from_file(settings_t *settings) { + return; FILE *fp = fopen(SC64_SETTINGS_FILEPATH, "r"); if (!fp) { printf("Error loading config file %s\n", SC64_SETTINGS_FILEPATH); @@ -157,7 +158,7 @@ void settings_load_default_state(settings_t *settings) { settings->last_rom.save_type = FLASHCART_SAVE_TYPE_NONE; settings->last_rom.save_writeback = false; - settings->last_state.current_directory = "/"; // This must not include the trailing slash on dirs! + settings->last_state.directory = ""; // This must not include the trailing slash on dirs! settings->last_state.auto_load_last_rom = false; settings->boot_params.device_type = BOOT_DEVICE_TYPE_ROM; diff --git a/src/menu/settings.h b/src/menu/settings.h index b77ea1fe..4195558a 100644 --- a/src/menu/settings.h +++ b/src/menu/settings.h @@ -17,7 +17,7 @@ typedef struct { typedef struct { bool auto_load_last_rom; - char* current_directory; + char* directory; // TODO: // Menu layout: list vs grid // Hide extensions diff --git a/src/menu/views/browser.c b/src/menu/views/browser.c index ae4bd60c..89dc61cb 100644 --- a/src/menu/views/browser.c +++ b/src/menu/views/browser.c @@ -1,12 +1,17 @@ +#include +#include + #include #include -#include -#include "../menu.h" -#include "../menu_res_setup.h" -#include "../../utils/str_utils.h" + +#include "fragments/fragments.h" +#include "utils/fs.h" +#include "views.h" -#define BROWSER_LIST_ROWS 21 +static const char *rom_extensions[] = { "z64", "n64", "v64", NULL }; +static const char *save_extensions[] = { "sav", NULL }; +static const char *music_extensions[] = { "mp3", NULL }; static int compare_entry (const void *pa, const void *pb) { @@ -26,34 +31,37 @@ static int compare_entry (const void *pa, const void *pb) { return -1; } else if (b->type == ENTRY_TYPE_SAVE) { return 1; + } else if (a->type == ENTRY_TYPE_MUSIC) { + return -1; + } else if (b->type == ENTRY_TYPE_MUSIC) { + return 1; } } return strcasecmp((const char *) (a->name), (const char *) (b->name)); } -static void load_directory (menu_t *menu) { +static bool load_directory (menu_t *menu) { DIR dir; FILINFO info; - for (int i = 0; i < menu->browser.entries; i++) { + for (int i = menu->browser.entries - 1; i >= 0; i--) { free(menu->browser.list[i].name); } menu->browser.entries = 0; menu->browser.selected = -1; if (f_opendir(&dir, path_get(menu->browser.directory)) != FR_OK) { - menu->next_mode = MENU_MODE_ERROR; - return; + return true; } - while (true) { + while (menu->browser.entries < BROWSER_LIST_SIZE) { if (f_readdir(&dir, &info) != FR_OK) { - menu->next_mode = MENU_MODE_ERROR; - return; + return true; } size_t length = strlen(info.fname); + if (length == 0) { break; } @@ -61,59 +69,112 @@ static void load_directory (menu_t *menu) { if (info.fattrib & AM_SYS) { continue; } + if (info.fattrib & AM_HID && !menu->browser.show_hidden) { + continue; + } - entry_t *entry = &menu->browser.list[menu->browser.entries]; + entry_t *entry = &menu->browser.list[menu->browser.entries++]; - entry->name = malloc((length + 1) * sizeof(char)); - strcpy(entry->name, info.fname); + entry->name = strdup(info.fname); + assert(entry->name != NULL); if (info.fattrib & AM_DIR) { entry->type = ENTRY_TYPE_DIR; - // TODO: use something like `ext_is_n64_rom(info.fname)` instead of `str_endswith(info.fname, ".xxx")` - } else if (str_endswith(info.fname, ".n64") || str_endswith(info.fname, ".z64") || str_endswith(info.fname, ".v64") || str_endswith(info.fname, ".N64")) { + } else if (file_has_extensions(info.fname, rom_extensions)) { entry->type = ENTRY_TYPE_ROM; - } else if (str_endswith(info.fname, ".sav")) { + } else if (file_has_extensions(info.fname, save_extensions)) { entry->type = ENTRY_TYPE_SAVE; + } else if (file_has_extensions(info.fname, music_extensions)) { + entry->type = ENTRY_TYPE_MUSIC; } else { - entry->type = ENTRY_TYPE_UNKNOWN; + entry->type = ENTRY_TYPE_OTHER; } - menu->browser.entries += 1; - if (menu->browser.entries == BROWSER_LIST_SIZE) { - break; - } + entry->size = info.fsize; } - f_closedir(&dir); + if (f_closedir(&dir) != FR_OK) { + return true; + } if (menu->browser.entries > 0) { menu->browser.selected = 0; } qsort(menu->browser.list, menu->browser.entries, sizeof(entry_t), compare_entry); + + return false; } -void push_directory (menu_t *menu, char *directory) { +static bool push_directory (menu_t *menu, char *directory) { + path_t *previous_directory = path_clone(menu->browser.directory); + path_push(menu->browser.directory, directory); - load_directory(menu); + + if (load_directory(menu)) { + path_free(menu->browser.directory); + menu->browser.directory = previous_directory; + return true; + } + + path_free(previous_directory); + + return false; } -void pop_directory (menu_t *menu) { - path_t *current_directory = path_clone(menu->browser.directory); +static bool pop_directory (menu_t *menu) { + path_t *previous_directory = path_clone(menu->browser.directory); + path_pop(menu->browser.directory); - load_directory(menu); + + if (load_directory(menu)) { + path_free(menu->browser.directory); + menu->browser.directory = previous_directory; + return true; + } + for (int i = 0; i < menu->browser.entries; i++) { - if (strcmp(menu->browser.list[i].name, path_last_get(current_directory)) == 0) { + if (strcmp(menu->browser.list[i].name, path_last_get(previous_directory)) == 0) { menu->browser.selected = i; break; } } - path_free(current_directory); + + path_free(previous_directory); + + return false; +} + +static void format_size (char *buffer, int size) { + if (size < 10000) { + sprintf(buffer, "%4d B ", size); + } else if (size < 10000000) { + sprintf(buffer, "%4d kB", size / 1024); + } else if (size < 1 * 1024 * 1024 * 1024) { + sprintf(buffer, "%4d MB", size / 1024 / 1024); + } else { + sprintf(buffer, "%4d GB", size / 1024 / 1024 / 1024); + } +} + +static void format_entry (char *buffer, entry_t *entry, bool selected) { + int cutoff_length = (entry->type == ENTRY_TYPE_DIR ? 57 : 49); + int name_length = strlen(entry->name); + strcpy(buffer, ""); + if (entry->type == ENTRY_TYPE_DIR) { + strcat(buffer, "/"); + } + if (name_length > cutoff_length) { + strncat(buffer, entry->name, cutoff_length - 1); + strcat(buffer, "…"); + } else { + strcat(buffer, entry->name); + } } static void process (menu_t *menu) { - int scroll_speed = menu->actions.fast ? BROWSER_LIST_ROWS : 1; + int scroll_speed = menu->actions.fast ? 10 : 1; if (menu->browser.entries > 1) { if (menu->actions.go_up) { @@ -134,85 +195,172 @@ static void process (menu_t *menu) { switch (entry->type) { case ENTRY_TYPE_DIR: - push_directory(menu, entry->name); + if (push_directory(menu, entry->name)) { + menu->browser.valid = false; + menu->next_mode = MENU_MODE_ERROR; + } break; - case ENTRY_TYPE_ROM: menu->next_mode = MENU_MODE_LOAD; break; - + case ENTRY_TYPE_MUSIC: + menu->next_mode = MENU_MODE_PLAYER; + break; default: menu->next_mode = MENU_MODE_FILE_INFO; break; } - } else if (menu->actions.back) { - pop_directory(menu); + } else if (menu->actions.back && !path_is_root(menu->browser.directory)) { + if (pop_directory(menu)) { + menu->browser.valid = false; + menu->next_mode = MENU_MODE_ERROR; + } } else if (menu->actions.info) { - menu->next_mode = MENU_MODE_FILE_INFO; + if (menu->browser.selected >= 0) { + menu->next_mode = MENU_MODE_FILE_INFO; + } } else if (menu->actions.settings) { menu->next_mode = MENU_MODE_CREDITS; } } static void draw (menu_t *menu, surface_t *d) { - int x = 24; - int y = 35; + char buffer[64]; + + layout_t *layout = get_layout(); + + const int text_x = layout->offset_x + layout->offset_text_x; + int text_y = layout->offset_y + layout->offset_text_y; + const int text_file_size_x = text_x + 478; + const int text_other_actions_x = text_x + 450; + const int highlight_offset = 2; + + const color_t bg_color = RGBA32(0x00, 0x00, 0x00, 0xFF); + const color_t highlight_color = RGBA32(0x3F, 0x3F, 0x3F, 0xFF); + const color_t text_color = RGBA32(0xFF, 0xFF, 0xFF, 0xFF); + const color_t directory_color = RGBA32(0xFF, 0xFF, 0x70, 0xFF); + const color_t save_color = RGBA32(0x70, 0xFF, 0x70, 0xFF); + const color_t music_color = RGBA32(0x70, 0xBC, 0xFF, 0xFF); + const color_t other_color = RGBA32(0xA0, 0xA0, 0xA0, 0xFF); int starting_position = 0; - int entries_drawn = 0; - if (menu->browser.entries > BROWSER_LIST_ROWS && menu->browser.selected >= (BROWSER_LIST_ROWS / 2)) { - starting_position = menu->browser.selected - (BROWSER_LIST_ROWS / 2); - if (starting_position >= menu->browser.entries - BROWSER_LIST_ROWS) { - starting_position = menu->browser.entries - BROWSER_LIST_ROWS; + if (menu->browser.entries > layout->main_lines && menu->browser.selected >= (layout->main_lines / 2)) { + starting_position = menu->browser.selected - (layout->main_lines / 2); + if (starting_position >= menu->browser.entries - layout->main_lines) { + starting_position = menu->browser.entries - layout->main_lines; } } - graphics_fill_screen(d, graphics_make_color(0, 0, 0, 255)); + rdpq_attach(d, NULL); + rdpq_clear(bg_color); - graphics_draw_text(d, (d->width / 2) - 36, vertical_start_position, "FILE MENU"); - graphics_draw_line(d, 0, 30, d->width, 30, 0xff); - - char str_buffer[1024]; + // Layout + fragment_borders(d); + fragment_scrollbar(d, menu->browser.selected, menu->browser.entries); + // Main screen + rdpq_font_begin(text_color); for (int i = starting_position; i < menu->browser.entries; i++) { - if (i == menu->browser.selected) { - uint32_t color; - switch (menu->browser.list[i].type) { - case ENTRY_TYPE_ROM: - color = graphics_make_color(0, 64, 0, 0); - break; - default: - color = graphics_make_color(64, 64, 64, 0); - break; - } - graphics_draw_box(d, x, y, (640 - x * 2), font_vertical_pixels, color); - } - snprintf(str_buffer, 1024, "%.74s", menu->browser.list[i].name); - graphics_draw_text(d, x, y, str_buffer); - - y += font_vertical_pixels; - - entries_drawn += 1; - if (entries_drawn == BROWSER_LIST_ROWS) { + if (i == (starting_position + layout->main_lines)) { break; } + + entry_t *entry = &menu->browser.list[i]; + bool selected = (i == menu->browser.selected); + + if (selected) { + rdpq_set_mode_fill(highlight_color); + rdpq_fill_rectangle( + layout->offset_x, + text_y + highlight_offset, + d->width - layout->offset_x - layout->scrollbar_width, + text_y + layout->line_height + highlight_offset + ); + rdpq_font_begin(text_color); + } + + switch (entry->type) { + case ENTRY_TYPE_DIR: + rdpq_set_prim_color(directory_color); + break; + case ENTRY_TYPE_SAVE: + rdpq_set_prim_color(save_color); + break; + case ENTRY_TYPE_OTHER: + rdpq_set_prim_color(other_color); + break; + case ENTRY_TYPE_MUSIC: + rdpq_set_prim_color(music_color); + break; + default: + rdpq_set_prim_color(text_color); + break; + } + + rdpq_font_position(text_x, text_y + menu->assets.font_height); + format_entry(buffer, entry, selected); + rdpq_font_print(menu->assets.font, buffer); + + if (entry->type != ENTRY_TYPE_DIR) { + rdpq_font_position(text_file_size_x, text_y + menu->assets.font_height); + format_size(buffer, entry->size); + rdpq_font_print(menu->assets.font, buffer); + } + + text_y += layout->line_height; } - graphics_draw_line(d, 0, d->height - overscan_vertical_pixels - font_vertical_pixels, d->width, d->height - overscan_vertical_pixels - font_vertical_pixels, 0xff); + if (menu->browser.entries == 0) { + rdpq_set_prim_color(other_color); + rdpq_font_position(text_x, text_y + menu->assets.font_height); + rdpq_font_print(menu->assets.font, "** empty directory **"); + } - sprintf(str_buffer, "Current Directory: SD:%s\nFile: %d of %d\n\n", path_get(menu->browser.directory), menu->browser.selected + 1, menu->browser.entries); + // Actions bar + text_y = layout->actions_y + layout->offset_text_y; + rdpq_set_prim_color(text_color); + if (menu->browser.entries > 0) { + rdpq_font_position(text_x, text_y + menu->assets.font_height); + switch (menu->browser.list[menu->browser.selected].type) { + case ENTRY_TYPE_DIR: + rdpq_font_print(menu->assets.font, "A: Enter"); + break; + case ENTRY_TYPE_ROM: + rdpq_font_print(menu->assets.font, "A: Load"); + break; + case ENTRY_TYPE_MUSIC: + rdpq_font_print(menu->assets.font, "A: Play"); + break; + default: + rdpq_font_print(menu->assets.font, "A: Info"); + break; + } + rdpq_font_position(text_other_actions_x, text_y + menu->assets.font_height); + rdpq_font_print(menu->assets.font, "Z: Info"); + } + text_y += layout->line_height; + if (!path_is_root(menu->browser.directory)) { + rdpq_font_position(text_x, text_y + menu->assets.font_height); + rdpq_font_print(menu->assets.font, "B: Back"); + } + rdpq_font_position(text_other_actions_x, text_y + menu->assets.font_height); + rdpq_font_print(menu->assets.font, "R: Settings"); + rdpq_font_end(); - graphics_draw_text(d, (d->width / 2) - 160, d->height - overscan_vertical_pixels, str_buffer); - - display_show(d); + rdpq_detach_show(); } void view_browser_init (menu_t *menu) { if (!menu->browser.valid) { - menu->browser.valid = true; - load_directory(menu); + if (load_directory(menu)) { + path_free(menu->browser.directory); + menu->browser.directory = path_init(NULL); + menu->next_mode = MENU_MODE_ERROR; + } else { + menu->browser.valid = true; + } } } diff --git a/src/menu/views/credits.c b/src/menu/views/credits.c index fdba385c..7ecc1bcf 100644 --- a/src/menu/views/credits.c +++ b/src/menu/views/credits.c @@ -1,5 +1,7 @@ #include -#include "../menu.h" + +#include "views.h" + #include "../menu_res_setup.h" diff --git a/src/menu/views/error.c b/src/menu/views/error.c index b884decc..a694f54f 100644 --- a/src/menu/views/error.c +++ b/src/menu/views/error.c @@ -1,5 +1,6 @@ #include -#include "../menu.h" + +#include "views.h" static void process (menu_t *menu) { diff --git a/src/menu/views/file_info.c b/src/menu/views/file_info.c index bd04a5e7..c080c9ab 100644 --- a/src/menu/views/file_info.c +++ b/src/menu/views/file_info.c @@ -1,9 +1,13 @@ #include #include -#include "../menu.h" + +#include "views.h" + #include "../menu_res_setup.h" #include "../../utils/str_utils.h" +#include "fragments/fragments.h" + static FILINFO info; @@ -44,6 +48,14 @@ static void process (menu_t *menu) { } static void draw (menu_t *menu, surface_t *d) { + // const color_t bg_color = RGBA32(0x00, 0x00, 0x00, 0xFF); + + // rdpq_attach(d, NULL); + // rdpq_clear(bg_color); + + // fragment_borders(d); + + char str_buffer[1024]; graphics_fill_screen(d, 0x00); @@ -88,6 +100,8 @@ static void draw (menu_t *menu, surface_t *d) { graphics_draw_text(d, (d->width / 2) - 80,d->height - overscan_vertical_pixels, "Press (B) to return!"); // centre = numchars * font_horizontal_pixels / 2 display_show(d); + + // rdpq_detach_show(); } diff --git a/src/menu/views/fragments/fragments.c b/src/menu/views/fragments/fragments.c new file mode 100644 index 00000000..cef29829 --- /dev/null +++ b/src/menu/views/fragments/fragments.c @@ -0,0 +1,66 @@ +#include "fragments.h" + + +// TODO: Prepare layout for PAL display +static layout_t layout = { + .offset_x = 32, + .offset_y = 24, + + .offset_text_x = 10, + .offset_text_y = 7, + + .line_height = 18, + .scrollbar_width = 10, + .progressbar_height = 16, + + .border_thickness = 2, + + .main_lines = 20, + + .actions_x = 20, + .actions_y = 404, + .actions_lines = 2, +}; + + +layout_t *get_layout(void) { + return &layout; +} + +void fragment_borders (surface_t *d) { + widget_border( + layout.offset_x, + layout.offset_y, + d->width - layout.offset_x, + d->height - layout.offset_y, + layout.border_thickness + ); + widget_horizontal_line( + layout.offset_x, + d->width - layout.offset_x, + layout.offset_y + ((layout.main_lines + 1) * layout.line_height), + layout.border_thickness + ); +} + +void fragment_scrollbar (surface_t *d, int position, int items) { + widget_scrollbar( + d->width - layout.offset_x - layout.scrollbar_width, + layout.offset_y, + layout.scrollbar_width, + (layout.main_lines + 1) * layout.line_height, + position, + items, + layout.main_lines + ); +} + +void fragment_progressbar (surface_t *d, float progress) { + widget_progressbar ( + layout.offset_x, + layout.actions_y - layout.border_thickness - layout.progressbar_height, + d->width - (layout.offset_x * 2), + layout.progressbar_height, + progress + ); +} diff --git a/src/menu/views/fragments/fragments.h b/src/menu/views/fragments/fragments.h new file mode 100644 index 00000000..e87658a3 --- /dev/null +++ b/src/menu/views/fragments/fragments.h @@ -0,0 +1,41 @@ +#ifndef FRAGMENTS_H__ +#define FRAGMENTS_H__ + + +#include + + +void widget_horizontal_line (int x1, int x2, int y, int thickness); +void widget_border (int x1, int y1, int x2, int y2, int thickness); +void widget_scrollbar (int x, int y, int width, int height, int position, int items, int visible_items); +void widget_progressbar (int x, int y, int width, int height, float progress); + + +typedef struct { + int offset_x; + int offset_y; + + int offset_text_x; + int offset_text_y; + + int line_height; + int scrollbar_width; + int progressbar_height; + + int border_thickness; + + int main_lines; + + int actions_x; + int actions_y; + int actions_lines; +} layout_t; + + +layout_t *get_layout(void); +void fragment_borders (surface_t *d); +void fragment_scrollbar (surface_t *d, int position, int items); +void fragment_progressbar (surface_t *d, float progress); + + +#endif diff --git a/src/menu/views/fragments/widgets.c b/src/menu/views/fragments/widgets.c new file mode 100644 index 00000000..502887cd --- /dev/null +++ b/src/menu/views/fragments/widgets.c @@ -0,0 +1,55 @@ +#include + + +void widget_horizontal_line (int x1, int x2, int y, int thickness) { + color_t line_color = RGBA32(0xFF, 0xFF, 0xFF, 0xFF); + + rdpq_set_mode_fill(line_color); + + rdpq_fill_rectangle(x1, y, x2, y + thickness); +} + +void widget_border (int x1, int y1, int x2, int y2, int thickness) { + color_t border_color = RGBA32(0xFF, 0xFF, 0xFF, 0xFF); + + rdpq_set_mode_fill(border_color); + + rdpq_fill_rectangle(x1 - thickness, y1 - thickness, x2 + thickness, y1); + rdpq_fill_rectangle(x1 - thickness, y2, x2 + thickness, y2 + thickness); + + rdpq_fill_rectangle(x1 - thickness, y1, x1, y2); + rdpq_fill_rectangle(x2, y1, x2 + thickness, y2); +} + +void widget_scrollbar (int x, int y, int width, int height, int position, int items, int visible_items) { + color_t bg_color = RGBA32(0x3F, 0x3F, 0x3F, 0xFF); + color_t inactive_color = RGBA32(0x5F, 0x5F, 0x5F, 0xFF); + color_t active_color = RGBA32(0x7F, 0x7F, 0x7F, 0xFF); + + if (items < 2 || items <= visible_items) { + rdpq_set_mode_fill(inactive_color); + rdpq_fill_rectangle(x, y, x + width, y + height); + } else { + int scroll_height = (int) ((visible_items / (float) (items)) * height); + float scroll_position = ((position / (float) (items - 1)) * (height - scroll_height)); + + rdpq_set_mode_fill(bg_color); + rdpq_fill_rectangle(x, y, x + width, y + height); + + rdpq_set_fill_color(active_color); + rdpq_fill_rectangle(x, y + scroll_position, x + width, y + scroll_position + scroll_height); + } +} + +void widget_progressbar (int x, int y, int width, int height, float progress) { + color_t bg_color = RGBA32(0x3F, 0x3F, 0x3F, 0xFF); + color_t fg_color = RGBA32(0x7F, 0x7F, 0x7F, 0xFF); + + float progress_width = progress * width; + + rdpq_set_fill_color(fg_color); + rdpq_fill_rectangle(x, y, x + progress_width, y + height); + + rdpq_set_mode_fill(bg_color); + rdpq_fill_rectangle(x + progress_width, y, x + width, y + height); +} diff --git a/src/menu/views/init.c b/src/menu/views/init.c deleted file mode 100644 index fcb95089..00000000 --- a/src/menu/views/init.c +++ /dev/null @@ -1,13 +0,0 @@ -#include -#include "../menu.h" - - -static void draw (menu_t *menu, surface_t *d) { - graphics_fill_screen(d, graphics_make_color(0, 0, 0, 255)); - - display_show(d); -} - -void view_init_display (menu_t *menu, surface_t *display) { - draw(menu, display); -} diff --git a/src/menu/views/load.c b/src/menu/views/load.c index 75e25583..7c0105d7 100644 --- a/src/menu/views/load.c +++ b/src/menu/views/load.c @@ -1,19 +1,14 @@ #include -#include "../menu.h" + +#include "flashcart/flashcart.h" + +#include "fragments/fragments.h" +#include "views.h" #include "../rom_database.h" -#include "../../flashcart/flashcart.h" -static void draw (menu_t *menu, surface_t *d) { - int x = 24; - int y = 36; +static bool load_pending; - graphics_fill_screen(d, graphics_make_color(0, 0, 0, 255)); - - graphics_draw_text(d, x, y, "booting..."); - - display_show(d); -} static void load (menu_t *menu) { menu->next_mode = MENU_MODE_BOOT; @@ -26,7 +21,7 @@ static void load (menu_t *menu) { uint8_t save_type = rom_db_match_save_type(temp_header); - if (flashcart_load_rom(path_get(path)) != FLASHCART_OK) { + if (flashcart_load_rom(path_get(path), false) != FLASHCART_OK) { menu->next_mode = MENU_MODE_ERROR; path_free(path); return; @@ -43,11 +38,38 @@ static void load (menu_t *menu) { } +static void process (menu_t *menu) { + if (menu->actions.enter) { + load_pending = true; + } else if (menu->actions.back) { + menu->next_mode = MENU_MODE_BROWSER; + } +} + +static void draw (menu_t *menu, surface_t *d) { + // layout_t *layout = get_layout(); + + const color_t bg_color = RGBA32(0x00, 0x00, 0x00, 0xFF); + + rdpq_attach(d, NULL); + rdpq_clear(bg_color); + + // Layout + fragment_borders(d); + + rdpq_detach_show(); +} + + void view_load_init (menu_t *menu) { - // Nothing to initialize (yet) + load_pending = false; } void view_load_display (menu_t *menu, surface_t *display) { + process(menu); draw(menu, display); - load(menu); + if (load_pending) { + load_pending = false; + load(menu); + } } diff --git a/src/menu/views/player.c b/src/menu/views/player.c new file mode 100644 index 00000000..ca048b8e --- /dev/null +++ b/src/menu/views/player.c @@ -0,0 +1,117 @@ +#include + +#include "fragments/fragments.h" +#include "views.h" + +#include "../mp3player.h" + + +static void format_name (char *buffer, char *name) { + int cutoff_length = 57; + int name_length = strlen(name); + strcpy(buffer, " "); + if (name_length > cutoff_length) { + strncat(buffer, name, cutoff_length - 1); + strcat(buffer, "…"); + } else { + strcat(buffer, name); + } +} + + +static void process (menu_t *menu) { + if (mp3player_process() != MP3PLAYER_OK) { + menu->next_mode = MENU_MODE_ERROR; + } else if (menu->actions.back) { + menu->next_mode = MENU_MODE_BROWSER; + } else if (menu->actions.enter) { + if (mp3player_toggle() != MP3PLAYER_OK) { + 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); + if (mp3player_seek(seconds) != MP3PLAYER_OK) { + menu->next_mode = MENU_MODE_ERROR; + } + } +} + +static void draw (menu_t *menu, surface_t *d) { + char buffer[64]; + + layout_t *layout = get_layout(); + + const int text_x = layout->offset_x + layout->offset_text_x; + int text_y = layout->offset_y + layout->offset_text_y; + + const color_t bg_color = RGBA32(0x00, 0x00, 0x00, 0xFF); + const color_t text_color = RGBA32(0xFF, 0xFF, 0xFF, 0xFF); + + rdpq_attach(d, NULL); + rdpq_clear(bg_color); + + // Layout + fragment_borders(d); + + // Progressbar + fragment_progressbar(d, mp3player_get_progress()); + + // Main screen + rdpq_font_begin(text_color); + rdpq_font_position(text_x, text_y + menu->assets.font_height); + rdpq_font_print(menu->assets.font, "Now playing:"); + text_y += layout->line_height; + rdpq_font_position(text_x, text_y + menu->assets.font_height); + format_name(buffer, menu->browser.list[menu->browser.selected].name); + rdpq_font_print(menu->assets.font, buffer); + + // Actions bar + text_y = layout->actions_y + layout->offset_text_y; + rdpq_font_position(text_x, text_y + menu->assets.font_height); + if (mp3player_is_playing()) { + rdpq_font_print(menu->assets.font, "A: Pause"); + } else if (mp3player_is_finished()) { + rdpq_font_print(menu->assets.font, "A: Play again"); + } else { + rdpq_font_print(menu->assets.font, "A: Play"); + } + 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_end(); + + rdpq_detach_show(); +} + + +void view_player_init (menu_t *menu) { + mp3player_err_t error; + + error = mp3player_init(); + if (error != MP3PLAYER_OK) { + menu->next_mode = MENU_MODE_ERROR; + mp3player_deinit(); + return; + } + + path_t *path = path_clone(menu->browser.directory); + path_push(path, menu->browser.list[menu->browser.selected].name); + + error = mp3player_load(path_get(path)); + if (error != MP3PLAYER_OK) { + menu->next_mode = MENU_MODE_ERROR; + mp3player_deinit(); + } else { + mp3player_play(); + } + + path_free(path); +} + +void view_player_display (menu_t *menu, surface_t *display) { + process(menu); + draw(menu, display); + if (menu->next_mode != MENU_MODE_PLAYER) { + mp3player_deinit(); + } +} diff --git a/src/menu/views/startup.c b/src/menu/views/startup.c new file mode 100644 index 00000000..e511cfab --- /dev/null +++ b/src/menu/views/startup.c @@ -0,0 +1,23 @@ +#include + +#include "views.h" + + +static void process (menu_t *menu) { + menu->next_mode = MENU_MODE_BROWSER; +} + +static void draw (menu_t *menu, surface_t *d) { + rdpq_attach_clear(d, NULL); + rdpq_detach_show(); +} + + +void view_startup_init (menu_t *menu) { + // Nothing to initialize (yet) +} + +void view_startup_display (menu_t *menu, surface_t *display) { + process(menu); + draw(menu, display); +} diff --git a/src/menu/views.h b/src/menu/views/views.h similarity index 71% rename from src/menu/views.h rename to src/menu/views/views.h index f7308d22..9a980c56 100644 --- a/src/menu/views.h +++ b/src/menu/views/views.h @@ -4,10 +4,11 @@ #include -#include "menu.h" +#include "../menu_state.h" -void view_init_display (menu_t *menu, surface_t *display); +void view_startup_init (menu_t *menu); +void view_startup_display (menu_t *menu, surface_t *display); void view_browser_init (menu_t *menu); void view_browser_display (menu_t *menu, surface_t *display); @@ -15,6 +16,9 @@ void view_browser_display (menu_t *menu, surface_t *display); void view_file_info_init (menu_t *menu); void view_file_info_display (menu_t *menu, surface_t *display); +void view_player_init (menu_t *menu); +void view_player_display (menu_t *menu, surface_t *display); + void view_credits_init (menu_t *menu); void view_credits_display (menu_t *menu, surface_t *display); diff --git a/src/utils/fs.c b/src/utils/fs.c index a78ffb1d..c993fee6 100644 --- a/src/utils/fs.c +++ b/src/utils/fs.c @@ -1,3 +1,6 @@ +#include +#include + #include #include "fs.h" @@ -88,3 +91,20 @@ bool file_get_sectors (char *path, uint32_t *sectors, size_t entries) { return error; } + +bool file_has_extensions (char *path, const char *extensions[]) { + char *ext = strrchr(path, '.'); + + if (ext == NULL) { + return false; + } + + while (*extensions != NULL) { + if (strcasecmp(ext + 1, *extensions) == 0) { + return true; + } + extensions++; + } + + return false; +} diff --git a/src/utils/fs.h b/src/utils/fs.h index a229dae7..18398a12 100644 --- a/src/utils/fs.h +++ b/src/utils/fs.h @@ -14,6 +14,7 @@ bool file_exists (char *path); size_t file_get_size (char *path); bool file_allocate (char *path, size_t size); bool file_get_sectors (char *path, uint32_t *sectors, size_t entries); +bool file_has_extensions (char *path, const char *extensions[]); #endif