MrzhangF1ghter 4 rokov pred
commit
82c37502e2
9 zmenil súbory, kde vykonal 2300 pridanie a 0 odobranie
  1. 201 0
      LICENSE
  2. 139 0
      README.md
  3. 140 0
      README_ZH.md
  4. 26 0
      SConscript
  5. 204 0
      inc/mp3_player.h
  6. 99 0
      inc/mp3_tag.h
  7. 718 0
      src/mp3_player.c
  8. 208 0
      src/mp3_player_cmd.c
  9. 565 0
      src/mp3_tag.c

+ 201 - 0
LICENSE

@@ -0,0 +1,201 @@
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.

+ 139 - 0
README.md

@@ -0,0 +1,139 @@
+# mp3player
+
+[中文页](README_ZH.md) | English
+
+## 1. Introduction
+
+**mp3player** is a simple mp3 format music player that provides functions for playing mp3 files,decode id3v1,id3v2 tag, supporting functions such as play, stop, pause, resume, seek,and volume adjustment.
+
+### 1.1. File structure
+
+| Folder | Description |
+| ---- | ---- |
+| src | Core source code, which mainly implements mp3 playback and tag decode, and export Finsh command line |
+| inc | Header file directory |
+
+### 1.2 License
+
+The mp3player package complies with the Apache 2.0 license, see the `LICENSE` file for details.
+
+### 1.3 Dependency
+
+- RT-Thread 4.0+
+- RT-Thread Audio driver framework
+- optparse command line parameter parsing package
+- helix mp3 decoder
+
+### 1.4 Configuration Macro Description
+
+
+```shell
+ --- mp3 player: Minimal music player for mp3 file play.   
+ [*]   Enable mp3 player                                   
+ (sound0) The play device name                                       
+ (2048) mp3 input buffer size                                   
+ (4608) mp3 output buffer size   
+ (50)  mp3 player default volume                                 
+       Version (v1.0.0)  --->  
+```
+
+**The play device name**: Specify the sound card device used for playback, default `sound0`
+
+## 2. Use
+
+Common functions of mp3player have been exported to Finsh command line for developers to test and use.
+
+**The functions provided by the play command are as follows**
+
+```shell
+msh />mp3play -help
+usage: mp3play [option] [target] ...
+
+usage options:
+  -h,     --help                     Print defined help message.
+  -s URI, --start=URI                Play mp3 music with URI(local files).
+  -t,     --stop                     Stop playing music.
+  -p,     --pause                    Pause the music.
+  -r,     --resume                   Resume the music.
+  -v lvl, --volume=lvl               Change the volume(0~99).
+  -d,     --dump                     Dump play relevant information.
+  -j      --jump                     Jump to seconds that given.
+```
+
+### 2.1 Play function
+
+- Start playing
+
+```shell
+msh />mp3play -s bryan_adams_-_here_i_am.mp3
+[I/mp3 player]: play start, uri=bryan_adams_-_here_i_am.mp3
+msh />------------MP3 INFO------------
+Title:Here I Am
+Artist:Bryan Adams
+Year:2002
+Comment:Spirit: Stallion Of The Cimarr
+Genre:Blues
+Length:04:45
+Bitrate:320 kbit/s
+Frequency:44100 Hz
+--------------------------------
+```
+- seek play
+```shell
+jump to 100 seconds
+msh />mp3play -j 100
+```
+
+- Stop play
+
+```shell
+msh />mp3play -t
+[I/mp3 player] play end
+```
+
+- Pause playback
+
+```shell
+msh />mp3play -p
+```
+
+- Resume playback
+
+```shell
+msh />mp3play -r
+```
+
+- Set volume
+
+```shell
+msh />mp3play -v 50
+```
+
+```shell
+msh />mp3play -d
+
+- dump info
+mp3_player status:
+uri     - bryan_adams_-_here_i_am.mp3
+status  - PLAYING
+volume  - 10
+00:03 / 04:45
+------------MP3 INFO------------
+Title:Here I Am
+Artist:Bryan Adams
+Year:2002
+Comment:Spirit: Stallion Of The Cimarr
+Genre:Blues
+Length:04:45
+Bitrate:320 kbit/s
+Frequency:44100 Hz
+--------------------------------
+```
+## 3. Matters needing attention
+
+- 
+
+## 4. Contact
+
+- Maintenance: MrzhangF1ghter
+- Homepage: https://github.com/MrzhangF1ghter/mp3player

+ 140 - 0
README_ZH.md

@@ -0,0 +1,140 @@
+# mp3player
+
+中文页 | [English](README.md)
+
+## 1. 简介
+
+**mp3player** 是一个简易的 mp3 格式的音乐播放器,提供播放mp3 文件的功能,支持获取MP3 标签信息、播放、跳转、停止、暂停、恢复,以及音量调节等功能。
+
+### 1.1. 文件结构
+
+| 文件夹 | 说明 |
+| ---- | ---- |
+| src  | 核心源码,主要实现 MP3 播放和MP3 标签解析,以及导出 Finsh 命令行 |
+| inc  | 头文件目录 |
+
+### 1.2 许可证
+
+mp3player package 遵循 Apache 2.0 许可,详见 `LICENSE` 文件。
+
+### 1.3 依赖
+
+- RT-Thread 4.0+
+- RT-Thread Audio 驱动框架
+- optparse 命令行参数解析软件包
+- helix MP3解码软件包
+
+### 1.4 配置宏说明
+
+```shell
+ --- mp3 player: Minimal music player for mp3 file play.   
+ [*]   Enable mp3 player                                   
+ (sound0) The play device name                                       
+ (2048) mp3 input buffer size                                   
+ (4608) mp3 output buffer size   
+ (50)  mp3 player default volume                                 
+       Version (v1.0.0)  --->  
+```
+
+**The play device name**:指定播放使用的声卡设备,默认`sound0`  
+
+## 2. 使用
+
+mp3player 的常用功能已经导出到 Finsh 命令行,以便开发者测试和使用。
+
+**播放命令提供的功能如下 **
+
+```shell
+msh />mp3play -help
+usage: mp3play [option] [target] ...
+
+usage options:
+  -h,     --help                     Print defined help message.
+  -s URI, --start=URI                Play mp3 music with URI(local files).
+  -t,     --stop                     Stop playing music.
+  -p,     --pause                    Pause the music.
+  -r,     --resume                   Resume the music.
+  -v lvl, --volume=lvl               Change the volume(0~99).
+  -d,     --dump                     Dump play relevant information.
+  -j      --jump                     Jump to seconds that given.
+```
+
+### 2.1 播放功能
+
+- 开始播放
+
+```shell
+msh />mp3play -s bryan_adams_-_here_i_am.mp3
+[I/mp3 player]: play start, uri=bryan_adams_-_here_i_am.mp3
+msh />------------MP3 INFO------------
+Title:Here I Am
+Artist:Bryan Adams
+Year:2002
+Comment:Spirit: Stallion Of The Cimarr
+Genre:Blues
+Length:04:45
+Bitrate:320 kbit/s
+Frequency:44100 Hz
+--------------------------------
+```
+
+- 跳转播放
+```shell
+跳转至100秒
+msh />mp3play -j 100
+```
+
+- 停止播放
+
+```shell
+msh />mp3play -t
+[I/mp3 player] play end
+```
+
+- 暂停播放
+
+```shell
+msh />mp3play -p
+```
+
+- 恢复播放
+
+```shell
+msh />mp3play -r
+```
+
+- 设置音量
+
+```shell
+msh />mp3play -v 50
+```
+
+- 打印播放信息
+```shell
+msh />mp3play -d
+
+mp3_player status:
+uri     - bryan_adams_-_here_i_am.mp3
+status  - PLAYING
+volume  - 10
+00:03 / 04:45
+------------MP3 INFO------------
+Title:Here I Am
+Artist:Bryan Adams
+Year:2002
+Comment:Spirit: Stallion Of The Cimarr
+Genre:Blues
+Length:04:45
+Bitrate:320 kbit/s
+Frequency:44100 Hz
+--------------------------------
+```
+
+## 3. 注意事项
+
+- 待补充
+
+## 4. 联系方式
+
+- 维护:MrzhangF1ghter
+- 主页:https://github.com/MrzhangF1ghter/mp3player

