#ifdef SUPPORT_MS_EXTENSIONS #pragma bss_seg(".file_player.data.bss") #pragma data_seg(".file_player.data") #pragma const_seg(".file_player.text.const") #pragma code_seg(".file_player.text") #endif #include "file_player.h" #include "fs/resfile.h" #include "os/os_api.h" #include "system/init.h" #include "system/includes.h" #include "app_config.h" #include "fs/fs.h" #include "effects/audio_pitchspeed.h" #include "audio_decoder.h" #include "jldemuxer.h" #include "clock_manager/clock_manager.h" #include "audio_config_def.h" #include "effects/audio_vbass.h" #if TCFG_APP_MUSIC_EN struct music_file_player_hdl { u8 player_id ; OS_MUTEX mutex; struct list_head head; #if FILE_DEC_DEST_PLAY || FILE_DEC_REPEAT_EN struct file_player *cur_player ; //当前的音乐播放器句柄 #endif }; static struct music_file_player_hdl g_file_player; static int music_file_player_start(struct file_player *); const struct stream_file_ops music_file_ops; __attribute__((weak)) void music_event_to_user(int event, u16 fname_uuid) { } void music_player_free(struct file_player *player) { if (--player->ref == 0) { if (player->break_point_flag == 1) { free(player->break_point); player->break_point = NULL; player->break_point_flag = 0; } free(player); #if FILE_DEC_DEST_PLAY || FILE_DEC_REPEAT_EN g_file_player.cur_player = NULL; #endif } } extern FILE *music_player_get_file_hdl(void); extern void music_player_remove_file_hdl(void); extern int music_player_play_auto_next(void); static void music_player_callback(void *_player_id, int event) { struct file_player *player; printf("music_callback: %x, %d\n", event, (u8)_player_id); switch (event) { case STREAM_EVENT_START: #if AUDIO_VBASS_LINK_VOLUME vbass_link_volume(); #endif if (list_empty(&(g_file_player.head))) { //先判断是否为空防止触发异常 break; } os_mutex_pend(&g_file_player.mutex, 0); player = list_first_entry(&(g_file_player.head), struct file_player, entry); if (player->player_id != (u8)_player_id) { os_mutex_post(&g_file_player.mutex); printf("player_id_not_match: %d\n", player->player_id); break; } os_mutex_post(&g_file_player.mutex); if (player->callback) { player->callback(player->priv, 0, STREAM_EVENT_START); } break; case STREAM_EVENT_PREEMPTED: break; case STREAM_EVENT_NEGOTIATE_FAILD: case STREAM_EVENT_STOP: if (list_empty(&(g_file_player.head))) { //先判断是否为空防止触发异常 break; } os_mutex_pend(&g_file_player.mutex, 0); player = list_first_entry(&(g_file_player.head), struct file_player, entry); if (player->player_id != (u8)_player_id) { os_mutex_post(&g_file_player.mutex); printf("player_id_not_match: %d\n", player->player_id); break; } //list_del(&player->entry); //jlstream_release(player->stream); //if (!list_empty(&(g_file_player.head))) { // struct file_player *p = list_first_entry(&(g_file_player.head), struct file_player, entry); // music_file_player_start(p); //} os_mutex_post(&g_file_player.mutex); if (player->callback) { player->callback(player->priv, player->read_err, STREAM_EVENT_STOP); } //music_player_free(player); /* int err = music_player_play_auto_next(); */ break; } } static int music_file_read(void *file, u8 *buf, int len) { int offset = 0; struct file_player *player = (struct file_player *)file; while (len) { if (!player->file) { break; } int rlen = 0; #if TCFG_DEC_DECRYPT_ENABLE u32 addr; addr = ftell(player->file); rlen = fread(buf + offset, len, 1, player->file); if (rlen && (rlen <= len)) { cryptanalysis_buff(&player->mply_cipher, buf + offset, addr, rlen); //解密了 } #else rlen = fread(buf + offset, len, 1, player->file); #endif if (rlen < 0 || rlen == 0) { if (rlen == (-1)) { player->read_err = 1; //file err } else { if (rlen != 0) { player->read_err = 2; //dis err } } break; } player->read_err = 0; offset += rlen; if ((len -= rlen) == 0) { break; } } return offset; } static int music_file_seek(void *file, int offset, int fromwhere) { struct file_player *player = (struct file_player *)file; return fseek(player->file, offset, fromwhere); } int music_file_flen(void *file) { struct file_player *player = (struct file_player *)file; u32 len = 0; if (player->file) { len = flen(player->file); } return len; } static int music_file_close(void *file) { struct file_player *player = (struct file_player *)file; if (player->file) { if (music_player_get_file_hdl()) { fclose(player->file); music_player_remove_file_hdl(); } player->file = NULL; } return 0; } static int music_file_get_fmt(void *file, struct stream_fmt *fmt) { u8 name[16]; struct file_player *player = (struct file_player *)file; fget_name(player->file, name, 16); struct stream_file_info info = { .file = player, .fname = (char *)name, .ops = &music_file_ops, .scene = player->scene, }; int err = jldemuxer_get_tone_file_fmt(&info, fmt); return err; } const struct stream_file_ops music_file_ops = { .read = music_file_read, .seek = music_file_seek, .close = music_file_close, .get_fmt = music_file_get_fmt, }; int music_file_player_pp(struct file_player *music_player) { if (music_player && music_player->stream) { jlstream_pp_toggle(music_player->stream, 50); if (music_player->status != FILE_PLAYER_STOP) { music_player->status = ((music_player->status == FILE_PLAYER_START) ? FILE_PLAYER_PAUSE : FILE_PLAYER_START); } return 0; } else { return -1; } } int music_file_player_ff(u16 step_s, struct file_player *music_player) { if (music_player && music_player->stream) { jlstream_node_ioctl(music_player->stream, NODE_UUID_DECODER, NODE_IOC_DECODER_FF, step_s); return 0; } else { return -1; } } int music_file_player_fr(u16 step_s, struct file_player *music_player) { if (music_player && music_player->stream) { jlstream_node_ioctl(music_player->stream, NODE_UUID_DECODER, NODE_IOC_DECODER_FR, step_s); return 0; } else { return -1; } } int music_file_get_cur_time(struct file_player *music_player) { os_mutex_pend(&g_file_player.mutex, 0); if (list_empty(&(g_file_player.head))) { //先判断是否为空 os_mutex_post(&g_file_player.mutex); return -1; } if (!music_player) { music_player = list_first_entry(&(g_file_player.head), struct file_player, entry); } if (music_player && music_player->stream) { int ret = jlstream_node_ioctl(music_player->stream, NODE_UUID_DECODER, NODE_IOC_GET_CUR_TIME, 0); os_mutex_post(&g_file_player.mutex); return ret; } os_mutex_post(&g_file_player.mutex); return -1; } int music_file_get_total_time(struct file_player *music_player) { if (music_player && music_player->stream) { return jlstream_node_ioctl(music_player->stream, NODE_UUID_DECODER, NODE_IOC_GET_TOTAL_TIME, 0); } return -1; } int music_file_get_player_status(struct file_player *music_player) { enum play_status status = FILE_PLAYER_STOP; //播放结束 if (!(list_empty(&(g_file_player.head))) && music_player && music_player->stream) { return music_player->status; } return status; } //变调接口 int music_file_pitch_up(struct file_player *music_player) { float pitch_param_table[] = {-12, -10, -8, -6, -4, -2, 0, 2, 4, 6, 8, 10, 12}; music_player->music_pitch_mode ++; if (music_player->music_pitch_mode > ARRAY_SIZE(pitch_param_table) - 1) { music_player->music_pitch_mode = ARRAY_SIZE(pitch_param_table) - 1; } printf("play pitch up+++%d\n", music_player->music_pitch_mode); int ret = music_file_set_pitch(music_player, music_player->music_pitch_mode); ret = (ret == true) ? music_player->music_pitch_mode : -1; return ret; } int music_file_pitch_down(struct file_player *music_player) { music_player->music_pitch_mode --; if (music_player->music_pitch_mode < 0) { music_player->music_pitch_mode = 0; } printf("play pitch down---%d\n", music_player->music_pitch_mode); int ret = music_file_set_pitch(music_player, music_player->music_pitch_mode); ret = (ret == true) ? music_player->music_pitch_mode : -1; return ret; } int music_file_set_pitch(struct file_player *music_player, enum _pitch_level pitch_mode) { float pitch_param_table[] = {-12, -10, -8, -6, -4, -2, 0, 2, 4, 6, 8, 10, 12}; if (pitch_mode > ARRAY_SIZE(pitch_param_table) - 1) { pitch_mode = ARRAY_SIZE(pitch_param_table) - 1; } pitch_speed_param_tool_set pitch_param = { .pitch = pitch_param_table[pitch_mode], .speed = 1, }; if (music_player) { music_player->music_pitch_mode = pitch_mode; return jlstream_node_ioctl(music_player->stream, NODE_UUID_PITCH_SPEED, NODE_IOC_SET_PARAM, (int)&pitch_param); } return -1; } int music_file_speed_up(struct file_player *music_player) //倍速播放接口 { float speed_param_table[] = {0.5, 0.75, 1.0, 1.25, 1.5, 2.0, 3.0, 4.0}; music_player->music_speed_mode ++; if (music_player->music_speed_mode > ARRAY_SIZE(speed_param_table) - 1) { music_player->music_speed_mode = ARRAY_SIZE(speed_param_table) - 1; } printf("play speed up+++%d\n", music_player->music_speed_mode); int ret = music_file_set_speed(music_player, music_player->music_speed_mode); ret = (ret == true) ? music_player->music_speed_mode : -1; return ret; } int music_file_speed_down(struct file_player *music_player) //慢速播放接口 { music_player->music_speed_mode --; if (music_player->music_speed_mode < 0) { music_player->music_speed_mode = 0; } printf("play speed down---%d\n", music_player->music_speed_mode); int ret = music_file_set_speed(music_player, music_player->music_speed_mode); ret = (ret == true) ? music_player->music_speed_mode : -1; return ret; } int music_file_set_speed(struct file_player *music_player, enum _speed_level speed_mode) //设置播放速度 { float speed_param_table[] = {0.5, 0.75, 1.0, 1.25, 1.5, 2.0, 3.0, 4.0}; if (speed_mode > ARRAY_SIZE(speed_param_table) - 1) { speed_mode = ARRAY_SIZE(speed_param_table) - 1; } pitch_speed_param_tool_set speed_param = { .pitch = 0, .speed = speed_param_table[speed_mode], }; if (music_player) { printf("set play speed ---%d, %d\n", music_player->music_speed_mode, (int)(speed_param_table[music_player->music_speed_mode] * 100)); music_player->music_speed_mode = speed_mode; return jlstream_node_ioctl(music_player->stream, NODE_UUID_PITCH_SPEED, NODE_IOC_SET_PARAM, (int)&speed_param); } return -1; } #if FILE_DEC_AB_REPEAT_EN #define AUDIO_AB_REPEAT_CODING_TYPE (AUDIO_CODING_MP3 | AUDIO_CODING_WMA | AUDIO_CODING_WAV | AUDIO_CODING_FLAC | AUDIO_CODING_APE | AUDIO_CODING_DTS) enum { AB_REPEAT_STA_NON = 0, AB_REPEAT_STA_ASTA, AB_REPEAT_STA_BSTA, }; static u8 ab_repeat_status = AB_REPEAT_STA_NON; // AB复读状态 /*----------------------------------------------------------------------------*/ /**@brief 设置AB点复读命令 @param ab_cmd: 命令 @param ab_mode: 参数 @return 1: 设置成功 0:设置失败 @note */ /*----------------------------------------------------------------------------*/ static int music_file_ab_repeat_set(int ab_cmd, int ab_mode, struct file_player *music_player) { int err = false; if (!music_player) { return false; } printf("ab repat, cmd:0x%x, mode:%d \n", ab_cmd, ab_mode); struct audio_ab_repeat_mode_param rpt = {0}; rpt.value = ab_mode; switch (ab_cmd) { case AUDIO_IOCTRL_CMD_SET_BREAKPOINT_A: printf(" SET BREAKPOINT A"); err = jlstream_node_ioctl(music_player->stream, NODE_UUID_DECODER, NODE_IOC_SET_BP_A, (int)&rpt); break; case AUDIO_IOCTRL_CMD_SET_BREAKPOINT_B: printf(" SET BREAKPOINT B"); err = jlstream_node_ioctl(music_player->stream, NODE_UUID_DECODER, NODE_IOC_SET_BP_B, (int)&rpt); break; case AUDIO_IOCTRL_CMD_SET_BREAKPOINT_MODE: printf(" CANCEL AB REPEAT"); err = jlstream_node_ioctl(music_player->stream, NODE_UUID_DECODER, NODE_IOC_SET_AB_REPEAT, (int)&rpt); break; default: break; } return err; } /*----------------------------------------------------------------------------*/ /**@brief 切换AB点复读状态 @param @return true:成功 @note */ /*----------------------------------------------------------------------------*/ int music_file_ab_repeat_switch(struct file_player *music_player) { if (!music_player) { return false; } switch (music_player->ab_repeat_status) { case AB_REPEAT_STA_NON: if (music_file_ab_repeat_set(AUDIO_IOCTRL_CMD_SET_BREAKPOINT_A, 0, music_player)) { music_player->ab_repeat_status = AB_REPEAT_STA_ASTA; } break; case AB_REPEAT_STA_ASTA: if (music_file_ab_repeat_set(AUDIO_IOCTRL_CMD_SET_BREAKPOINT_B, 0, music_player)) { music_player->ab_repeat_status = AB_REPEAT_STA_BSTA; } break; case AB_REPEAT_STA_BSTA: if (music_file_ab_repeat_set(AUDIO_IOCTRL_CMD_SET_BREAKPOINT_MODE, AB_REPEAT_MODE_CUR, music_player)) { music_player->ab_repeat_status = AB_REPEAT_STA_NON; } break; } printf("music_file_ab_repeat_switch = %d\n", music_player->ab_repeat_status); return true; } /*----------------------------------------------------------------------------*/ /**@brief 关闭AB点复读 @param @return true:成功 @note */ /*----------------------------------------------------------------------------*/ int music_file_ab_repeat_close(struct file_player *music_player) { if (!music_player) { return false; } if (music_player->ab_repeat_status == AB_REPEAT_STA_NON) { return true; } if (music_player->ab_repeat_status == AB_REPEAT_STA_ASTA) { struct stream_fmt fmt; jlstream_node_ioctl(music_player->stream, NODE_UUID_SOURCE, NODE_IOC_GET_FMT, (int)&fmt); switch (fmt.coding_type) { case AUDIO_CODING_FLAC: case AUDIO_CODING_DTS: case AUDIO_CODING_APE: music_file_ab_repeat_set(AUDIO_IOCTRL_CMD_SET_BREAKPOINT_B, 0, music_player); break; } } music_file_ab_repeat_set(AUDIO_IOCTRL_CMD_SET_BREAKPOINT_MODE, AB_REPEAT_MODE_CUR, music_player); music_player->ab_repeat_status = AB_REPEAT_STA_NON; return true; } #endif /*FILE_DEC_AB_REPEAT_EN*/ int music_file_get_breakpoints(struct audio_dec_breakpoint *bp, struct file_player *music_player) { if (music_player) { return jlstream_node_ioctl(music_player->stream, NODE_UUID_DECODER, NODE_IOC_GET_BP, (int)bp); } return -1; } #if FILE_DEC_REPEAT_EN //无缝循环播放 /*----------------------------------------------------------------------------*/ /**@brief 循环播放回调接口 @param *priv: 私有参数 @return 0:循环播放 @return 非0:结束循环 @note */ /*----------------------------------------------------------------------------*/ static int file_dec_repeat_cb(void *priv) { struct file_player *music_player = priv; y_printf("file_dec_repeat_cb\n"); if (music_player->repeat_num) { music_player->repeat_num--; } else { printf("file_dec_repeat_cb end\n"); return -1; } return 0; } /*----------------------------------------------------------------------------*/ /**@brief 设置循环播放次数 @param repeat_num: 循环次数 @return true:成功 @return false:失败 @note */ /*----------------------------------------------------------------------------*/ static struct audio_repeat_mode_param rep = {0}; int file_dec_repeat_set(u8 repeat_num, u32 coding_type) { struct file_player *music_player = g_file_player.cur_player; //当前的音乐播放器句柄 if (!music_player) { return false; } switch (music_player->stream->coding_type) { case AUDIO_CODING_MP3: case AUDIO_CODING_WAV: { music_player->repeat_num = repeat_num; /* struct audio_repeat_mode_param rep = {0}; */ rep.flag = 1; //使能 rep.headcut_frame = 2; //依据需求砍掉前面几帧,仅mp3格式有效 rep.tailcut_frame = 2; //依据需求砍掉后面几帧,仅mp3格式有效 rep.repeat_callback = file_dec_repeat_cb; rep.callback_priv = music_player; rep.repair_buf = &music_player->repair_buf; jlstream_node_ioctl(music_player->stream, NODE_UUID_DECODER, NODE_IOC_DECODER_REPEAT, (int)&rep); /* audio_decoder_ioctrl(&dec->file_dec.decoder, AUDIO_IOCTRL_CMD_REPEAT_PLAY, &rep); */ } return true; } return false; } #endif #if FILE_DEC_DEST_PLAY /*----------------------------------------------------------------------------*/ /**@brief 跳到指定位置开始播放,播放到目标时间后回调 @param start_time: 要跳转过去播放的起始时间 @param dest_time: 要跳转过去播放的目标时间 @param *cb: 到达目标时间后回调 @param *cb_priv: 回调参数 @return true:成功 @return false:失败 @note */ /*----------------------------------------------------------------------------*/ struct audio_dest_time_play_param param = {0}; int file_dec_set_start_dest_play(u32 start_time, u32 dest_time, u32(*cb)(void *), void *cb_priv, u32 coding_type, struct file_player *music_player) { if (!music_player) { return false; } switch (coding_type) { case AUDIO_CODING_MP3: { /* struct audio_dest_time_play_param param = {0}; */ param.start_time = start_time; param.dest_time = dest_time; param.callback_func = cb; param.callback_priv = cb_priv; jlstream_node_ioctl(music_player->stream, NODE_UUID_DECODER, NODE_IOC_DECODER_DEST_PLAY, (int)¶m); } return true; } return false; } /*----------------------------------------------------------------------------*/ /**@brief 跳到指定位置开始播放 @param start_time: 要跳转过去播放的起始时间 @return true:成功 @return false:失败 @note */ /*----------------------------------------------------------------------------*/ int file_dec_set_start_play(u32 start_time, u32 coding_type) { return file_dec_set_start_dest_play(start_time, 0x7fffffff, NULL, NULL, g_file_player.cur_player->stream->coding_type, g_file_player.cur_player); } #endif static int music_file_player_start(struct file_player *player) { int err = -EINVAL; u16 uuid = jlstream_event_notify(STREAM_EVENT_GET_PIPELINE_UUID, (int)"music"); player->stream = jlstream_pipeline_parse(uuid, NODE_UUID_MUSIC); if (!player->stream) { goto __exit0; } #if TCFG_USER_EMITTER_ENABLE extern u8 *get_cur_connect_emitter_mac_addr(void); void *bt_addr = get_cur_connect_emitter_mac_addr(); if (!bt_addr) { printf("========================= err bt_addr null"); } else { jlstream_node_ioctl(player->stream, NODE_UUID_A2DP_TX, NODE_IOC_SET_BTADDR, (int)bt_addr); } #endif int player_id = player->player_id; jlstream_set_callback(player->stream, (void *)player_id, music_player_callback); jlstream_set_scene(player->stream, player->scene); jlstream_set_coexist(player->stream, player->coexist); jlstream_node_ioctl(player->stream, NODE_UUID_DECODER, NODE_IOC_SET_BP, (int)player->break_point); jlstream_node_ioctl(player->stream, NODE_UUID_DECODER, NODE_IOC_SET_FILE_LEN, (int)music_file_flen(player)); if (player->callback) { err = player->callback(player->priv, 0, STREAM_EVENT_INIT); if (err) { goto __exit1; } } jlstream_set_dec_file(player->stream, player, &music_file_ops); err = jlstream_start(player->stream); if (player->stream->coding_type == AUDIO_CODING_MP3) { #if TCFG_DEC_ID3_V1_ENABLE if (player->p_mp3_id3_v1) { id3_obj_post(&player->p_mp3_id3_v1); } player->p_mp3_id3_v1 = id3_v1_obj_get(player->file); #endif #if TCFG_DEC_ID3_V2_ENABLE if (player->p_mp3_id3_v2) { id3_obj_post(&player->p_mp3_id3_v2); } player->p_mp3_id3_v2 = id3_v2_obj_get(player->file); #endif } if (err) { goto __exit1; } return 0; __exit1: jlstream_release(player->stream); __exit0: list_del(&player->entry); if (player->callback) { /* err = player->callback(player->priv, 0,STREAM_EVENT_NONE); */ } music_player_free(player); return err; } int music_player_init(struct file_player *player, void *file, struct audio_dec_breakpoint *dbp) { player->ref = 1; if (!file) { printf("music_player_faild\n"); return -EINVAL; } player->file = file; player->scene = STREAM_SCENE_MUSIC; player->player_id = g_file_player.player_id++; player->coexist = STREAM_COEXIST_AUTO; if (dbp == NULL) { player->break_point = zalloc(sizeof(struct audio_dec_breakpoint) + BREAKPOINT_DATA_LEN); player->break_point->data_len = BREAKPOINT_DATA_LEN; player->break_point_flag = 1; } else { player->break_point = dbp; player->break_point_flag = 0; } player->music_speed_mode = PLAY_SPEED_1; //固定开始的时候使用1倍速播放 */ player->music_pitch_mode = PITCH_0; //固定开始时不变调 #if TCFG_DEC_DECRYPT_ENABLE cipher_init(&player->mply_cipher, TCFG_DEC_DECRYPT_KEY); void cipher_check_decode_file(CIPHER * pcipher, void *file); cipher_check_decode_file(&player->mply_cipher, file); #endif INIT_LIST_HEAD(&player->entry); return 0; } static struct file_player *music_player_create(void *file, struct audio_dec_breakpoint *dbp) { struct file_player *player; player = zalloc(sizeof(*player)); if (!player) { return NULL; } #if FILE_DEC_DEST_PLAY || FILE_DEC_REPEAT_EN g_file_player.cur_player = player; #endif int err = music_player_init(player, file, dbp); if (err) { music_player_free(player); return NULL; } return player; } struct file_player *music_player_add(struct file_player *player) { os_mutex_pend(&g_file_player.mutex, 0); if (list_empty(&(g_file_player.head))) { int err = music_file_player_start(player); if (err) { os_mutex_post(&g_file_player.mutex); return NULL; } player->status = FILE_PLAYER_START; } list_add_tail(&player->entry, &(g_file_player.head)); os_mutex_post(&g_file_player.mutex); return player; } struct file_player *music_file_play(FILE *file, struct audio_dec_breakpoint *dbp) { struct file_player *player; player = music_player_create(file, dbp); if (!player) { return NULL; } return music_player_add(player); } struct file_player *music_file_play_callback(FILE *file, void *priv, music_player_cb_t callback, struct audio_dec_breakpoint *dbp) { struct file_player *player; player = music_player_create(file, dbp); if (!player) { return NULL; } player->priv = priv; player->callback = callback; return music_player_add(player); } int music_player_runing() { local_irq_disable(); int ret = list_empty(&(g_file_player.head)) ? 0 : 1; local_irq_enable(); return ret; } void music_file_player_stop() { struct file_player *player, *n; os_mutex_pend(&g_file_player.mutex, 0); list_for_each_entry_safe(player, n, &(g_file_player.head), entry) { __list_del_entry(&player->entry); if (player->stream) { jlstream_stop(player->stream, 50); jlstream_release(player->stream); } #if TCFG_DEC_ID3_V1_ENABLE if (player->p_mp3_id3_v1) { id3_obj_post(&player->p_mp3_id3_v1); } #endif #if TCFG_DEC_ID3_V2_ENABLE if (player->p_mp3_id3_v2) { id3_obj_post(&player->p_mp3_id3_v2); } #endif music_player_free(player); } jlstream_event_notify(STREAM_EVENT_CLOSE_PLAYER, (int)"music"); os_mutex_post(&g_file_player.mutex); } struct file_player *get_music_file_player(void) //返回第一个打开的音乐播放器指针 { struct file_player *player = NULL; os_mutex_pend(&g_file_player.mutex, 0); if (list_empty(&(g_file_player.head))) { //先判断是否为空 os_mutex_post(&g_file_player.mutex); return NULL; } player = list_first_entry(&(g_file_player.head), struct file_player, entry); os_mutex_post(&g_file_player.mutex); return player; } static int __music_player_init() { INIT_LIST_HEAD(&g_file_player.head); os_mutex_create(&g_file_player.mutex); return 0; } __initcall(__music_player_init); #endif