+ 26 - 0
SConscript

@@ -0,0 +1,26 @@
+'''
+Author: your name
+Date: 2021-05-25 14:57:35
+LastEditTime: 2021-06-04 14:31:00
+LastEditors: Please set LastEditors
+Description: In User Settings Edit
+FilePath: \gd32f407-sbrgate\third-packages\mp3player\SConscript
+'''
+Import('rtconfig')
+from building import *
+
+cwd = GetCurrentDir()
+
+path = [cwd,
+    cwd + '/inc']
+
+src = []
+src +=  Split('''
+        src/mp3_player.c
+        src/mp3_player_cmd.c
+        src/mp3_tag.c
+        ''')
+
+group = DefineGroup('mp3player', src, depend = ['PKG_USING_MP3PLAYER'], CPPPATH = path)
+
+Return('group')

+ 204 - 0
inc/mp3_player.h

@@ -0,0 +1,204 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * Date           Author       Notes
+ * 2021-06-02     MrzhangF1ghter    first implementation
+ */
+
+#ifndef __MP3_PLAYER_H__
+#define __MP3_PLAYER_H__
+
+#include <stdio.h>
+#include <rtdevice.h>
+
+#include "mp3dec.h" /* helix include files */
+
+enum MSG_TYPE
+{
+    MSG_NONE = 0,
+    MSG_START = 1,
+    MSG_STOP = 2,
+    MSG_PAUSE = 3,
+    MSG_RESUME = 4,
+};
+
+enum PLAYER_EVENT
+{
+    PLAYER_EVENT_NONE = 0,
+    PLAYER_EVENT_PLAY = 1,
+    PLAYER_EVENT_STOP = 2,
+    PLAYER_EVENT_PAUSE = 3,
+    PLAYER_EVENT_RESUME = 4,
+};
+
+struct play_msg
+{
+    int type;
+    void *data;
+};
+
+#pragma pack(1)
+typedef struct
+{
+    uint8_t *read_ptr;
+    int read_offset;
+    int bytes_left;
+} decode_oper_t;
+
+/* 
+ * music basic info structure definition
+ */
+typedef struct
+{
+    uint8_t title[30];
+    uint8_t artist[30];
+    uint8_t year[4];
+    uint8_t comment[30];
+    uint8_t genre;
+} mp3_basic_info_t;
+
+/* 
+ * mp3_info structure definition
+ */
+typedef struct
+{
+    mp3_basic_info_t mp3_basic_info;
+    uint32_t total_seconds;
+    uint32_t curent_seconds;
+
+    uint32_t bitrate;
+    uint32_t samplerate;
+    uint16_t outsamples;
+    uint8_t vbr;
+    uint32_t data_start;
+    long file_size;
+} mp3_info_t;
+
+/* 
+ * mp3 player main structure definition
+ */
+struct mp3_player
+{
+    int state;
+    char *uri;
+    uint8_t *in_buffer;
+    uint16_t *out_buffer;
+    rt_device_t audio_device;
+    rt_mq_t mq;
+    rt_mutex_t lock;
+    struct rt_completion ack;
+    FILE *fp;
+
+    int volume;
+
+    /* helix decoder */
+    HMP3Decoder mp3_decoder;
+    MP3FrameInfo mp3_frameinfo;
+
+    mp3_info_t mp3_info;
+
+    decode_oper_t decode_oper;
+};
+#pragma pack()
+
+/**
+ * mp3 player status
+ */
+enum PLAYER_STATE
+{
+    PLAYER_STATE_STOPED = 0,
+    PLAYER_STATE_PLAYING = 1,
+    PLAYER_STATE_PAUSED = 2,
+};
+
+/**
+ * @brief             Play wav music
+ *
+ * @param uri         the pointer for file path
+ *
+ * @return
+ *      - 0      Success
+ *      - others Failed
+ */
+int mp3_player_play(char *uri);
+
+/**
+ * @brief             Stop music
+ *
+ * @return
+ *      - 0      Success
+ *      - others Failed
+ */
+int mp3_player_stop(void);
+
+/**
+ * @brief             Pause music
+ *
+ * @return
+ *      - 0      Success
+ *      - others Failed
+ */
+int mp3_player_pause(void);
+
+/**
+ * @brief             Resume music
+ *
+ * @return
+ *      - 0      Success
+ *      - others Failed
+ */
+int mp3_player_resume(void);
+
+/**
+ * @brief             Sev volume
+ *
+ * @param volume      volume value(0 ~ 99)
+ *
+ * @return
+ *      - 0      Success
+ *      - others Failed
+ */
+int mp3_player_volume_set(int volume);
+
+/**
+ * @brief             Get volume
+ *
+ * @return            volume value(0~00)
+ */
+int mp3_player_volume_get(void);
+
+/**
+ * @brief             Get wav player state
+ *
+ * @return
+ *      - PLAYER_STATE_STOPED   stoped status
+ *      - PLAYER_STATE_PLAYING  playing status
+ *      - PLAYER_STATE_PAUSED   paused
+ */
+int mp3_player_state_get(void);
+
+/**
+ * @brief             Get the uri that is currently playing
+ *
+ * @return            uri that is currently playing
+ */
+char *mp3_player_uri_get(void);
+
+/**
+ * @brief             show mp3 info
+ */
+void mp3_info_show(void);
+
+/**
+ * @brief             show mp3 info
+ */
+void mp3_disp_time(void);
+
+/**
+ * @brief             seek to destination seconds
+ *
+ * @return            the error code,0 on success
+ */
+rt_err_t mp3_seek(uint32_t seconds);
+
+#endif

+ 99 - 0
inc/mp3_tag.h

@@ -0,0 +1,99 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * Date           Author       Notes
+ * 2021-06-02     MrzhangF1ghter    first implementation
+ */
+
+#ifndef __MP3_TAG_H__
+#define __MP3_TAG_H__
+
+#include <stdint.h>
+#include <stdio.h>
+#include "mp3_player.h"
+
+#define TITLE_LEN_MAX 30
+#define ARTIST_LEN_MAX 30
+#define ALBUM_LEN_MAX 30
+
+/* 
+ *  ID3V1 TAG
+ */
+typedef struct
+{
+    uint8_t id[3];
+    uint8_t title[30];
+    uint8_t artist[30];
+    uint8_t year[4];
+    uint8_t comment[30];
+    uint8_t genre;
+} ID3V1_Tag_t;
+
+/* 
+ *  ID3V2 TAG header
+ */
+typedef struct
+{
+    uint8_t id[3];
+    uint8_t mversion;
+    uint8_t sversion;
+    uint8_t flags;
+    uint8_t size[4];
+} ID3V2_TagHead_t;
+
+/* 
+ *  ID3V2.3 TAG header
+ */
+typedef struct
+{
+    uint8_t id[4];
+    uint8_t size[4];
+    uint16_t flags;
+} ID3V23_FrameHead_t;
+
+/* 
+ *  MP3 Xing Frame
+ */
+typedef struct
+{
+    uint8_t id[4];
+    uint8_t flags[4];
+    uint8_t frames[4];
+    uint8_t fsize[4];
+} MP3_FrameXing_t;
+
+/* 
+ *  MP3 VBRI Frame
+ */
+typedef struct
+{
+    uint8_t id[4];      /* 帧ID,为Xing/Info */
+    uint8_t version[2]; /* 版本号 */
+    uint8_t delay[2];   /* 延迟 */
+    uint8_t quality[2]; /* 音频质量,0~100,越大质量越好 */
+    uint8_t fsize[4];   /* 文件总大小 */
+    uint8_t frames[4];  /* 文件总帧数  */
+} MP3_FrameVBRI_t;
+
+/**
+ * @description: Get genre string by genre id
+ * @param {uint16_t} genre_id [0,147]
+ * @return {char *} genre string
+ */
+char *mp3_get_genre_string_by_id(uint16_t genre_id);
+
+/**
+ * @description: get mp3 tag info
+ * @param {struct mp3_player} *player
+ * @return the error code,0 on success
+ */
+rt_err_t mp3_get_info(struct mp3_player *player);
+
+/**
+ * @description: print mp3 info
+ * @param {mp3_info_t} mp3_info
+ * @return the error code,0 on success
+ */
+rt_err_t mp3_info_print(mp3_info_t mp3_info);
+
+#endif

+ 718 - 0
src/mp3_player.c

@@ -0,0 +1,718 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * Date           Author       Notes
+ * 2021-06-02     MrzhangF1ghter    first implementation
+ */
+
+#include "mp3_player.h"
+#include <string.h>
+
+#define LOG_TAG "mp3 player"
+#define LOG_LVL DBG_INFO
+#include <ulog.h>
+
+#include "mp3_tag.h"
+
+#define VOLUME_MIN (0)
+#define VOLUME_MAX (100)
+
+#define MP3_PLAYER_MSG_SIZE (10)
+#define MP3_THREAD_STATCK_SIZE (1024 * 2)
+#define MP3_THREAD_PRIORITY (15)
+
+static struct mp3_player player = {0};
+
+#if (LOG_LVL >= DBG_LOG)
+
+static const char *state_str[] =
+    {
+        "STOPPED",
+        "PLAYING",
+        "PAUSED",
+};
+
+static const char *event_str[] =
+    {
+        "NONE",
+        "PLAY"
+        "STOP"
+        "PAUSE"
+        "RESUME"};
+
+#endif
+
+static char *MP3Decode_ERR_CODE_get(int err_code);
+
+/**
+ * @description: lock player
+ * @param None
+ * @return None
+ */
+static void play_lock(void)
+{
+    rt_mutex_take(player.lock, RT_WAITING_FOREVER);
+}
+
+/**
+ * @description: unlock player
+ * @param None
+ * @return None
+ */
+static void play_unlock(void)
+{
+    rt_mutex_release(player.lock);
+}
+
+/**
+ * @description: send msg to player
+ * @param {struct mp3_player} *player
+ * @param {int} type
+ * @param {void} *data
+ * @return the error code,0 on success
+ */
+static rt_err_t play_msg_send(struct mp3_player *player, int type, void *data)
+{
+    struct play_msg msg;
+
+    msg.type = type;
+    msg.data = data;
+
+    return rt_mq_send(player->mq, &msg, sizeof(struct play_msg));
+}
+
+/**
+ * @description: start playing
+ * @param {char} *uri
+ * @return the error code,0 on success
+ */
+int mp3_player_play(char *uri)
+{
+    rt_err_t result = RT_EOK;
+
+    rt_completion_init(&player.ack);
+    play_lock();
+    if (player.state != PLAYER_STATE_STOPED)
+    {
+        mp3_player_stop();
+    }
+    if (player.uri)
+    {
+        rt_free(player.uri);
+    }
+    player.uri = rt_strdup(uri);
+    result = play_msg_send(&player, MSG_START, RT_NULL);
+    rt_completion_wait(&player.ack, RT_WAITING_FOREVER);
+    play_unlock();
+
+    return result;
+}
+
+/**
+ * @description: stop playing
+ * @param None
+ * @return the error code,0 on success
+ */
+int mp3_player_stop(void)
+{
+    rt_err_t result = RT_EOK;
+
+    rt_completion_init(&player.ack);
+
+    play_lock();
+    if (player.state != PLAYER_STATE_STOPED)
+    {
+        result = play_msg_send(&player, MSG_STOP, RT_NULL);
+        rt_completion_wait(&player.ack, RT_WAITING_FOREVER);
+    }
+    play_unlock();
+
+    return result;
+}
+
+/**
+ * @description: pause playing
+ * @param None
+ * @return the error code,0 on success
+ */
+int mp3_player_pause(void)
+{
+    rt_err_t result = RT_EOK;
+
+    rt_completion_init(&player.ack);
+    play_lock();
+    if (player.state == PLAYER_STATE_PLAYING)
+    {
+        result = play_msg_send(&player, MSG_PAUSE, RT_NULL);
+        rt_completion_wait(&player.ack, RT_WAITING_FOREVER);
+    }
+    play_unlock();
+
+    return result;
+}
+
+/**
+ * @description: resume playing
+ * @param None
+ * @return the error code,0 on success
+ */
+int mp3_player_resume(void)
+{
+    rt_err_t result = RT_EOK;
+    rt_completion_init(&player.ack);
+    play_lock();
+    if (player.state == PLAYER_STATE_PAUSED)
+    {
+        result = play_msg_send(&player, MSG_RESUME, RT_NULL);
+        rt_completion_wait(&player.ack, RT_WAITING_FOREVER);
+    }
+    play_unlock();
+    return result;
+}
+
+/**
+ * @description: set volume
+ * @param {int} volume
+ * @return the error code,0 on success
+ */
+int mp3_player_volume_set(int volume)
+{
+    struct rt_audio_caps caps;
+    if (volume < VOLUME_MIN)
+        volume = VOLUME_MIN;
+    else if (volume > VOLUME_MAX)
+        volume = VOLUME_MAX;
+    player.audio_device = rt_device_find(MP3_SOUND_DEVICE_NAME);
+    if (player.audio_device == RT_NULL)
+        return RT_ERROR;
+
+    player.volume = volume;
+    caps.main_type = AUDIO_TYPE_MIXER;
+    caps.sub_type = AUDIO_MIXER_VOLUME;
+    caps.udata.value = volume;
+
+    LOG_D("set volume = %d", volume);
+    return rt_device_control(player.audio_device, AUDIO_CTL_CONFIGURE, &caps);
+}
+
+/**
+ * @description: get current player volume
+ * @param None
+ * @return volume
+ */
+int mp3_player_volume_get(void)
+{
+    return player.volume;
+}
+
+/**
+ * @description: get current player state
+ * @param None
+ * @return enum PLAYER_STATE
+ */
+int mp3_player_state_get(void)
+{
+    return player.state;
+}
+
+/**
+ * @description: get current player uri
+ * @param None
+ * @return pointer to uri
+ */
+char *mp3_player_uri_get(void)
+{
+    return player.uri;
+}
+
+/**
+ * @description: get mp3 current time in seconds
+ * @param {FILE} *fp
+ * @param {mp3_info_t} *mp3_info
+ * @return current seconds,-1 on error
+ */
+uint32_t mp3_get_cur_seconds(void)
+{
+    uint32_t fpos = 0;
+    uint32_t fptr;
+    uint32_t curent_seconds;
+
+    if (player.fp == RT_NULL)
+    {
+        return -1;
+    }
+    fptr = ftell(player.fp);
+    if (fptr > player.mp3_info.data_start)
+        fpos = fptr - player.mp3_info.data_start;
+
+    curent_seconds = fpos * player.mp3_info.total_seconds / (player.mp3_info.file_size - player.mp3_info.data_start);
+    player.mp3_info.curent_seconds = curent_seconds;
+    return curent_seconds;
+}
+
+/**
+ * @description: seek to destination seconds
+ * @param {uint32_t} seconds
+ * @return the error code,0 on success
+ */
+rt_err_t mp3_seek(uint32_t seconds)
+{
+    long fpos;
+    if (seconds > player.mp3_info.total_seconds)
+        return RT_ERROR;
+    /* calculate position by seconds*/
+    fpos = seconds * (player.mp3_info.bitrate / 8) + player.mp3_info.data_start;
+    if (fpos < player.mp3_info.data_start)
+        return RT_ERROR;
+    return fseek(player.fp, fpos + player.mp3_info.data_start, SEEK_SET);
+}
+
+/**
+ * @description: show mp3 info
+ * @param None
+ * @return None
+ */
+void mp3_info_show(void)
+{
+    mp3_info_print(player.mp3_info);
+}
+
+/**
+ * @description: show mp3 current seconds
+ * @param None
+ * @return None
+ */
+void mp3_disp_time(void)
+{
+    uint32_t cur_seconds;
+    cur_seconds = mp3_get_cur_seconds();
+    rt_kprintf("%02d:%02d / %02d:%02d\r\n", cur_seconds / 60, cur_seconds % 60, player.mp3_info.total_seconds / 60, player.mp3_info.total_seconds % 60);
+}
+
+/**
+ * @description: open mp3 player
+ * @param {struct mp3_player} *player
+ * @return the error code,0 on success
+ */
+static rt_err_t mp3_player_open(struct mp3_player *player)
+{
+    rt_err_t result = RT_EOK;
+    struct rt_audio_caps caps;
+
+    /* find device */
+    player->audio_device = rt_device_find(MP3_SOUND_DEVICE_NAME);
+    if (player->audio_device == RT_NULL)
+    {
+        LOG_E("audio_device %s not found", MP3_SOUND_DEVICE_NAME);
+        result = -RT_ERROR;
+        goto __exit;
+    }
+
+    /* open file */
+    player->fp = fopen(player->uri, "rb"); /* readonly */
+    if (player->fp == RT_NULL)
+    {
+        LOG_E("open file %s failed", player->uri);
+        result = -RT_ERROR;
+        goto __exit;
+    }
+
+    /* open sound device */
+    result = rt_device_open(player->audio_device, RT_DEVICE_OFLAG_WRONLY);
+    if (result != RT_EOK)
+    {
+        LOG_E("open %s audio_device failed", MP3_SOUND_DEVICE_NAME);
+        goto __exit;
+    }
+
+    /* init decoder */
+    player->mp3_decoder = MP3InitDecoder();
+    if (player->mp3_decoder == 0)
+    {
+        LOG_E("initialize helix mp3 decoder fail!");
+        result = RT_ERROR;
+        goto __exit;
+    }
+
+    /* set sampletate,channels, samplebits */
+    caps.main_type = AUDIO_TYPE_OUTPUT;
+    caps.sub_type = AUDIO_DSP_PARAM;
+    caps.udata.config.samplerate = 44100;
+    caps.udata.config.channels = 2;
+    caps.udata.config.samplebits = 16;
+    rt_device_control(player->audio_device, AUDIO_CTL_CONFIGURE, &caps);
+
+    return RT_EOK;
+
+__exit:
+    if (player->fp)
+    {
+        fclose(player->fp);
+        player->fp = RT_NULL;
+    }
+
+    if (player->audio_device)
+    {
+        rt_device_close(player->audio_device);
+        player->audio_device = RT_NULL;
+    }
+
+    if (player->mp3_decoder)
+    {
+        MP3FreeDecoder(player->mp3_decoder);
+    }
+
+    return result;
+}
+
+/**
+ * @description:close mp3 player 
+ * @param {struct mp3_player} *player
+ * @return None
+ */
+static void mp3_player_close(struct mp3_player *player)
+{
+    if (player->fp)
+    {
+        fclose(player->fp);
+        player->fp = RT_NULL;
+    }
+    if (player->audio_device)
+    {
+        rt_device_close(player->audio_device);
+        player->audio_device = RT_NULL;
+    }
+    if (player->mp3_decoder)
+    {
+        MP3FreeDecoder(player->mp3_decoder);
+    }
+    LOG_D("close mp3 player");
+}
+
+/**
+ * @description: player event handler
+ * @param {struct mp3_player} *player
+ * @param {int} timeout
+ * @return {int} mp3 event
+ */
+static int mp3_player_event_handler(struct mp3_player *player, int timeout)
+{
+    int event;
+    rt_err_t result;
+    struct play_msg msg;
+#if (LOG_LVL >= DBG_LOG)
+    rt_uint8_t last_state;
+#endif
+
+    result = rt_mq_recv(player->mq, &msg, sizeof(struct play_msg), timeout);
+    if (result != RT_EOK)
+    {
+        event = PLAYER_EVENT_NONE;
+        return event;
+    }
+#if (LOG_LVL >= DBG_LOG)
+    last_state = player->state;
+#endif
+    switch (msg.type)
+    {
+    case MSG_START:
+        event = PLAYER_EVENT_PLAY;
+        player->state = PLAYER_STATE_PLAYING;
+        break;
+    case MSG_STOP:
+        event = PLAYER_EVENT_STOP;
+        player->state = PLAYER_STATE_STOPED;
+        break;
+
+    case MSG_PAUSE:
+        event = PLAYER_EVENT_PAUSE;
+        player->state = PLAYER_STATE_PAUSED;
+        break;
+
+    case MSG_RESUME:
+        event = PLAYER_EVENT_RESUME;
+        player->state = PLAYER_STATE_PLAYING;
+        break;
+
+    default:
+        event = PLAYER_EVENT_NONE;
+        break;
+    }
+    rt_completion_done(&player->ack);
+#if (LOG_LVL >= DBG_LOG)
+    LOG_D("EVENT:%s, STATE:%s -> %s", event_str[event], state_str[last_state], state_str[player->state]);
+#endif
+
+    return event;
+}
+
+/**
+ * @description: mp3 player thread
+ * @param {void *}parameter
+ * @return None
+ */
+static void mp3_player_entry(void *parameter)
+{
+    rt_err_t result = RT_EOK;
+    rt_int32_t size;
+    int event;
+
+    uint32_t count = 0;
+
+    /* decoder relate */
+    int i = 0;
+    int err;
+
+    uint32_t cur_ms = 0;
+
+    player.in_buffer = rt_malloc(MP3_INPUT_BUFFER_SIZE);
+    if (player.in_buffer == RT_NULL)
+    {
+        LOG_E("can not malloc input buffer for mp3 player.");
+        return;
+    }
+    player.out_buffer = rt_malloc(MP3_OUTPUT_BUFFER_SIZE);
+    if (player.out_buffer == RT_NULL)
+    {
+        LOG_E("can not malloc output buffer for mp3 player.");
+        return;
+    }
+    memset(player.in_buffer, 0, MP3_INPUT_BUFFER_SIZE);
+    memset(player.out_buffer, 0, MP3_OUTPUT_BUFFER_SIZE);
+
+    player.mq = rt_mq_create("mp3_mq", 10, sizeof(struct play_msg), RT_IPC_FLAG_FIFO);
+    if (player.mq == RT_NULL)
+        goto __exit;
+
+    player.lock = rt_mutex_create("mp3_lock", RT_IPC_FLAG_FIFO);
+    if (player.lock == RT_NULL)
+        goto __exit;
+
+    player.volume = MP3_PLAYER_VOLUME_DEFAULT;
+    /* set volume */
+    mp3_player_volume_set(player.volume);
+
+    while (1)
+    {
+        /* wait play event forever */
+        event = mp3_player_event_handler(&player, RT_WAITING_FOREVER);
+        if (event != PLAYER_EVENT_PLAY)
+            continue;
+
+        /* open mp3 player */
+        result = mp3_player_open(&player);
+        if (result != RT_EOK)
+        {
+            player.state = PLAYER_STATE_STOPED;
+            LOG_I("open mp3 player failed");
+            continue;
+        }
+        LOG_I("play start, uri=%s", player.uri);
+        /* get current mp3 basic info  */
+        if (mp3_get_info(&player) == RT_EOK)
+        {
+            mp3_info_print(player.mp3_info);
+        }
+        
+        fseek(player.fp, player.mp3_info.data_start, SEEK_SET);
+        size = fread(player.in_buffer, 1, MP3_INPUT_BUFFER_SIZE, player.fp);
+        if (size <= 0)
+            goto __exit;
+
+        /* set read ptr to inputbuffer */
+        player.decode_oper.read_ptr = player.in_buffer;
+        player.decode_oper.bytes_left = size;
+
+        while (1)
+        {
+            event = mp3_player_event_handler(&player, RT_WAITING_NO);
+            switch (event)
+            {
+            case PLAYER_EVENT_NONE:
+            {
+                /* find syncword */
+                player.decode_oper.read_offset = MP3FindSyncWord(player.decode_oper.read_ptr, player.decode_oper.bytes_left);
+                if (player.decode_oper.read_offset < 0) /* can not find syncword */
+                {
+                    size = fread(player.in_buffer, 1, MP3_INPUT_BUFFER_SIZE, player.fp);
+                    if (size <= 0)
+                        goto __exit;
+                    player.decode_oper.read_ptr = player.in_buffer;
+                    player.decode_oper.bytes_left = size;
+                    continue;
+                }
+
+                player.decode_oper.read_ptr += player.decode_oper.read_offset;   /* move read pointer to syncword */
+                player.decode_oper.bytes_left -= player.decode_oper.read_offset; /* data size after syncword */
+                if (player.decode_oper.bytes_left < MAINBUF_SIZE * 2)            /* append data */
+                {
+                    i = (uint32_t)(player.decode_oper.bytes_left) & 3;
+                    if (i)
+                        i = 4 - i; /* bytes need to append */
+                    memcpy(player.in_buffer + i, player.decode_oper.read_ptr, player.decode_oper.bytes_left);
+                    player.decode_oper.read_ptr = player.in_buffer + i;
+                    size = fread(player.in_buffer + player.decode_oper.bytes_left + i, 1, MP3_INPUT_BUFFER_SIZE - player.decode_oper.bytes_left - i, player.fp); /* copy at aligned position */
+                    player.decode_oper.bytes_left += size;
+                }
+                /* start decode */
+                err = MP3Decode(player.mp3_decoder, &player.decode_oper.read_ptr, &player.decode_oper.bytes_left, (short *)player.out_buffer, 0);
+                if (err != ERR_MP3_NONE)
+                {
+                    switch (err)
+                    {
+                    case ERR_MP3_INDATA_UNDERFLOW:
+                        LOG_D("ERR_MP3_INDATA_UNDERFLOW");
+                        size = fread(player.in_buffer, 1, MP3_INPUT_BUFFER_SIZE, player.fp); /* append data */
+                        player.decode_oper.read_ptr = player.in_buffer;
+                        player.decode_oper.bytes_left = size;
+                        break;
+                    case ERR_MP3_MAINDATA_UNDERFLOW:
+                        /* do nothing - next call to decode will provide more mainData */
+                        LOG_D("ERR_MP3_MAINDATA_UNDERFLOW");
+                        break;
+                    default:
+                        LOG_D("%s", MP3Decode_ERR_CODE_get(err));
+                        if (player.decode_oper.bytes_left > 0)
+                        {
+                            player.decode_oper.bytes_left--;
+                            player.decode_oper.read_ptr++;
+                        }
+                        break;
+                    }
+                }
+                else /* decode success */
+                {
+                    MP3GetLastFrameInfo(player.mp3_decoder, &player.mp3_frameinfo); /* get decode info */
+                    player.mp3_info.outsamples = player.mp3_frameinfo.outputSamps;
+                    if (player.mp3_info.outsamples > 0)
+                    {
+                        if (player.mp3_frameinfo.nChans == 1) /* Mono */
+                        {
+                            /* Mono need to copy one channel to another */
+                            for (i = player.mp3_info.outsamples - 1; i >= 0; i--)
+                            {
+                                player.out_buffer[i * 2] = player.out_buffer[i];
+                                player.out_buffer[i * 2 + 1] = player.out_buffer[i];
+                            }
+                            player.mp3_info.outsamples *= 2;
+                        }
+                    }
+                    if (player.mp3_frameinfo.samprate != player.mp3_info.samplerate && player.mp3_info.vbr)
+                    {
+                        /* set samplerate by frameinfo*/
+                        player.mp3_info.samplerate = player.mp3_frameinfo.samprate;
+                        struct rt_audio_caps caps;
+                        /* set sampletate,channels, samplebits */
+                        caps.main_type = AUDIO_TYPE_OUTPUT;
+                        caps.sub_type = AUDIO_DSP_PARAM;
+                        caps.udata.config.samplerate = player.mp3_info.samplerate;
+                        caps.udata.config.channels = 2;
+                        caps.udata.config.samplebits = 16;
+                        rt_device_control(player.audio_device, AUDIO_CTL_CONFIGURE, &caps);
+                    }
+                    /* write pcm data to soundcard */
+                    rt_device_write(player.audio_device, 0, (uint8_t *)player.out_buffer, MP3_OUTPUT_BUFFER_SIZE);
+                }
+                if (ftell(player.fp) >= player.mp3_info.file_size)
+                {
+                    /* FILE END*/
+                    player.state = PLAYER_STATE_STOPED;
+                }
+                break;
+            }
+            case PLAYER_EVENT_PAUSE:
+            {
+                /* wait resume or stop event forever */
+                event = mp3_player_event_handler(&player, RT_WAITING_FOREVER);
+            }
+
+            default:
+                break;
+            }
+            if (player.state == PLAYER_STATE_STOPED)
+            {
+                break;
+            }
+        }
+        /* close mp3 player */
+        mp3_player_close(&player);
+        LOG_I("play end");
+    }
+
+__exit:
+    if (player.in_buffer)
+    {
+        rt_free(player.in_buffer);
+        player.in_buffer = RT_NULL;
+    }
+
+    if (player.out_buffer)
+    {
+        rt_free(player.out_buffer);
+        player.out_buffer = RT_NULL;
+    }
+
+    if (player.mq)
+    {
+        rt_mq_delete(player.mq);
+        player.mq = RT_NULL;
+    }
+
+    if (player.lock)
+    {
+        rt_mutex_delete(player.lock);
+        player.lock = RT_NULL;
+    }
+}
+
+int mp3_player_init(void)
+{
+    rt_thread_t tid;
+
+    tid = rt_thread_create("mp3_player",
+                           mp3_player_entry,
+                           RT_NULL,
+                           MP3_THREAD_STATCK_SIZE,
+                           MP3_THREAD_PRIORITY, 10);
+    if (tid)
+        rt_thread_startup(tid);
+
+    return RT_EOK;
+}
+
+INIT_APP_EXPORT(mp3_player_init);
+
+static char *MP3Decode_ERR_CODE_get(int err_code)
+{
+    switch (err_code)
+    {
+    case ERR_MP3_NONE:
+        return "ERR_MP3_NONE";
+    case ERR_MP3_INDATA_UNDERFLOW:
+        return "ERR_MP3_INDATA_UNDERFLOW";
+    case ERR_MP3_MAINDATA_UNDERFLOW:
+        return "ERR_MP3_MAINDATA_UNDERFLOW";
+    case ERR_MP3_FREE_BITRATE_SYNC:
+        return "ERR_MP3_FREE_BITRATE_SYNC";
+    case ERR_MP3_OUT_OF_MEMORY:
+        return "ERR_MP3_OUT_OF_MEMORY";
+    case ERR_MP3_NULL_POINTER:
+        return "ERR_MP3_NULL_POINTER";
+    case ERR_MP3_INVALID_FRAMEHEADER:
+        return "ERR_MP3_INVALID_FRAMEHEADER";
+    case ERR_MP3_INVALID_SIDEINFO:
+        return "ERR_MP3_INVALID_SIDEINFO";
+    case ERR_MP3_INVALID_SCALEFACT:
+        return "ERR_MP3_INVALID_SCALEFACT";
+    case ERR_MP3_INVALID_HUFFCODES:
+        return "ERR_MP3_INVALID_HUFFCODES";
+    case ERR_MP3_INVALID_DEQUANTIZE:
+        return "ERR_MP3_INVALID_DEQUANTIZE";
+    case ERR_MP3_INVALID_IMDCT:
+        return "ERR_MP3_INVALID_IMDCT";
+    case ERR_MP3_INVALID_SUBBAND:
+        return "ERR_MP3_INVALID_SUBBAND";
+    case ERR_UNKNOWN:
+        return "ERR_UNKNOWN";
+    }
+}

+ 208 - 0
src/mp3_player_cmd.c

@@ -0,0 +1,208 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * Date           Author       Notes
+ * 2021-06-02     MrzhangF1ghter    first implementation
+ */
+
+#include "mp3_player.h"
+
+#include <rtthread.h>
+#include <rtdevice.h>
+#include <optparse.h>
+#include <mp3_player.h>
+
+#include <stdlib.h>
+
+enum MP3_PLAYER_ACTTION
+{
+    MP3_PLAYER_ACTION_HELP = 0,
+    MP3_PLAYER_ACTION_START = 1,
+    MP3_PLAYER_ACTION_STOP = 2,
+    MP3_PLAYER_ACTION_PAUSE = 3,
+    MP3_PLAYER_ACTION_RESUME = 4,
+    MP3_PLAYER_ACTION_VOLUME = 5,
+    MP3_PLAYER_ACTION_DUMP = 6,
+    MP3_PLAYER_ACTION_JUMP = 7
+};
+
+struct mp3_play_args
+{
+    int action;
+    char *uri;
+    int volume;
+    int seconds;
+};
+
+static const char *state_str[] =
+    {
+        "STOPPED",
+        "PLAYING",
+        "PAUSED",
+};
+
+static struct optparse_long opts[] =
+    {
+        {"help", 'h', OPTPARSE_NONE},
+        {"start", 's', OPTPARSE_REQUIRED},
+        {"stop", 't', OPTPARSE_NONE},
+        {"pause", 'p', OPTPARSE_NONE},
+        {"resume", 'r', OPTPARSE_NONE},
+        {"volume", 'v', OPTPARSE_REQUIRED},
+        {"dump", 'd', OPTPARSE_NONE},
+        {"jump", 'j', OPTPARSE_REQUIRED},
+        {NULL, 0, OPTPARSE_NONE}};
+
+static void usage(void)
+{
+    rt_kprintf("usage: mp3_play [option] [target] ...\n\n");
+    rt_kprintf("usage options:\n");
+    rt_kprintf("  -h,     --help                     Print defined help message.\n");
+    rt_kprintf("  -s URI, --start=URI                Play mp3 music with URI(local files).\n");
+    rt_kprintf("  -t,     --stop                     Stop playing music.\n");
+    rt_kprintf("  -p,     --pause                    Pause the music.\n");
+    rt_kprintf("  -r,     --resume                   Resume the music.\n");
+    rt_kprintf("  -v lvl, --volume=lvl               Change the volume(0~99).\n");
+    rt_kprintf("  -d,     --dump                     Dump play relevant information.\n");
+    rt_kprintf("  -j,     --jump                     Jump to seconds that given.\n");
+}
+
+static void dump_status(void)
+{
+    rt_kprintf("\nmp3_player status:\n");
+    rt_kprintf("uri     - %s\n", mp3_player_uri_get());
+    rt_kprintf("status  - %s\n", state_str[mp3_player_state_get()]);
+    rt_kprintf("volume  - %d\n", mp3_player_volume_get());
+    mp3_disp_time();
+    mp3_info_show();
+}
+
+int mp3_play_args_prase(int argc, char *argv[], struct mp3_play_args *play_args)
+{
+    int ch;
+    int option_index;
+    struct optparse options;
+    rt_uint8_t action_cnt = 0;
+    rt_err_t result = RT_EOK;
+
+    if (argc == 1)
+    {
+        play_args->action = MP3_PLAYER_ACTION_HELP;
+        return RT_EOK;
+    }
+
+    /* Parse cmd */
+    optparse_init(&options, argv);
+    while ((ch = optparse_long(&options, opts, &option_index)) != -1)
+    {
+        switch (ch)
+        {
+        case 'h':
+            play_args->action = MP3_PLAYER_ACTION_HELP;
+            break;
+
+        case 's':
+            play_args->action = MP3_PLAYER_ACTION_START;
+            play_args->uri = options.optarg;
+            action_cnt++;
+            break;
+
+        case 't':
+            play_args->action = MP3_PLAYER_ACTION_STOP;
+            action_cnt++;
+            break;
+
+        case 'p':
+            play_args->action = MP3_PLAYER_ACTION_PAUSE;
+            action_cnt++;
+            break;
+
+        case 'r':
+            play_args->action = MP3_PLAYER_ACTION_RESUME;
+            action_cnt++;
+            break;
+
+        case 'v':
+            play_args->action = MP3_PLAYER_ACTION_VOLUME;
+            play_args->volume = (options.optarg == RT_NULL) ? (-1) : atoi(options.optarg);
+            action_cnt++;
+            break;
+
+        case 'd':
+            play_args->action = MP3_PLAYER_ACTION_DUMP;
+            break;
+
+        case 'j':
+            play_args->action = MP3_PLAYER_ACTION_JUMP;
+            play_args->seconds = (options.optarg == RT_NULL) ? (-1) : atoi(options.optarg);
+            break;
+
+        default:
+            result = -RT_EINVAL;
+            break;
+        }
+    }
+
+    if (action_cnt > 1)
+    {
+        rt_kprintf("START STOP PAUSE RESUME parameter can't be used at the same time.\n");
+        result = -RT_EINVAL;
+    }
+
+    return result;
+}
+
+int mp3_player(int argc, char *argv[])
+{
+    int result = RT_EOK;
+    struct mp3_play_args play_args = {0};
+
+    result = mp3_play_args_prase(argc, argv, &play_args);
+    if (result != RT_EOK)
+    {
+        usage();
+        return result;
+    }
+
+    switch (play_args.action)
+    {
+    case MP3_PLAYER_ACTION_HELP:
+        usage();
+        break;
+
+    case MP3_PLAYER_ACTION_START:
+        mp3_player_play(play_args.uri);
+        break;
+
+    case MP3_PLAYER_ACTION_STOP:
+        mp3_player_stop();
+        break;
+
+    case MP3_PLAYER_ACTION_PAUSE:
+        mp3_player_pause();
+        break;
+
+    case MP3_PLAYER_ACTION_RESUME:
+        mp3_player_resume();
+        break;
+
+    case MP3_PLAYER_ACTION_VOLUME:
+        mp3_player_volume_set(play_args.volume);
+        break;
+
+    case MP3_PLAYER_ACTION_DUMP:
+        dump_status();
+        break;
+
+    case MP3_PLAYER_ACTION_JUMP:
+        mp3_seek(play_args.seconds);
+        break;
+    default:
+        result = -RT_ERROR;
+        break;
+    }
+
+    return result;
+}
+
+MSH_CMD_EXPORT_ALIAS(mp3_player, mp3play, play mp3 music);

+ 565 - 0
src/mp3_tag.c

@@ -0,0 +1,565 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * Date           Author       Notes
+ * 2021-06-02     MrzhangF1ghter    first implementation
+ */
+
+#include "mp3_tag.h"
+#include <rtthread.h>
+#include <string.h>
+
+#define LOG_TAG "mp3 tag"
+#define LOG_LVL DBG_INFO
+#include <ulog.h>
+
+#ifndef MIN
+#define MIN(a, b) ((a) < (b) ? (a) : (b))
+#endif
+
+char *genre_type[] = {
+    "Blues",
+    "Classic Rock",
+    "Country",
+    "Dance",
+    "Disco",
+    "Funk",
+    "Grunge",
+    "Hip-Hop",
+    "Jazz",
+    "Metal",
+    "New Age",
+    "Oldies",
+    "Other",
+    "Pop",
+    "R&B",
+    "Rap",
+    "Reggae",
+    "Rock",
+    "Techno",
+    "Industrial",
+    "Alternative",
+    "Ska",
+    "Death Metal",
+    "Pranks",
+    "Soundtrack",
+    "Euro-Techno",
+    "Ambient",
+    "Trip-Hop",
+    "Vocal",
+    "Jazz+Funk",
+    "Fusion",
+    "Trance",
+    "Classical",
+    "Instrumental",
+    "Acid",
+    "House",
+    "Game",
+    "Sound Clip",
+    "Gospel",
+    "Noise",
+    "Alternative Rock",
+    "Bass",
+    "Soul",
+    "Punk",
+    "Space",
+    "Meditative",
+    "Instrumental Pop",
+    "Instrumental Rock",
+    "Ethnic",
+    "Gothic",
+    "Darkwave",
+    "Techno-Industrial",
+    "Electronic",
+    "Pop-Folk",
+    "Eurodance",
+    "Dream",
+    "Southern Rock",
+    "Comedy",
+    "Cult",
+    "Gangsta",
+    "Top 40",
+    "Christian Rap",
+    "Pop/Funk",
+    "Jungle",
+    "Native US",
+    "Cabaret",
+    "New Wave",
+    "Psychadelic",
+    "Rave",
+    "Showtunes",
+    "Trailer",
+    "Lo-Fi",
+    "Tribal",
+    "Acid Punk",
+    "Acid Jazz",
+    "Polka",
+    "Retro",
+    "Musical",
+    "Rock & Roll",
+    "Hard Rock",
+    "Folk",
+    "Folk-Rock",
+    "National Folk",
+    "Swing",
+    "Fast Fusion",
+    "Bebob",
+    "Latin",
+    "Revival",
+    "Celtic",
+    "Bluegrass",
+    "Avantgarde",
+    "Gothic Rock",
+    "Progressive Rock",
+    "Psychedelic Rock",
+    "Symphonic Rock",
+    "Slow Rock",
+    "Big Band",
+    "Chorus",
+    "Easy Listening",
+    "Acoustic",
+    "Humour",
+    "Speech",
+    "Chanson",
+    "Opera",
+    "Chamber Music",
+    "Symphony",
+    "Booty Bass",
+    "Primus",
+    "Porn Groove",
+    "Satire",
+    "Slow Jam",
+    "Club",
+    "Tango",
+    "Samba",
+    "Folklore",
+    "Ballad",
+    "Power Ballad",
+    "Rhytmic Soul",
+    "Freestyle",
+    "Duet",
+    "Punk Rock",
+    "Drum Solo",
+    "Acapella",
+    "Euro-House",
+    "Dance Hall",
+    "Goa",
+    "Drum & Bass",
+    "Club-House",
+    "Hardcore",
+    "Terror",
+    "Indie",
+    "BritPop",
+    "Negerpunk",
+    "Polsk Punk",
+    "Beat",
+    "Christian Gangsta",
+    "Heavy Metal",
+    "Black Metal",
+    "Crossover",
+    "Contemporary C",
+    "Christian Rock",
+    "Merengue",
+    "Salsa",
+    "Thrash Metal",
+    "Anime",
+    "JPop",
+    "SynthPop",
+};
+
+/**
+ * @description: Get genre string by genre id
+ * @param {uint16_t} genre_id [0,147]
+ * @return {char *} genre string
+ */
+char *mp3_get_genre_string_by_id(uint16_t genre_id)
+{
+    if (genre_id < 0 || genre_id > 147)
+        return RT_NULL;
+    return genre_type[genre_id];
+}
+
+/**
+ * @description: print id3v1 tag
+ * @param {ID3V1_Tag_t} id3v1_tag
+ * @return None
+ */
+void mp3_id3v1_tag_print(ID3V1_Tag_t id3v1_tag)
+{
+    rt_kprintf("Title:%s\r\n", id3v1_tag.title);
+    rt_kprintf("Artist:%s\r\n", id3v1_tag.artist);
+    rt_kprintf("Year:%s\r\n", id3v1_tag.year);
+    rt_kprintf("Comment:%s\r\n", id3v1_tag.comment);
+    rt_kprintf("Genre:%s\r\n", mp3_get_genre_string_by_id(id3v1_tag.genre));
+}
+
+/**
+ * @description: print mp3 info
+ * @param {mp3_info_t} mp3_info
+ * @return the error code,0 on success
+ */
+rt_err_t mp3_info_print(mp3_info_t mp3_info)
+{
+    rt_kprintf("------------MP3 INFO------------\r\n");
+    rt_kprintf("Title:%s\r\n", mp3_info.mp3_basic_info.title);
+    rt_kprintf("Artist:%s\r\n", mp3_info.mp3_basic_info.artist);
+    rt_kprintf("Year:%s\r\n", mp3_info.mp3_basic_info.year);
+    rt_kprintf("Comment:%s\r\n", mp3_info.mp3_basic_info.comment);
+    rt_kprintf("Genre:%s\r\n", mp3_get_genre_string_by_id(mp3_info.mp3_basic_info.genre));
+    rt_kprintf("Length:%02d:%02d\r\n", mp3_info.total_seconds / 60, mp3_info.total_seconds % 60);
+    rt_kprintf("Bitrate:%d kbit/s\r\n", mp3_info.bitrate / 1000);
+    rt_kprintf("Frequency:%d Hz\r\n", mp3_info.samplerate);
+    rt_kprintf("--------------------------------\r\n");
+}
+
+/**
+ * @description: id3v1 tag decode
+ * @param {uint8_t} *buf
+ * @param {ID3V1_Tag_t} *id3v1_tag
+ * @return the error code,0 on success
+ */
+static rt_err_t mp3_id3v1_tag_decode(FILE *fp, uint8_t *buf, mp3_basic_info_t *basic_info)
+{
+    ID3V1_Tag_t *tag;
+    fpos_t file_pos = 0;
+    int read_size;
+    rt_err_t ret;
+
+    if (fp == RT_NULL || buf == RT_NULL)
+    {
+        return RT_ERROR;
+    }
+
+    file_pos = ftell(fp); /* save current file positon */
+
+    fseek(fp, -128, SEEK_END);       /* move read pointer to id3v1 position */
+    if (fread(buf, 1, 128, fp) <= 0) /* read 128 bytes */
+    {
+        ret = RT_ERROR;
+        goto __exit;
+    }
+
+    tag = (ID3V1_Tag_t *)buf;
+    /* check header */
+    if (strncmp("TAG", (char *)tag->id, 3) == 0)
+    {
+        /* is id3v1 tag,copy to user */
+        if (tag->title)
+            memcpy(basic_info->title, tag->title, 30);
+        if (tag->artist)
+            memcpy(basic_info->artist, tag->artist, 30);
+        if (tag->year)
+            memcpy(basic_info->year, tag->year, 4);
+        if (tag->comment)
+            memcpy(basic_info->comment, tag->comment, 30);
+        basic_info->genre = tag->genre;
+        ret = RT_EOK;
+    }
+    else
+    {
+        ret = RT_ERROR;
+    }
+
+__exit:
+    fseek(fp, file_pos, SEEK_SET); /* resume file positon */
+    return ret;
+}
+
+/**
+ * @description: read id3v2 text
+ * @param {FILE} *fp
+ * @param {uint32_t} data_len
+ * @param {char} *buff
+ * @param {uint32_t} buff_size
+ * @return the error code,0 on success
+ * @verbatim  Taken from http://www.mikrocontroller.net/topic/252319
+ */
+static uint8_t mp3_read_id3v2_text(FILE *fp, uint32_t data_len, char *buff, uint32_t buff_size)
+{
+    uint32_t read_size = 0;
+    uint8_t byEncoding = 0;
+    if (fread(&byEncoding, 1, 1, fp) == 1)
+    {
+        data_len--;
+        if (data_len <= (buff_size - 1))
+        {
+            if ((fread(buff, 1, data_len, fp) == data_len))
+            {
+                if (byEncoding == 0)
+                {
+                    // ISO-8859-1 multibyte
+                    // just add a terminating zero
+                    buff[data_len] = 0;
+                }
+                else if (byEncoding == 1)
+                {
+                    // UTF16LE unicode
+                    uint32_t r = 0;
+                    uint32_t w = 0;
+                    if ((data_len > 2) && (buff[0] == 0xFF) && (buff[1] == 0xFE))
+                    {
+                        // ignore BOM, assume LE
+                        r = 2;
+                    }
+                    for (; r < data_len; r += 2, w += 1)
+                    {
+                        // should be acceptable for 7 bit ascii
+                        buff[w] = buff[r];
+                    }
+                    buff[w] = 0;
+                }
+            }
+            else
+            {
+                return 1;
+            }
+        }
+        else
+        {
+            // we won't read a partial text
+            if (fseek(fp, ftell(fp) + data_len, SEEK_SET) != 0)
+            {
+                return 1;
+            }
+        }
+    }
+    else
+    {
+        return 1;
+    }
+    return 0;
+}
+
+/**
+ * @description: id3v2 decode
+ * @param {FILE} *fp
+ * @param {mp3_basic_info_t} *mp3_info
+ * @return {*}
+ * @verbatim  Taken from http://www.mikrocontroller.net/topic/252319
+ */
+static uint32_t mp3_id3v2_tag_decode(FILE *fp, mp3_basic_info_t *mp3_info)
+{
+    ID3V2_TagHead_t id3_head;
+    ID3V23_FrameHead_t frame_head;
+
+    uint32_t ret = 0;
+
+    uint32_t read_size;
+    uint32_t tag_size = 0;
+    fpos_t file_pos = 0;
+
+    uint32_t offset = 0;
+    uint8_t exhd[4];
+    uint32_t frame_to_read = 2;
+    uint32_t i;
+    uint32_t frame_size = 0;
+
+    if (fp == RT_NULL || mp3_info == RT_NULL)
+        return 0;
+
+    file_pos = ftell(fp); /* save current file positon */
+
+    if (fread(&id3_head, 1, 10, fp) != 10)
+    {
+        ret = 0;
+        goto __exit;
+    }
+    else
+    {
+        /* check header */
+        if (strncmp("ID3", (const char *)id3_head.id, 3) == 0)
+        {
+            offset += 10; /* move to tag frame */
+            tag_size = ((id3_head.size[0] & 0x7f) << 21) | ((id3_head.size[1] & 0x7f) << 14) | ((id3_head.size[2] & 0x7f) << 7) | (id3_head.size[3] & 0x7f);
+            ret = tag_size;
+            LOG_D("tag_size:%.2f kB", tag_size / 1024.0);
+            // try to get some information from the tag
+            // skip the extended header, if present
+            if (id3_head.flags & 0x40)
+            {
+                fread(&exhd, 1, 4, fp);
+                uint32_t ex_hdr_skip = ((exhd[0] & 0x7f) << 21) | ((exhd[1] & 0x7f) << 14) | ((exhd[2] & 0x7f) << 7) | (exhd[3] & 0x7f);
+                ex_hdr_skip -= 4;
+                if (fseek(fp, ftell(fp) + ex_hdr_skip, SEEK_SET) != 0)
+                {
+                    ret = 0;
+                    goto __exit;
+                }
+            }
+            while (frame_to_read > 0)
+            {
+                if (fread(&frame_head, 1, 10, fp) != 10)
+                {
+                    ret = 0;
+                    goto __exit;
+                }
+                if (frame_head.id[0] == 0 || (strncmp(frame_head.id, "3DI", 3) == 0))
+                {
+                    break;
+                }
+                for (; i < 4; i++)
+                {
+                    if (id3_head.mversion == 3)
+                    {
+                        /* ID3v2.3 */
+                        frame_size <<= 8;
+                        frame_size += frame_head.size[i];
+                    }
+                    if (id3_head.mversion == 4)
+                    {
+                        /* ID3v2.4 */
+                        frame_size <<= 7;
+                        frame_size += frame_head.size[i] & 0x7F;
+                    }
+                }
+
+                if (strcmp(frame_head.id, "TPE1") == 0)
+                {
+                    /* artist */
+                    if (mp3_read_id3v2_text(fp, frame_size, mp3_info->artist, 30) != 0)
+                    {
+                        break;
+                    }
+                    frame_to_read--;
+                }
+                else if (strcmp(frame_head.id, "TIT2") == 0)
+                {
+                    /* title */
+                    if (mp3_read_id3v2_text(fp, frame_size, mp3_info->title, 30) != 0)
+                    {
+                        break;
+                    }
+                    frame_to_read--;
+                }
+                else
+                {
+                    if (fseek(fp, ftell(fp) + frame_size, SEEK_SET) != 0)
+                    {
+                        return 0;
+                    }
+                }
+            }
+        }
+    }
+__exit:
+    fseek(fp, file_pos, SEEK_SET); /* resume file positon */
+    return ret;
+}
+
+/**
+ * @description: get mp3 tag info
+ * @param {struct mp3_player} *player
+ * @return the error code,0 on success
+ */
+rt_err_t mp3_get_info(struct mp3_player *player)
+{
+    rt_err_t ret = RT_EOK;
+
+    MP3FrameInfo frame_info;
+    MP3_FrameXing_t *fxing;
+    MP3_FrameVBRI_t *fvbri;
+
+    int offset = 0;
+    uint32_t p;
+    short samples_per_frame;
+    uint32_t total_frame;
+    ID3V1_Tag_t ID3V1_Tag = {0};
+    uint32_t read_size = 0;
+    fpos_t file_pos = 0;
+
+    if (player->fp == NULL)
+    {
+        LOG_E("%s is not opened", player->uri);
+        return RT_ERROR;
+    }
+    file_pos = ftell(player->fp); /* save current file positon */
+    LOG_D("current file pos:%d", file_pos);
+    memset(&player->mp3_info, 0, sizeof(mp3_info_t));
+
+    /* get file size */
+    fseek(player->fp, 0, SEEK_END);
+    player->mp3_info.file_size = ftell(player->fp);
+    fseek(player->fp, 0, SEEK_SET);
+    LOG_D("%s:%d KB,%.2f MB", player->uri, player->mp3_info.file_size / 1024, player->mp3_info.file_size / 1024 / 1024.0);
+
+    player->mp3_info.data_start = mp3_id3v2_tag_decode(player->fp, &player->mp3_info.mp3_basic_info); /* decode ID3V2 tag*/
+
+    mp3_id3v1_tag_decode(player->fp, player->in_buffer, &player->mp3_info.mp3_basic_info); /* decode ID3V1 tag */
+
+    LOG_D("mp3 data start at :%f KB", player->mp3_info.data_start / 1024.0);
+
+    fseek(player->fp, player->mp3_info.data_start, SEEK_SET);
+    while ((read_size = fread(player->in_buffer, 1, MP3_INPUT_BUFFER_SIZE, player->fp)))
+    {
+        if ((offset = MP3FindSyncWord(player->in_buffer, read_size)) >= 0)
+            break;
+    }
+
+    LOG_D("sync word offset at:%d", offset);
+    if (offset >= 0 && MP3GetNextFrameInfo(player->mp3_decoder, &frame_info, &player->in_buffer[offset]) == 0)
+    {
+
+        p = offset + 4 + 32;
+        fvbri = (MP3_FrameVBRI_t *)(player->in_buffer + p);
+        if (strncmp("VBRI", (char *)fvbri->id, 4) == 0) /* VBRI frame*/
+        {
+            if (frame_info.version == MPEG1)
+                samples_per_frame = 1152;
+            else
+                samples_per_frame = 576;
+            total_frame = ((uint32_t)fvbri->frames[0] << 24) | ((uint32_t)fvbri->frames[1] << 16) | ((uint16_t)fvbri->frames[2] << 8) | fvbri->frames[3]; /* get total frame */
+            player->mp3_info.total_seconds = total_frame * samples_per_frame / frame_info.samprate;                                                       /* get total length */
+        }
+        else /* maybe is Xing frame*/
+        {
+            if (frame_info.version == MPEG1)
+            {
+                p = frame_info.nChans == 2 ? 32 : 17;
+                samples_per_frame = 1152;
+            }
+            else
+            {
+                p = frame_info.nChans == 2 ? 17 : 9;
+                samples_per_frame = 576;
+            }
+            p += offset + 4;
+            fxing = (MP3_FrameXing_t *)(player->in_buffer + p);
+            if (strncmp("Xing", (char *)fxing->id, 4) == 0 || strncmp("Info", (char *)fxing->id, 4) == 0)
+            {
+                if (fxing->flags[3] & 0X01) /* TOC is valid */
+                {
+                    total_frame = ((uint32_t)fxing->frames[0] << 24) | ((uint32_t)fxing->frames[1] << 16) | ((uint16_t)fxing->frames[2] << 8) | fxing->frames[3]; /* get total flame */
+
+                    player->mp3_info.total_seconds = total_frame * samples_per_frame / frame_info.samprate; /* get total length */
+                    player->mp3_info.vbr = 1;
+                }
+                else
+                {
+                    player->mp3_info.total_seconds = (player->mp3_info.file_size - (128 + player->mp3_info.data_start)) / (frame_info.bitrate / 8);
+                    player->mp3_info.vbr = 0;
+                }
+            }
+            else /* CBR Format */
+            {
+                player->mp3_info.total_seconds = (player->mp3_info.file_size - (128 + player->mp3_info.data_start)) / (frame_info.bitrate / 8);
+                player->mp3_info.vbr = 0;
+            }
+        }
+        player->mp3_info.bitrate = frame_info.bitrate;
+        player->mp3_info.samplerate = frame_info.samprate;
+        if (frame_info.nChans == 2)
+            player->mp3_info.outsamples = frame_info.outputSamps;
+        else
+            player->mp3_info.outsamples = frame_info.outputSamps * 2;
+    }
+    else
+    {
+        LOG_E("can not find sync frame");
+        ret = RT_ERROR;
+    }
+__exit:
+
+    if (player->fp)
+        fsetpos(player->fp, &file_pos); /* resume file positon */
+    return ret;
+}