Initial public release of the 2024A software.
This commit is contained in:
parent
7b9ad3edfd
commit
303e9e1dad
361 changed files with 60083 additions and 2 deletions
|
@ -0,0 +1 @@
|
|||
c8ac1998e9af863bc41b57e592f88d1a5791a0f891485122336ddabbf7a65033
|
27
managed_components/chmorgan__esp-audio-player/CMakeLists.txt
Normal file
27
managed_components/chmorgan__esp-audio-player/CMakeLists.txt
Normal file
|
@ -0,0 +1,27 @@
|
|||
|
||||
set(srcs
|
||||
"audio_player.cpp"
|
||||
)
|
||||
|
||||
set(includes
|
||||
"include"
|
||||
)
|
||||
|
||||
set(requires "")
|
||||
|
||||
if(CONFIG_AUDIO_PLAYER_ENABLE_MP3)
|
||||
list(APPEND srcs "audio_mp3.cpp")
|
||||
endif()
|
||||
|
||||
# TODO: move inside of the 'if(CONFIG_AUDIO_PLAYER_ENABLE_MP3)' when everything builds correctly
|
||||
list(APPEND requires "esp-libhelix-mp3")
|
||||
|
||||
if(CONFIG_AUDIO_PLAYER_ENABLE_WAV)
|
||||
list(APPEND srcs "audio_wav.cpp")
|
||||
endif()
|
||||
|
||||
idf_component_register(SRCS "${srcs}"
|
||||
REQUIRES "${requires}"
|
||||
INCLUDE_DIRS "${includes}"
|
||||
REQUIRES driver
|
||||
)
|
20
managed_components/chmorgan__esp-audio-player/Kconfig
Normal file
20
managed_components/chmorgan__esp-audio-player/Kconfig
Normal file
|
@ -0,0 +1,20 @@
|
|||
menu "Audio playback"
|
||||
|
||||
config AUDIO_PLAYER_ENABLE_MP3
|
||||
bool "Enable mp3 decoding."
|
||||
default y
|
||||
help
|
||||
The audio player can play mp3 files using libhelix-mp3.
|
||||
config AUDIO_PLAYER_ENABLE_WAV
|
||||
bool "Enable wav file playback"
|
||||
default y
|
||||
help
|
||||
Audio player can decode wave files.
|
||||
|
||||
config AUDIO_PLAYER_LOG_LEVEL
|
||||
int "Audio Player log level (0 none - 3 highest)"
|
||||
default 0
|
||||
range 0 3
|
||||
help
|
||||
Specify the verbosity of Audio Player log output.
|
||||
endmenu
|
201
managed_components/chmorgan__esp-audio-player/LICENSE
Normal file
201
managed_components/chmorgan__esp-audio-player/LICENSE
Normal file
|
@ -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.
|
80
managed_components/chmorgan__esp-audio-player/README.md
Normal file
80
managed_components/chmorgan__esp-audio-player/README.md
Normal file
|
@ -0,0 +1,80 @@
|
|||
# Audio player component for esp32
|
||||
|
||||
|
||||
[](https://github.com/chmorgan/esp-audio-player/actions/workflows/cppcheck.yml)
|
||||
|
||||
## Capabilities
|
||||
|
||||
* MP3 decoding (via libhelix-mp3)
|
||||
* Wav/wave file decoding
|
||||
|
||||
## Who is this for?
|
||||
|
||||
Decode only audio playback on esp32 series of chips, where the features and footprint of esp-adf are not
|
||||
necessary.
|
||||
|
||||
## What about esp-adf?
|
||||
|
||||
This component is not intended to compete with esp-adf, a much more fully developed
|
||||
audio framework.
|
||||
|
||||
It does however have a number of advantages at the moment including:
|
||||
|
||||
* Fully open source (esp-adf has a number of binary modules at the moment)
|
||||
* Minimal size (it's less capable, but also simpler, than esp-adf)
|
||||
|
||||
## Getting started
|
||||
|
||||
### Examples
|
||||
|
||||
* [esp-box mp3_demo](https://github.com/espressif/esp-box/tree/master/examples/mp3_demo) uses esp-audio-player.
|
||||
* The [test example](https://github.com/chmorgan/esp-audio-player/tree/main/test) is a simpler example than mp3_demo that also uses the esp-box hardware.
|
||||
|
||||
### How to use this?
|
||||
[esp-audio-player is a component](https://components.espressif.com/components/chmorgan/esp-audio-player) on the [Espressif component registry](https://components.espressif.com).
|
||||
|
||||
In your project run:
|
||||
```
|
||||
idf.py add-dependency chmorgan/esp-audio-player
|
||||
```
|
||||
|
||||
to add the component dependency to the project's manifest file.
|
||||
|
||||
|
||||
## Dependencies
|
||||
|
||||
For MP3 support you'll need the [esp-libhelix-mp3](https://github.com/chmorgan/esp-libhelix-mp3) component.
|
||||
|
||||
## Tests
|
||||
|
||||
Unity tests are implemented in the [test/](../test) folder.
|
||||
|
||||
## States
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> Idle : new(), cb(IDLE)
|
||||
Idle --> Playing : play(), cb(PLAYING)
|
||||
Playing --> Paused : pause(), cb(PAUSE)
|
||||
Paused --> Playing : resume(), cb(PLAYING)
|
||||
Playing --> Playing : play(), cb(COMPLETED_PLAYING_NEXT)
|
||||
Paused --> Idle : stop(), cb(IDLE)
|
||||
Playing --> Idle : song complete, cb(IDLE)
|
||||
[*] --> Shutdown : delete(), cb(SHUTDOWN)
|
||||
Shutdown --> Idle : new(), cb(IDLE)
|
||||
```
|
||||
|
||||
Note: Diagram shortens callbacks from AUDIO_PLAYER_EVENT_xxx to xxx, and functions from audio_player_xxx() to xxx(), for clarity.
|
||||
|
||||
|
||||
## Release process - Pushing component to the IDF Component Registry
|
||||
|
||||
The github workflow, .github/workflows/esp_upload_component.yml, pushes data to the espressif
|
||||
[IDF component registry](https://components.espressif.com).
|
||||
|
||||
To push a new version:
|
||||
|
||||
* Apply a git tag via 'git tag vA.B.C'
|
||||
* Push tags via 'git push --tags'
|
||||
|
||||
The github workflow *should* run and automatically push to the IDF component registry.
|
|
@ -0,0 +1,57 @@
|
|||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
|
||||
typedef enum {
|
||||
DECODE_STATUS_CONTINUE, /*< data remaining, call decode again */
|
||||
DECODE_STATUS_NO_DATA_CONTINUE, /*< data remaining but none in this call */
|
||||
DECODE_STATUS_DONE, /*< no data remaining to decode */
|
||||
DECODE_STATUS_ERROR /*< unrecoverable error */
|
||||
} DECODE_STATUS;
|
||||
|
||||
typedef struct {
|
||||
int sample_rate;
|
||||
uint32_t bits_per_sample;
|
||||
uint32_t channels;
|
||||
} format;
|
||||
|
||||
/**
|
||||
* Decoded audio data ready for playback
|
||||
*
|
||||
* Fields in this structure are expected to be updated
|
||||
* upon each cycle of the decoder, as the decoder stores
|
||||
* audio data to be played back.
|
||||
*/
|
||||
typedef struct {
|
||||
/**
|
||||
* NOTE: output_samples is flushed each decode cycle
|
||||
*
|
||||
* NOTE: the decode format determines how to convert samples to frames, ie.
|
||||
* whether these samples are stero or mono samples and what the bits per sample are
|
||||
*/
|
||||
uint8_t *samples;
|
||||
|
||||
/** capacity of samples */
|
||||
size_t samples_capacity;
|
||||
|
||||
/**
|
||||
* 2x samples_capacity to allow for in-place conversion of
|
||||
* mono to stereo
|
||||
*/
|
||||
size_t samples_capacity_max;
|
||||
|
||||
/**
|
||||
* Number of frames in samples,
|
||||
* Note that each frame consists of 'fmt.channels' number of samples,
|
||||
* for example for stereo output the number of samples is 2x the
|
||||
* frame count.
|
||||
*/
|
||||
size_t frame_count;
|
||||
|
||||
format fmt;
|
||||
} decode_data;
|
||||
|
||||
|
||||
#define BYTES_IN_WORD 2
|
||||
#define BITS_PER_BYTE 8
|
26
managed_components/chmorgan__esp-audio-player/audio_log.h
Normal file
26
managed_components/chmorgan__esp-audio-player/audio_log.h
Normal file
|
@ -0,0 +1,26 @@
|
|||
#pragma once
|
||||
|
||||
#include "esp_log.h"
|
||||
|
||||
#if CONFIG_AUDIO_PLAYER_LOG_LEVEL >= 1
|
||||
#define LOGI_1(FMT, ...) \
|
||||
ESP_LOGI(TAG, "[1] " FMT, ##__VA_ARGS__)
|
||||
#else
|
||||
#define LOGI_1(FMT, ...) { (void)TAG; }
|
||||
#endif
|
||||
|
||||
#if CONFIG_AUDIO_PLAYER_LOG_LEVEL >= 2
|
||||
#define LOGI_2(FMT, ...) \
|
||||
ESP_LOGI(TAG, "[2] " FMT, ##__VA_ARGS__)
|
||||
#else
|
||||
#define LOGI_2(FMT, ...) { (void)TAG;}
|
||||
#endif
|
||||
|
||||
#if CONFIG_AUDIO_PLAYER_LOG_LEVEL >= 3
|
||||
#define LOGI_3(FMT, ...) \
|
||||
ESP_LOGI(TAG, "[3] " FMT, ##__VA_ARGS__)
|
||||
#define COMPILE_3(x) x
|
||||
#else
|
||||
#define LOGI_3(FMT, ...) { (void)TAG; }
|
||||
#define COMPILE_3(x) {}
|
||||
#endif
|
169
managed_components/chmorgan__esp-audio-player/audio_mp3.cpp
Normal file
169
managed_components/chmorgan__esp-audio-player/audio_mp3.cpp
Normal file
|
@ -0,0 +1,169 @@
|
|||
#include <string.h>
|
||||
#include "audio_log.h"
|
||||
#include "audio_mp3.h"
|
||||
|
||||
static const char *TAG = "mp3";
|
||||
|
||||
bool is_mp3(FILE *fp) {
|
||||
bool is_mp3_file = false;
|
||||
|
||||
fseek(fp, 0, SEEK_SET);
|
||||
|
||||
// see https://en.wikipedia.org/wiki/List_of_file_signatures
|
||||
uint8_t magic[3];
|
||||
if(sizeof(magic) == fread(magic, 1, sizeof(magic), fp)) {
|
||||
if((magic[0] == 0xFF) &&
|
||||
(magic[1] == 0xFB))
|
||||
{
|
||||
is_mp3_file = true;
|
||||
} else if((magic[0] == 0xFF) &&
|
||||
(magic[1] == 0xF3))
|
||||
{
|
||||
is_mp3_file = true;
|
||||
} else if((magic[0] == 0xFF) &&
|
||||
(magic[1] == 0xF2))
|
||||
{
|
||||
is_mp3_file = true;
|
||||
} else if((magic[0] == 0x49) &&
|
||||
(magic[1] == 0x44) &&
|
||||
(magic[2] == 0x33)) /* 'ID3' */
|
||||
{
|
||||
fseek(fp, 0, SEEK_SET);
|
||||
|
||||
/* Get ID3 head */
|
||||
mp3_id3_header_v2_t tag;
|
||||
if (sizeof(mp3_id3_header_v2_t) == fread(&tag, 1, sizeof(mp3_id3_header_v2_t), fp)) {
|
||||
if (memcmp("ID3", (const void *) &tag, sizeof(tag.header)) == 0) {
|
||||
is_mp3_file = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// seek back to the start of the file to avoid
|
||||
// missing frames upon decode
|
||||
fseek(fp, 0, SEEK_SET);
|
||||
|
||||
return is_mp3_file;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if data remains, false on error or end of file
|
||||
*/
|
||||
DECODE_STATUS decode_mp3(HMP3Decoder mp3_decoder, FILE *fp, decode_data *pData, mp3_instance *pInstance) {
|
||||
MP3FrameInfo frame_info;
|
||||
|
||||
size_t unread_bytes = pInstance->bytes_in_data_buf - (pInstance->read_ptr - pInstance->data_buf);
|
||||
|
||||
/* somewhat arbitrary trigger to refill buffer - should always be enough for a full frame */
|
||||
if (unread_bytes < 1.25 * MAINBUF_SIZE && !pInstance->eof_reached) {
|
||||
uint8_t *write_ptr = pInstance->data_buf + unread_bytes;
|
||||
size_t free_space = pInstance->data_buf_size - unread_bytes;
|
||||
|
||||
/* move last, small chunk from end of buffer to start,
|
||||
then fill with new data */
|
||||
memmove(pInstance->data_buf, pInstance->read_ptr, unread_bytes);
|
||||
|
||||
size_t nRead = fread(write_ptr, 1, free_space, fp);
|
||||
|
||||
pInstance->bytes_in_data_buf = unread_bytes + nRead;
|
||||
pInstance->read_ptr = pInstance->data_buf;
|
||||
|
||||
if ((nRead == 0) || feof(fp)) {
|
||||
pInstance->eof_reached = true;
|
||||
}
|
||||
|
||||
LOGI_2("pos %ld, nRead %d, eof %d", ftell(fp), nRead, pInstance->eof_reached);
|
||||
|
||||
unread_bytes = pInstance->bytes_in_data_buf;
|
||||
}
|
||||
|
||||
LOGI_3("data_buf 0x%p, read 0x%p", pInstance->data_buf, pInstance->read_ptr);
|
||||
|
||||
if(unread_bytes == 0) {
|
||||
LOGI_1("unread_bytes == 0, status done");
|
||||
return DECODE_STATUS_DONE;
|
||||
}
|
||||
|
||||
/* Find MP3 sync word from read buffer */
|
||||
int offset = MP3FindSyncWord(pInstance->read_ptr, unread_bytes);
|
||||
|
||||
LOGI_2("unread %d, total %d, offset 0x%x(%d)",
|
||||
unread_bytes, pInstance->bytes_in_data_buf, offset, offset);
|
||||
|
||||
if (offset >= 0) {
|
||||
COMPILE_3(int starting_unread_bytes = unread_bytes);
|
||||
uint8_t *read_ptr = pInstance->read_ptr + offset; /*!< Data start point */
|
||||
unread_bytes -= offset;
|
||||
LOGI_3("read 0x%p, unread %d", read_ptr, unread_bytes);
|
||||
int mp3_dec_err = MP3Decode(mp3_decoder, &read_ptr, (int*)&unread_bytes, reinterpret_cast<int16_t *>(pData->samples),
|
||||
0);
|
||||
|
||||
pInstance->read_ptr = read_ptr;
|
||||
|
||||
if(mp3_dec_err == ERR_MP3_NONE) {
|
||||
/* Get MP3 frame info */
|
||||
MP3GetLastFrameInfo(mp3_decoder, &frame_info);
|
||||
|
||||
pData->fmt.sample_rate = frame_info.samprate;
|
||||
pData->fmt.bits_per_sample = frame_info.bitsPerSample;
|
||||
pData->fmt.channels = frame_info.nChans;
|
||||
|
||||
pData->frame_count = (frame_info.outputSamps / frame_info.nChans);
|
||||
|
||||
LOGI_3("mp3: channels %d, sr %d, bps %d, frame_count %d, processed %d",
|
||||
pData->fmt.channels,
|
||||
pData->fmt.sample_rate,
|
||||
pData->fmt.bits_per_sample,
|
||||
frame_info.outputSamps,
|
||||
starting_unread_bytes - unread_bytes);
|
||||
} else {
|
||||
if (pInstance->eof_reached) {
|
||||
ESP_LOGE(TAG, "status error %d, but EOF", mp3_dec_err);
|
||||
return DECODE_STATUS_DONE;
|
||||
} else if (mp3_dec_err == ERR_MP3_MAINDATA_UNDERFLOW) {
|
||||
// underflow indicates MP3Decode should be called again
|
||||
LOGI_1("underflow read ptr is 0x%p", read_ptr);
|
||||
return DECODE_STATUS_NO_DATA_CONTINUE;
|
||||
} else {
|
||||
// NOTE: some mp3 files result in misdetection of mp3 frame headers
|
||||
// and during decode these misdetected frames cannot be
|
||||
// decoded
|
||||
//
|
||||
// Rather than give up on the file by returning
|
||||
// DECODE_STATUS_ERROR, we ask the caller
|
||||
// to continue to call us, by returning DECODE_STATUS_NO_DATA_CONTINUE.
|
||||
//
|
||||
// The invalid frame data is skipped over as a search for the next frame
|
||||
// on the subsequent call to this function will start searching
|
||||
// AFTER the misdetected frmame header, dropping the invalid data.
|
||||
//
|
||||
// We may want to consider a more sophisticated approach here at a later time.
|
||||
ESP_LOGE(TAG, "status error %d", mp3_dec_err);
|
||||
return DECODE_STATUS_NO_DATA_CONTINUE;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// if we are dropping data there were no frames decoded
|
||||
pData->frame_count = 0;
|
||||
|
||||
// drop an even count of words
|
||||
size_t words_to_drop = unread_bytes / BYTES_IN_WORD;
|
||||
size_t bytes_to_drop = words_to_drop * BYTES_IN_WORD;
|
||||
|
||||
// if the unread bytes is less than BYTES_IN_WORD, we should drop any unread bytes
|
||||
// to avoid the situation where the file could have a few extra bytes at the end
|
||||
// of the file that isn't at least BYTES_IN_WORD and decoding would get stuck
|
||||
if(unread_bytes < BYTES_IN_WORD) {
|
||||
bytes_to_drop = unread_bytes;
|
||||
}
|
||||
|
||||
// shift the read_ptr to drop the bytes in the buffer
|
||||
pInstance->read_ptr += bytes_to_drop;
|
||||
|
||||
/* Sync word not found in frame. Drop data that was read until a word boundary */
|
||||
ESP_LOGE(TAG, "MP3 sync word not found, dropping %d bytes", bytes_to_drop);
|
||||
}
|
||||
|
||||
return DECODE_STATUS_CONTINUE;
|
||||
}
|
48
managed_components/chmorgan__esp-audio-player/audio_mp3.h
Normal file
48
managed_components/chmorgan__esp-audio-player/audio_mp3.h
Normal file
|
@ -0,0 +1,48 @@
|
|||
#pragma once
|
||||
|
||||
#include <stdio.h>
|
||||
#include "audio_decode_types.h"
|
||||
#include "mp3dec.h"
|
||||
|
||||
typedef struct {
|
||||
char header[3]; /*!< Always "TAG" */
|
||||
char title[30]; /*!< Audio title */
|
||||
char artist[30]; /*!< Audio artist */
|
||||
char album[30]; /*!< Album name */
|
||||
char year[4]; /*!< Char array of year */
|
||||
char comment[30]; /*!< Extra comment */
|
||||
char genre; /*!< See "https://en.wikipedia.org/wiki/ID3" */
|
||||
} __attribute__((packed)) mp3_id3_header_v1_t;
|
||||
|
||||
typedef struct {
|
||||
char header[3]; /*!< Always "ID3" */
|
||||
char ver; /*!< Version, equals to3 if ID3V2.3 */
|
||||
char revision; /*!< Revision, should be 0 */
|
||||
char flag; /*!< Flag byte, use Bit[7..5] only */
|
||||
char size[4]; /*!< TAG size */
|
||||
} __attribute__((packed)) mp3_id3_header_v2_t;
|
||||
|
||||
typedef struct {
|
||||
// Constants below
|
||||
uint8_t *data_buf;
|
||||
|
||||
/** number of bytes in data_buf */
|
||||
size_t data_buf_size;
|
||||
|
||||
// Values that change at runtime are below
|
||||
|
||||
/**
|
||||
* Total bytes in data_buf,
|
||||
* not the number of bytes remaining after the read_ptr
|
||||
*/
|
||||
size_t bytes_in_data_buf;
|
||||
|
||||
/** Pointer to read location in data_buf */
|
||||
uint8_t *read_ptr;
|
||||
|
||||
// set to true if the end of file has been reached
|
||||
bool eof_reached;
|
||||
} mp3_instance;
|
||||
|
||||
bool is_mp3(FILE *fp);
|
||||
DECODE_STATUS decode_mp3(HMP3Decoder mp3_decoder, FILE *fp, decode_data *pData, mp3_instance *pInstance);
|
608
managed_components/chmorgan__esp-audio-player/audio_player.cpp
Normal file
608
managed_components/chmorgan__esp-audio-player/audio_player.cpp
Normal file
|
@ -0,0 +1,608 @@
|
|||
/**
|
||||
* @file
|
||||
* @version 0.1
|
||||
*
|
||||
* @copyright Copyright 2021 Espressif Systems (Shanghai) Co. Ltd.
|
||||
* @copyright Copyright 2022 Chris Morgan <chmorgan@gmail.com>
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/unistd.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#include "esp_check.h"
|
||||
#include "esp_log.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "freertos/queue.h"
|
||||
|
||||
#include "sdkconfig.h"
|
||||
|
||||
#include "audio_player.h"
|
||||
|
||||
#include "audio_wav.h"
|
||||
#include "audio_mp3.h"
|
||||
|
||||
static const char *TAG = "audio";
|
||||
|
||||
typedef enum {
|
||||
AUDIO_PLAYER_REQUEST_NONE = 0,
|
||||
AUDIO_PLAYER_REQUEST_PAUSE, /**< pause playback */
|
||||
AUDIO_PLAYER_REQUEST_RESUME, /**< resumed paused playback */
|
||||
AUDIO_PLAYER_REQUEST_PLAY, /**< initiate playing a new file */
|
||||
AUDIO_PLAYER_REQUEST_STOP, /**< stop playback */
|
||||
AUDIO_PLAYER_REQUEST_SHUTDOWN_THREAD, /**< shutdown audio playback thread */
|
||||
AUDIO_PLAYER_REQUEST_MAX
|
||||
} audio_player_event_type_t;
|
||||
|
||||
typedef struct {
|
||||
audio_player_event_type_t type;
|
||||
|
||||
// valid if type == AUDIO_PLAYER_EVENT_TYPE_PLAY
|
||||
FILE* fp;
|
||||
} audio_player_event_t;
|
||||
|
||||
typedef enum {
|
||||
FILE_TYPE_UNKNOWN,
|
||||
#if defined(CONFIG_AUDIO_PLAYER_ENABLE_MP3)
|
||||
FILE_TYPE_MP3,
|
||||
#endif
|
||||
#if defined(CONFIG_AUDIO_PLAYER_ENABLE_WAV)
|
||||
FILE_TYPE_WAV
|
||||
#endif
|
||||
} FILE_TYPE;
|
||||
|
||||
typedef struct audio_instance {
|
||||
/**
|
||||
* Set to true before task is created, false immediately before the
|
||||
* task is deleted.
|
||||
*/
|
||||
bool running;
|
||||
|
||||
decode_data output;
|
||||
|
||||
QueueHandle_t event_queue;
|
||||
|
||||
/* **************** AUDIO CALLBACK **************** */
|
||||
audio_player_cb_t s_audio_cb;
|
||||
void *audio_cb_usrt_ctx;
|
||||
audio_player_state_t state;
|
||||
|
||||
audio_player_config_t config;
|
||||
|
||||
#if defined(CONFIG_AUDIO_PLAYER_ENABLE_WAV)
|
||||
wav_instance wav_data;
|
||||
#endif
|
||||
|
||||
#if defined(CONFIG_AUDIO_PLAYER_ENABLE_MP3)
|
||||
HMP3Decoder mp3_decoder;
|
||||
mp3_instance mp3_data;
|
||||
#endif
|
||||
} audio_instance_t;
|
||||
|
||||
static audio_instance_t instance;
|
||||
|
||||
audio_player_state_t audio_player_get_state() {
|
||||
return instance.state;
|
||||
}
|
||||
|
||||
esp_err_t audio_player_callback_register(audio_player_cb_t call_back, void *user_ctx)
|
||||
{
|
||||
#if CONFIG_IDF_TARGET_ARCH_XTENSA
|
||||
ESP_RETURN_ON_FALSE(esp_ptr_executable(reinterpret_cast<void*>(call_back)), ESP_ERR_INVALID_ARG,
|
||||
TAG, "Not a valid call back");
|
||||
#else
|
||||
ESP_RETURN_ON_FALSE(reinterpret_cast<void*>(call_back), ESP_ERR_INVALID_ARG,
|
||||
TAG, "Not a valid call back");
|
||||
#endif
|
||||
instance.s_audio_cb = call_back;
|
||||
instance.audio_cb_usrt_ctx = user_ctx;
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
// This function is used in some optional logging functions so we don't want to
|
||||
// have a cppcheck warning here
|
||||
// cppcheck-suppress unusedFunction
|
||||
const char* event_to_string(audio_player_callback_event_t event) {
|
||||
switch(event) {
|
||||
case AUDIO_PLAYER_CALLBACK_EVENT_IDLE:
|
||||
return "AUDIO_PLAYER_CALLBACK_EVENT_IDLE";
|
||||
case AUDIO_PLAYER_CALLBACK_EVENT_COMPLETED_PLAYING_NEXT:
|
||||
return "AUDIO_PLAYER_CALLBACK_EVENT_COMPLETED_PLAYING_NEXT";
|
||||
case AUDIO_PLAYER_CALLBACK_EVENT_PLAYING:
|
||||
return "AUDIO_PLAYER_CALLBACK_EVENT_PLAYING";
|
||||
case AUDIO_PLAYER_CALLBACK_EVENT_PAUSE:
|
||||
return "AUDIO_PLAYER_CALLBACK_EVENT_PAUSE";
|
||||
case AUDIO_PLAYER_CALLBACK_EVENT_SHUTDOWN:
|
||||
return "AUDIO_PLAYER_CALLBACK_EVENT_SHUTDOWN";
|
||||
case AUDIO_PLAYER_CALLBACK_EVENT_UNKNOWN_FILE_TYPE:
|
||||
return "AUDIO_PLAYER_CALLBACK_EVENT_UNKNOWN_FILE_TYPE";
|
||||
case AUDIO_PLAYER_CALLBACK_EVENT_UNKNOWN:
|
||||
return "AUDIO_PLAYER_CALLBACK_EVENT_UNKNOWN";
|
||||
}
|
||||
|
||||
return "unknown event";
|
||||
}
|
||||
|
||||
static audio_player_callback_event_t state_to_event(audio_player_state_t state) {
|
||||
audio_player_callback_event_t event = AUDIO_PLAYER_CALLBACK_EVENT_UNKNOWN;
|
||||
|
||||
switch(state) {
|
||||
case AUDIO_PLAYER_STATE_IDLE:
|
||||
event = AUDIO_PLAYER_CALLBACK_EVENT_IDLE;
|
||||
break;
|
||||
case AUDIO_PLAYER_STATE_PAUSE:
|
||||
event = AUDIO_PLAYER_CALLBACK_EVENT_PAUSE;
|
||||
break;
|
||||
case AUDIO_PLAYER_STATE_PLAYING:
|
||||
event = AUDIO_PLAYER_CALLBACK_EVENT_PLAYING;
|
||||
break;
|
||||
case AUDIO_PLAYER_STATE_SHUTDOWN:
|
||||
event = AUDIO_PLAYER_CALLBACK_EVENT_SHUTDOWN;
|
||||
break;
|
||||
};
|
||||
|
||||
return event;
|
||||
}
|
||||
|
||||
static void dispatch_callback(audio_instance_t *i, audio_player_callback_event_t event) {
|
||||
LOGI_1("event '%s'", event_to_string(event));
|
||||
|
||||
#if CONFIG_IDF_TARGET_ARCH_XTENSA
|
||||
if (esp_ptr_executable(reinterpret_cast<void*>(i->s_audio_cb))) {
|
||||
#else
|
||||
if (reinterpret_cast<void*>(i->s_audio_cb)) {
|
||||
#endif
|
||||
audio_player_cb_ctx_t ctx = {
|
||||
.audio_event = event,
|
||||
.user_ctx = i->audio_cb_usrt_ctx,
|
||||
};
|
||||
i->s_audio_cb(&ctx);
|
||||
}
|
||||
}
|
||||
|
||||
static void set_state(audio_instance_t *i, audio_player_state_t new_state) {
|
||||
if(i->state != new_state) {
|
||||
i->state = new_state;
|
||||
audio_player_callback_event_t event = state_to_event(new_state);
|
||||
dispatch_callback(i, event);
|
||||
}
|
||||
}
|
||||
|
||||
static void audio_instance_init(audio_instance_t &i) {
|
||||
i.event_queue = NULL;
|
||||
i.s_audio_cb = NULL;
|
||||
i.audio_cb_usrt_ctx = NULL;
|
||||
i.state = AUDIO_PLAYER_STATE_IDLE;
|
||||
}
|
||||
|
||||
static esp_err_t mono_to_stereo(uint32_t output_bits_per_sample, decode_data &adata)
|
||||
{
|
||||
size_t data = adata.frame_count * (output_bits_per_sample / BITS_PER_BYTE);
|
||||
data *= 2;
|
||||
|
||||
// do we have enough space in the output buffer to convert mono to stereo?
|
||||
if(data > adata.samples_capacity_max) {
|
||||
ESP_LOGE(TAG, "insufficient space in output.samples to convert mono to stereo, need %d, have %d", data, adata.samples_capacity_max);
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
|
||||
size_t new_sample_count = adata.frame_count * 2;
|
||||
|
||||
// convert from back to front to allow conversion in-place
|
||||
//
|
||||
// NOTE: -1 is because we want to shift to the sample at position X
|
||||
// but if we do (ptr + X) we end up at the sample at index X instead
|
||||
// which is one further
|
||||
int16_t *out = reinterpret_cast<int16_t*>(adata.samples) + (new_sample_count - 1);
|
||||
int16_t *in = reinterpret_cast<int16_t*>(adata.samples) + (adata.frame_count - 1);
|
||||
size_t samples = adata.frame_count;
|
||||
while(samples) {
|
||||
// write right channel
|
||||
*out = *in;
|
||||
out--;
|
||||
|
||||
// write left channel
|
||||
*out = *in;
|
||||
out--;
|
||||
|
||||
// move input buffer back and decrement samples
|
||||
in--;
|
||||
samples--;
|
||||
}
|
||||
|
||||
// adjust channels to 2
|
||||
adata.fmt.channels = 2;
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static esp_err_t aplay_file(audio_instance_t *i, FILE *fp)
|
||||
{
|
||||
LOGI_1("start to decode");
|
||||
|
||||
format i2s_format;
|
||||
memset(&i2s_format, 0, sizeof(i2s_format));
|
||||
|
||||
esp_err_t ret = ESP_OK;
|
||||
audio_player_event_t audio_event = { .type = AUDIO_PLAYER_REQUEST_NONE, .fp = NULL };
|
||||
|
||||
FILE_TYPE file_type = FILE_TYPE_UNKNOWN;
|
||||
|
||||
#if defined(CONFIG_AUDIO_PLAYER_ENABLE_MP3)
|
||||
if(is_mp3(fp)) {
|
||||
file_type = FILE_TYPE_MP3;
|
||||
LOGI_1("file is mp3");
|
||||
|
||||
// initialize mp3_instance
|
||||
i->mp3_data.bytes_in_data_buf = 0;
|
||||
i->mp3_data.read_ptr = i->mp3_data.data_buf;
|
||||
i->mp3_data.eof_reached = false;
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(CONFIG_AUDIO_PLAYER_ENABLE_WAV)
|
||||
// This can be a pointless condition depending on the build options, no reason to warn about it
|
||||
// cppcheck-suppress knownConditionTrueFalse
|
||||
if(file_type == FILE_TYPE_UNKNOWN)
|
||||
{
|
||||
if(is_wav(fp, &i->wav_data)) {
|
||||
file_type = FILE_TYPE_WAV;
|
||||
LOGI_1("file is wav");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// cppcheck-suppress knownConditionTrueFalse
|
||||
if(file_type == FILE_TYPE_UNKNOWN) {
|
||||
ESP_LOGE(TAG, "unknown file type, cleaning up");
|
||||
dispatch_callback(i, AUDIO_PLAYER_CALLBACK_EVENT_UNKNOWN_FILE_TYPE);
|
||||
goto clean_up;
|
||||
}
|
||||
|
||||
do {
|
||||
/* Process audio event sent from other task */
|
||||
if (pdPASS == xQueuePeek(i->event_queue, &audio_event, 0)) {
|
||||
LOGI_2("event in queue");
|
||||
if (AUDIO_PLAYER_REQUEST_PAUSE == audio_event.type) {
|
||||
// receive the pause event to take it off of the queue
|
||||
xQueueReceive(i->event_queue, &audio_event, 0);
|
||||
|
||||
set_state(i, AUDIO_PLAYER_STATE_PAUSE);
|
||||
|
||||
// wait until an event is received that will cause playback to resume,
|
||||
// stop, or change file
|
||||
while(1) {
|
||||
xQueuePeek(i->event_queue, &audio_event, portMAX_DELAY);
|
||||
|
||||
if((AUDIO_PLAYER_REQUEST_PLAY != audio_event.type) &&
|
||||
(AUDIO_PLAYER_REQUEST_STOP != audio_event.type) &&
|
||||
(AUDIO_PLAYER_REQUEST_RESUME != audio_event.type))
|
||||
{
|
||||
// receive to discard the event
|
||||
xQueueReceive(i->event_queue, &audio_event, 0);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(AUDIO_PLAYER_REQUEST_RESUME == audio_event.type) {
|
||||
// receive to discard the event
|
||||
xQueueReceive(i->event_queue, &audio_event, 0);
|
||||
continue;
|
||||
}
|
||||
|
||||
// else fall out of this condition and let the below logic
|
||||
// handle the other event types
|
||||
}
|
||||
|
||||
if ((AUDIO_PLAYER_REQUEST_STOP == audio_event.type) ||
|
||||
(AUDIO_PLAYER_REQUEST_PLAY == audio_event.type)) {
|
||||
ret = ESP_OK;
|
||||
goto clean_up;
|
||||
} else {
|
||||
// receive to discard the event, this event has no
|
||||
// impact on the state of playback
|
||||
xQueueReceive(i->event_queue, &audio_event, 0);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
set_state(i, AUDIO_PLAYER_STATE_PLAYING);
|
||||
|
||||
DECODE_STATUS decode_status = DECODE_STATUS_ERROR;
|
||||
|
||||
switch(file_type) {
|
||||
#if defined(CONFIG_AUDIO_PLAYER_ENABLE_MP3)
|
||||
case FILE_TYPE_MP3:
|
||||
decode_status = decode_mp3(i->mp3_decoder, fp, &i->output, &i->mp3_data);
|
||||
break;
|
||||
#endif
|
||||
#if defined(CONFIG_AUDIO_PLAYER_ENABLE_WAV)
|
||||
case FILE_TYPE_WAV:
|
||||
decode_status = decode_wav(fp, &i->output, &i->wav_data);
|
||||
break;
|
||||
#endif
|
||||
case FILE_TYPE_UNKNOWN:
|
||||
ESP_LOGE(TAG, "unexpected unknown file type when decoding");
|
||||
break;
|
||||
}
|
||||
|
||||
// break out and exit if we aren't supposed to continue decoding
|
||||
if(decode_status == DECODE_STATUS_CONTINUE)
|
||||
{
|
||||
// if mono, convert to stereo as es8311 requires stereo input
|
||||
// even though it is mono output
|
||||
if(i->output.fmt.channels == 1) {
|
||||
LOGI_3("c == 1, mono -> stereo");
|
||||
ret = mono_to_stereo(i->output.fmt.bits_per_sample, i->output);
|
||||
if(ret != ESP_OK) {
|
||||
goto clean_up;
|
||||
}
|
||||
}
|
||||
|
||||
/* Configure I2S clock if the output format changed */
|
||||
if ((i2s_format.sample_rate != i->output.fmt.sample_rate) ||
|
||||
(i2s_format.channels != i->output.fmt.channels) ||
|
||||
(i2s_format.bits_per_sample != i->output.fmt.bits_per_sample)) {
|
||||
i2s_format = i->output.fmt;
|
||||
LOGI_1("format change: sr=%d, bit=%d, ch=%d",
|
||||
i2s_format.sample_rate,
|
||||
i2s_format.bits_per_sample,
|
||||
i2s_format.channels);
|
||||
i2s_slot_mode_t channel_setting = (i2s_format.channels == 1) ? I2S_SLOT_MODE_MONO : I2S_SLOT_MODE_STEREO;
|
||||
ret = i->config.clk_set_fn(i2s_format.sample_rate,
|
||||
i2s_format.bits_per_sample,
|
||||
channel_setting);
|
||||
ESP_GOTO_ON_ERROR(ret, clean_up, TAG, "i2s_set_clk");
|
||||
}
|
||||
|
||||
/**
|
||||
* Block until all data has been accepted into the i2s driver, however
|
||||
* the i2s driver has been configured with a buffer to allow for the next round of
|
||||
* audio decoding to occur while the previous set of samples is finishing playback, in order
|
||||
* to ensure playback without interruption.
|
||||
*/
|
||||
size_t i2s_bytes_written = 0;
|
||||
size_t bytes_to_write = i->output.frame_count * i->output.fmt.channels * (i2s_format.bits_per_sample / 8);
|
||||
LOGI_2("c %d, bps %d, bytes %d, frame_count %d",
|
||||
i->output.fmt.channels,
|
||||
i2s_format.bits_per_sample,
|
||||
bytes_to_write,
|
||||
i->output.frame_count);
|
||||
|
||||
i->config.write_fn(i->output.samples, bytes_to_write, &i2s_bytes_written, portMAX_DELAY);
|
||||
if(bytes_to_write != i2s_bytes_written) {
|
||||
ESP_LOGE(TAG, "to write %d != written %d", bytes_to_write, i2s_bytes_written);
|
||||
}
|
||||
} else if(decode_status == DECODE_STATUS_NO_DATA_CONTINUE)
|
||||
{
|
||||
LOGI_2("no data");
|
||||
} else { // DECODE_STATUS_DONE || DECODE_STATUS_ERROR
|
||||
LOGI_1("breaking out of playback");
|
||||
break;
|
||||
}
|
||||
} while (true);
|
||||
|
||||
clean_up:
|
||||
return ret;
|
||||
}
|
||||
|
||||
static void audio_task(void *pvParam)
|
||||
{
|
||||
audio_instance_t *i = static_cast<audio_instance_t*>(pvParam);
|
||||
audio_player_event_t audio_event;
|
||||
|
||||
while (true) {
|
||||
// pull items off of the queue until we run into a PLAY request
|
||||
while(true) {
|
||||
// zero delay in the case where we are playing as we want to
|
||||
// send an event indicating either
|
||||
// PLAYING -> IDLE (IDLE) or PLAYING -> PLAYING (COMPLETED PLAYING NEXT)
|
||||
// and thus don't want to block until the next request comes in
|
||||
// in the case when there are no further requests pending
|
||||
int delay = (i->state == AUDIO_PLAYER_STATE_PLAYING) ? 0 : portMAX_DELAY;
|
||||
|
||||
int retval = xQueuePeek(i->event_queue, &audio_event, delay);
|
||||
if (pdPASS == retval) { // item on the queue, process it
|
||||
xQueueReceive(i->event_queue, &audio_event, 0);
|
||||
|
||||
// if the item is a play request, process it
|
||||
if(AUDIO_PLAYER_REQUEST_PLAY == audio_event.type) {
|
||||
if(i->state == AUDIO_PLAYER_STATE_PLAYING) {
|
||||
dispatch_callback(i, AUDIO_PLAYER_CALLBACK_EVENT_COMPLETED_PLAYING_NEXT);
|
||||
} else {
|
||||
set_state(i, AUDIO_PLAYER_STATE_PLAYING);
|
||||
}
|
||||
|
||||
break;
|
||||
} else if(AUDIO_PLAYER_REQUEST_SHUTDOWN_THREAD == audio_event.type) {
|
||||
set_state(i, AUDIO_PLAYER_STATE_SHUTDOWN);
|
||||
i->running = false;
|
||||
|
||||
// should never return
|
||||
vTaskDelete(NULL);
|
||||
break;
|
||||
} else {
|
||||
// ignore other events when not playing
|
||||
}
|
||||
} else { // no items on the queue
|
||||
// if we are playing transition to idle and indicate the transition via callback
|
||||
if(i->state == AUDIO_PLAYER_STATE_PLAYING) {
|
||||
set_state(i, AUDIO_PLAYER_STATE_IDLE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
i->config.mute_fn(AUDIO_PLAYER_UNMUTE);
|
||||
esp_err_t ret_val = aplay_file(i, audio_event.fp);
|
||||
if(ret_val != ESP_OK)
|
||||
{
|
||||
ESP_LOGE(TAG, "aplay_file() %d", ret_val);
|
||||
}
|
||||
i->config.mute_fn(AUDIO_PLAYER_MUTE);
|
||||
|
||||
if(audio_event.fp) fclose(audio_event.fp);
|
||||
}
|
||||
}
|
||||
|
||||
/* **************** AUDIO PLAY CONTROL **************** */
|
||||
static esp_err_t audio_send_event(audio_instance_t *i, audio_player_event_t event) {
|
||||
ESP_RETURN_ON_FALSE(NULL != i->event_queue, ESP_ERR_INVALID_STATE,
|
||||
TAG, "Audio task not started yet");
|
||||
|
||||
BaseType_t ret_val = xQueueSend(i->event_queue, &event, 0);
|
||||
|
||||
ESP_RETURN_ON_FALSE(pdPASS == ret_val, ESP_ERR_INVALID_STATE,
|
||||
TAG, "The last event has not been processed yet");
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t audio_player_play(FILE *fp)
|
||||
{
|
||||
LOGI_1("%s", __FUNCTION__);
|
||||
audio_player_event_t event = { .type = AUDIO_PLAYER_REQUEST_PLAY, .fp = fp };
|
||||
return audio_send_event(&instance, event);
|
||||
}
|
||||
|
||||
esp_err_t audio_player_pause(void)
|
||||
{
|
||||
LOGI_1("%s", __FUNCTION__);
|
||||
audio_player_event_t event = { .type = AUDIO_PLAYER_REQUEST_PAUSE, .fp = NULL };
|
||||
return audio_send_event(&instance, event);
|
||||
}
|
||||
|
||||
esp_err_t audio_player_resume(void)
|
||||
{
|
||||
LOGI_1("%s", __FUNCTION__);
|
||||
audio_player_event_t event = { .type = AUDIO_PLAYER_REQUEST_RESUME, .fp = NULL };
|
||||
return audio_send_event(&instance, event);
|
||||
}
|
||||
|
||||
esp_err_t audio_player_stop(void)
|
||||
{
|
||||
LOGI_1("%s", __FUNCTION__);
|
||||
audio_player_event_t event = { .type = AUDIO_PLAYER_REQUEST_STOP, .fp = NULL };
|
||||
return audio_send_event(&instance, event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Can only shut down the playback thread if the thread is not presently playing audio.
|
||||
* Call audio_player_stop()
|
||||
*/
|
||||
static esp_err_t _internal_audio_player_shutdown_thread(void)
|
||||
{
|
||||
LOGI_1("%s", __FUNCTION__);
|
||||
audio_player_event_t event = { .type = AUDIO_PLAYER_REQUEST_SHUTDOWN_THREAD, .fp = NULL };
|
||||
return audio_send_event(&instance, event);
|
||||
}
|
||||
|
||||
static void cleanup_memory(audio_instance_t &i)
|
||||
{
|
||||
#if defined(CONFIG_AUDIO_PLAYER_ENABLE_MP3)
|
||||
if(i.mp3_decoder) MP3FreeDecoder(i.mp3_decoder);
|
||||
if(i.mp3_data.data_buf) free(i.mp3_data.data_buf);
|
||||
#endif
|
||||
if(i.output.samples) free(i.output.samples);
|
||||
|
||||
vQueueDelete(i.event_queue);
|
||||
}
|
||||
|
||||
esp_err_t audio_player_new(audio_player_config_t config)
|
||||
{
|
||||
BaseType_t task_val;
|
||||
|
||||
audio_instance_init(instance);
|
||||
|
||||
instance.config = config;
|
||||
|
||||
/* Audio control event queue */
|
||||
instance.event_queue = xQueueCreate(4, sizeof(audio_player_event_t));
|
||||
ESP_RETURN_ON_FALSE(NULL != instance.event_queue, -1, TAG, "xQueueCreate");
|
||||
|
||||
/** See https://github.com/ultraembedded/libhelix-mp3/blob/0a0e0673f82bc6804e5a3ddb15fb6efdcde747cd/testwrap/main.c#L74 */
|
||||
instance.output.samples_capacity = MAX_NCHAN * MAX_NGRAN * MAX_NSAMP;
|
||||
instance.output.samples_capacity_max = instance.output.samples_capacity * 2;
|
||||
instance.output.samples = static_cast<uint8_t*>(malloc(instance.output.samples_capacity_max));
|
||||
LOGI_1("samples_capacity %d bytes", instance.output.samples_capacity_max);
|
||||
int ret = ESP_OK;
|
||||
ESP_GOTO_ON_FALSE(NULL != instance.output.samples, ESP_ERR_NO_MEM, cleanup,
|
||||
TAG, "Failed allocate output buffer");
|
||||
|
||||
#if defined(CONFIG_AUDIO_PLAYER_ENABLE_MP3)
|
||||
instance.mp3_data.data_buf_size = MAINBUF_SIZE * 3;
|
||||
instance.mp3_data.data_buf = static_cast<uint8_t*>(malloc(instance.mp3_data.data_buf_size));
|
||||
ESP_GOTO_ON_FALSE(NULL != instance.mp3_data.data_buf, ESP_ERR_NO_MEM, cleanup,
|
||||
TAG, "Failed allocate mp3 data buffer");
|
||||
|
||||
instance.mp3_decoder = MP3InitDecoder();
|
||||
ESP_GOTO_ON_FALSE(NULL != instance.mp3_decoder, ESP_ERR_NO_MEM, cleanup,
|
||||
TAG, "Failed create MP3 decoder");
|
||||
#endif
|
||||
|
||||
instance.running = true;
|
||||
task_val = xTaskCreatePinnedToCore(
|
||||
(TaskFunction_t) audio_task,
|
||||
"Audio Task",
|
||||
4 * 1024,
|
||||
&instance,
|
||||
(UBaseType_t) instance.config.priority,
|
||||
(TaskHandle_t * const) NULL,
|
||||
(BaseType_t) instance.config.coreID);
|
||||
|
||||
ESP_GOTO_ON_FALSE(pdPASS == task_val, ESP_ERR_NO_MEM, cleanup,
|
||||
TAG, "Failed create audio task");
|
||||
|
||||
// start muted
|
||||
instance.config.mute_fn(AUDIO_PLAYER_MUTE);
|
||||
|
||||
return ret;
|
||||
|
||||
// At the moment when we run cppcheck there is a lack of esp-idf header files this
|
||||
// means cppcheck doesn't know that ESP_GOTO_ON_FALSE() etc are making use of this label
|
||||
// cppcheck-suppress unusedLabelConfiguration
|
||||
cleanup:
|
||||
cleanup_memory(instance);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
esp_err_t audio_player_delete() {
|
||||
const int MAX_RETRIES = 5;
|
||||
int retries = MAX_RETRIES;
|
||||
while(instance.running && retries) {
|
||||
// stop any playback and shutdown the thread
|
||||
audio_player_stop();
|
||||
_internal_audio_player_shutdown_thread();
|
||||
|
||||
vTaskDelay(pdMS_TO_TICKS(100));
|
||||
retries--;
|
||||
}
|
||||
|
||||
cleanup_memory(instance);
|
||||
|
||||
// if we ran out of retries, return fail code
|
||||
if(retries == 0) {
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
}
|
81
managed_components/chmorgan__esp-audio-player/audio_wav.cpp
Normal file
81
managed_components/chmorgan__esp-audio-player/audio_wav.cpp
Normal file
|
@ -0,0 +1,81 @@
|
|||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include "audio_wav.h"
|
||||
|
||||
static const char *TAG = "wav";
|
||||
|
||||
/**
|
||||
* @param fp
|
||||
* @param pInstance - Values can be considered valid if true is returned
|
||||
* @return true if file is a wav file
|
||||
*/
|
||||
bool is_wav(FILE *fp, wav_instance *pInstance) {
|
||||
fseek(fp, 0, SEEK_SET);
|
||||
|
||||
size_t bytes_read = fread(&pInstance->header, 1, sizeof(wav_header_t), fp);
|
||||
if(bytes_read != sizeof(wav_header_t)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
wav_header_t *wav_head = &pInstance->header;
|
||||
if((NULL == strstr(reinterpret_cast<char *>(wav_head->ChunkID), "RIFF")) ||
|
||||
(NULL == strstr(reinterpret_cast<char*>(wav_head->Format), "WAVE"))
|
||||
)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// decode chunks until we find the 'data' one
|
||||
wav_subchunk_header_t subchunk;
|
||||
while(true) {
|
||||
bytes_read = fread(&subchunk, 1, sizeof(wav_subchunk_header_t), fp);
|
||||
if(bytes_read != sizeof(wav_subchunk_header_t)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if(memcmp(subchunk.SubchunkID, "data", 4) == 0)
|
||||
{
|
||||
break;
|
||||
} else {
|
||||
// advance beyond this subchunk, it could be a 'LIST' chunk with file info or some other unhandled subchunk
|
||||
fseek(fp, subchunk.SubchunkSize, SEEK_CUR);
|
||||
}
|
||||
}
|
||||
|
||||
LOGI_2("sample_rate=%d, channels=%d, bps=%d",
|
||||
wav_head->SampleRate,
|
||||
wav_head->NumChannels,
|
||||
wav_head->BitsPerSample);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if data remains, false on error or end of file
|
||||
*/
|
||||
DECODE_STATUS decode_wav(FILE *fp, decode_data *pData, wav_instance *pInstance) {
|
||||
// read an even multiple of frames that can fit into output_samples buffer, otherwise
|
||||
// we would have to manage what happens with partial frames in the output buffer
|
||||
size_t bytes_per_frame = (pInstance->header.BitsPerSample / BITS_PER_BYTE) * pInstance->header.NumChannels;
|
||||
size_t frames_to_read = pData->samples_capacity / bytes_per_frame;
|
||||
size_t bytes_to_read = frames_to_read * bytes_per_frame;
|
||||
|
||||
size_t bytes_read = fread(pData->samples, 1, bytes_to_read, fp);
|
||||
|
||||
pData->fmt.channels = pInstance->header.NumChannels;
|
||||
pData->fmt.bits_per_sample = pInstance->header.BitsPerSample;
|
||||
pData->fmt.sample_rate = pInstance->header.SampleRate;
|
||||
|
||||
if(bytes_read != 0)
|
||||
{
|
||||
pData->frame_count = (bytes_read / (pInstance->header.BitsPerSample / BITS_PER_BYTE)) / pInstance->header.NumChannels;
|
||||
} else {
|
||||
pData->frame_count = 0;
|
||||
}
|
||||
|
||||
LOGI_2("bytes_per_frame %d, bytes_to_read %d, bytes_read %d, frame_count %d",
|
||||
bytes_per_frame, bytes_to_read, bytes_read,
|
||||
pData->frame_count);
|
||||
|
||||
return (bytes_read == 0) ? DECODE_STATUS_DONE : DECODE_STATUS_CONTINUE;
|
||||
}
|
34
managed_components/chmorgan__esp-audio-player/audio_wav.h
Normal file
34
managed_components/chmorgan__esp-audio-player/audio_wav.h
Normal file
|
@ -0,0 +1,34 @@
|
|||
#pragma once
|
||||
|
||||
#include <stdio.h>
|
||||
#include "audio_log.h"
|
||||
#include "audio_decode_types.h"
|
||||
|
||||
typedef struct {
|
||||
// The "RIFF" chunk descriptor
|
||||
uint8_t ChunkID[4];
|
||||
int32_t ChunkSize;
|
||||
uint8_t Format[4];
|
||||
// The "fmt" sub-chunk
|
||||
uint8_t Subchunk1ID[4];
|
||||
int32_t Subchunk1Size;
|
||||
int16_t AudioFormat;
|
||||
int16_t NumChannels;
|
||||
int32_t SampleRate;
|
||||
int32_t ByteRate;
|
||||
int16_t BlockAlign;
|
||||
int16_t BitsPerSample;
|
||||
} wav_header_t;
|
||||
|
||||
typedef struct {
|
||||
// The "data" sub-chunk
|
||||
uint8_t SubchunkID[4];
|
||||
int32_t SubchunkSize;
|
||||
} wav_subchunk_header_t;
|
||||
|
||||
typedef struct {
|
||||
wav_header_t header;
|
||||
} wav_instance;
|
||||
|
||||
bool is_wav(FILE *fp, wav_instance *pInstance);
|
||||
DECODE_STATUS decode_wav(FILE *fp, decode_data *pData, wav_instance *pInstance);
|
|
@ -0,0 +1,8 @@
|
|||
dependencies:
|
||||
chmorgan/esp-libhelix-mp3:
|
||||
version: '>=1.0.0,<2.0.0'
|
||||
idf:
|
||||
version: '>=5.0'
|
||||
description: Lightweight audio decoding component for esp processors
|
||||
url: https://github.com/chmorgan/esp-audio-player
|
||||
version: 1.0.7
|
|
@ -0,0 +1,182 @@
|
|||
/**
|
||||
* @file
|
||||
* @version 0.1
|
||||
*
|
||||
* @copyright Copyright 2021 Espressif Systems (Shanghai) Co. Ltd.
|
||||
* @copyright Copyright 2022 Chris Morgan <chmorgan@gmail.com>
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Design notes
|
||||
*
|
||||
* - There is a distinct event for playing -> playing state transitions.
|
||||
* COMPLETED_PLAYING_NEXT is helpful for users of the audio player to know
|
||||
* the difference between playing and transitioning to another audio file
|
||||
* vs. detecting that the audio file transitioned by looking at
|
||||
* events indicating IDLE and then PLAYING within a short period of time.
|
||||
*
|
||||
* State machine diagram
|
||||
*
|
||||
* cb is the callback function registered with audio_player_callback_register()
|
||||
*
|
||||
* cb(PLAYING) cb(PLAYING)
|
||||
* _______________________________ ____________________________________
|
||||
* | | | |
|
||||
* | | | |
|
||||
* | cb(IDLE) V V cb(PAUSE) |
|
||||
* Idle <------------------------ Playing ----------------------------> Pause
|
||||
* ^ |_____^ |
|
||||
* | cb(COMPLETED_PLAYING_NEXT) |
|
||||
* | |
|
||||
* |______________________________________________________________________|
|
||||
* cb(IDLE)
|
||||
*
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdio.h>
|
||||
#include "esp_err.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "driver/i2s_std.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef enum {
|
||||
AUDIO_PLAYER_STATE_IDLE,
|
||||
AUDIO_PLAYER_STATE_PLAYING,
|
||||
AUDIO_PLAYER_STATE_PAUSE,
|
||||
AUDIO_PLAYER_STATE_SHUTDOWN
|
||||
} audio_player_state_t;
|
||||
|
||||
/**
|
||||
* @brief Get the audio player state
|
||||
*
|
||||
* @return the present audio_player_state_t
|
||||
*/
|
||||
audio_player_state_t audio_player_get_state();
|
||||
|
||||
typedef enum {
|
||||
AUDIO_PLAYER_CALLBACK_EVENT_IDLE, /**< Player is idle, not playing audio */
|
||||
AUDIO_PLAYER_CALLBACK_EVENT_COMPLETED_PLAYING_NEXT, /**< Player is playing and playing a new audio file */
|
||||
AUDIO_PLAYER_CALLBACK_EVENT_PLAYING, /**< Player is playing */
|
||||
AUDIO_PLAYER_CALLBACK_EVENT_PAUSE, /**< Player is pausing */
|
||||
AUDIO_PLAYER_CALLBACK_EVENT_SHUTDOWN, /**< Player is shutting down */
|
||||
AUDIO_PLAYER_CALLBACK_EVENT_UNKNOWN_FILE_TYPE, /**< File type is unknown */
|
||||
AUDIO_PLAYER_CALLBACK_EVENT_UNKNOWN /**< Unknown event */
|
||||
} audio_player_callback_event_t;
|
||||
|
||||
typedef struct {
|
||||
audio_player_callback_event_t audio_event;
|
||||
void *user_ctx;
|
||||
} audio_player_cb_ctx_t;
|
||||
|
||||
/** Audio callback function type */
|
||||
typedef void (*audio_player_cb_t)(audio_player_cb_ctx_t *);
|
||||
|
||||
/**
|
||||
* @brief Play mp3 audio file.
|
||||
*
|
||||
* Will interrupt a present playback and start the new playback
|
||||
* as soon as possible.
|
||||
*
|
||||
* @param fp - If ESP_OK is returned, will be fclose()ed by the audio system
|
||||
* when the playback has completed or in the event of a playback error.
|
||||
* If not ESP_OK returned then should be fclose()d by the caller.
|
||||
* @return
|
||||
* - ESP_OK: Success in queuing play request
|
||||
* - Others: Fail
|
||||
*/
|
||||
esp_err_t audio_player_play(FILE *fp);
|
||||
|
||||
/**
|
||||
* @brief Pause playback
|
||||
*
|
||||
* @return
|
||||
* - ESP_OK: Success in queuing pause request
|
||||
* - Others: Fail
|
||||
*/
|
||||
esp_err_t audio_player_pause(void);
|
||||
|
||||
/**
|
||||
* @brief Resume playback
|
||||
*
|
||||
* Has no effect if playback is not in progress
|
||||
* @return esp_err_t
|
||||
* - ESP_OK: Success in queuing resume request
|
||||
* - Others: Fail
|
||||
*/
|
||||
esp_err_t audio_player_resume(void);
|
||||
|
||||
/**
|
||||
* @brief Stop playback
|
||||
*
|
||||
* Has no effect if playback is already stopped
|
||||
* @return esp_err_t
|
||||
* - ESP_OK: Success in queuing resume request
|
||||
* - Others: Fail
|
||||
*/
|
||||
esp_err_t audio_player_stop(void);
|
||||
|
||||
/**
|
||||
* @brief Register callback for audio event
|
||||
*
|
||||
* @param call_back Call back function
|
||||
* @param user_ctx User context
|
||||
* @return
|
||||
* - ESP_OK: Success
|
||||
* - Others: Fail
|
||||
*/
|
||||
esp_err_t audio_player_callback_register(audio_player_cb_t call_back, void *user_ctx);
|
||||
|
||||
typedef enum {
|
||||
AUDIO_PLAYER_MUTE,
|
||||
AUDIO_PLAYER_UNMUTE
|
||||
} AUDIO_PLAYER_MUTE_SETTING;
|
||||
|
||||
typedef esp_err_t (*audio_player_mute_fn)(AUDIO_PLAYER_MUTE_SETTING setting);
|
||||
typedef esp_err_t (*audio_reconfig_std_clock)(uint32_t rate, uint32_t bits_cfg, i2s_slot_mode_t ch);
|
||||
typedef esp_err_t (*audio_player_write_fn)(void *audio_buffer, size_t len, size_t *bytes_written, uint32_t timeout_ms);
|
||||
|
||||
typedef struct {
|
||||
audio_player_mute_fn mute_fn;
|
||||
audio_reconfig_std_clock clk_set_fn;
|
||||
audio_player_write_fn write_fn;
|
||||
UBaseType_t priority; /*< FreeRTOS task priority */
|
||||
BaseType_t coreID; /*< ESP32 core ID */
|
||||
} audio_player_config_t;
|
||||
|
||||
/**
|
||||
* @brief Initialize hardware, allocate memory, create and start audio task.
|
||||
* Call before any other 'audio' functions.
|
||||
*
|
||||
* @param port - The i2s port for output
|
||||
* @return esp_err_t
|
||||
*/
|
||||
esp_err_t audio_player_new(audio_player_config_t config);
|
||||
|
||||
/**
|
||||
* @brief Shut down audio task, free allocated memory.
|
||||
*
|
||||
* @return esp_err_t ESP_OK upon success, ESP_FAIL if unable to shutdown due to retries exhausted
|
||||
*/
|
||||
esp_err_t audio_player_delete();
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
|
@ -0,0 +1,4 @@
|
|||
idf_component_register(SRC_DIRS "."
|
||||
PRIV_INCLUDE_DIRS "."
|
||||
PRIV_REQUIRES unity test_utils audio_player
|
||||
EMBED_TXTFILES gs-16b-1c-44100hz.mp3)
|
|
@ -0,0 +1,282 @@
|
|||
// Copyright 2020 Espressif Systems (Shanghai) Co. Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#include "esp_log.h"
|
||||
#include "esp_check.h"
|
||||
#include "unity.h"
|
||||
#include "audio_player.h"
|
||||
#include "driver/gpio.h"
|
||||
#include "test_utils.h"
|
||||
#include "freertos/semphr.h"
|
||||
|
||||
static const char *TAG = "AUDIO PLAYER TEST";
|
||||
|
||||
#define CONFIG_BSP_I2S_NUM 1
|
||||
|
||||
/* Audio */
|
||||
#define BSP_I2S_SCLK (GPIO_NUM_17)
|
||||
#define BSP_I2S_MCLK (GPIO_NUM_2)
|
||||
#define BSP_I2S_LCLK (GPIO_NUM_47)
|
||||
#define BSP_I2S_DOUT (GPIO_NUM_15) // To Codec ES8311
|
||||
#define BSP_I2S_DSIN (GPIO_NUM_16) // From ADC ES7210
|
||||
#define BSP_POWER_AMP_IO (GPIO_NUM_46)
|
||||
#define BSP_MUTE_STATUS (GPIO_NUM_1)
|
||||
|
||||
/**
|
||||
* @brief ESP-BOX I2S pinout
|
||||
*
|
||||
* Can be used for i2s_std_gpio_config_t and/or i2s_std_config_t initialization
|
||||
*/
|
||||
#define BSP_I2S_GPIO_CFG \
|
||||
{ \
|
||||
.mclk = BSP_I2S_MCLK, \
|
||||
.bclk = BSP_I2S_SCLK, \
|
||||
.ws = BSP_I2S_LCLK, \
|
||||
.dout = BSP_I2S_DOUT, \
|
||||
.din = BSP_I2S_DSIN, \
|
||||
.invert_flags = { \
|
||||
.mclk_inv = false, \
|
||||
.bclk_inv = false, \
|
||||
.ws_inv = false, \
|
||||
}, \
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Mono Duplex I2S configuration structure
|
||||
*
|
||||
* This configuration is used by default in bsp_audio_init()
|
||||
*/
|
||||
#define BSP_I2S_DUPLEX_MONO_CFG(_sample_rate) \
|
||||
{ \
|
||||
.clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(_sample_rate), \
|
||||
.slot_cfg = I2S_STD_PHILIP_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_MONO), \
|
||||
.gpio_cfg = BSP_I2S_GPIO_CFG, \
|
||||
}
|
||||
|
||||
static i2s_chan_handle_t i2s_tx_chan;
|
||||
static i2s_chan_handle_t i2s_rx_chan;
|
||||
|
||||
static esp_err_t bsp_i2s_write(void * audio_buffer, size_t len, size_t *bytes_written, uint32_t timeout_ms)
|
||||
{
|
||||
esp_err_t ret = ESP_OK;
|
||||
ret = i2s_channel_write(i2s_tx_chan, (char *)audio_buffer, len, bytes_written, timeout_ms);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static esp_err_t bsp_i2s_reconfig_clk(uint32_t rate, uint32_t bits_cfg, i2s_slot_mode_t ch)
|
||||
{
|
||||
esp_err_t ret = ESP_OK;
|
||||
i2s_std_config_t std_cfg = {
|
||||
.clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(rate),
|
||||
.slot_cfg = I2S_STD_PHILIP_SLOT_DEFAULT_CONFIG((i2s_data_bit_width_t)bits_cfg, (i2s_slot_mode_t)ch),
|
||||
.gpio_cfg = BSP_I2S_GPIO_CFG,
|
||||
};
|
||||
|
||||
ret |= i2s_channel_disable(i2s_tx_chan);
|
||||
ret |= i2s_channel_reconfig_std_clock(i2s_tx_chan, &std_cfg.clk_cfg);
|
||||
ret |= i2s_channel_reconfig_std_slot(i2s_tx_chan, &std_cfg.slot_cfg);
|
||||
ret |= i2s_channel_enable(i2s_tx_chan);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static esp_err_t audio_mute_function(AUDIO_PLAYER_MUTE_SETTING setting) {
|
||||
ESP_LOGI(TAG, "mute setting %d", setting);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
TEST_CASE("audio player can be newed and deleted", "[audio player]")
|
||||
{
|
||||
audio_player_config_t config = { .mute_fn = audio_mute_function,
|
||||
.write_fn = bsp_i2s_write,
|
||||
.clk_set_fn = bsp_i2s_reconfig_clk,
|
||||
.priority = 0,
|
||||
.coreID = 0 };
|
||||
esp_err_t ret = audio_player_new(config);
|
||||
TEST_ASSERT_EQUAL(ret, ESP_OK);
|
||||
|
||||
ret = audio_player_delete();
|
||||
TEST_ASSERT_EQUAL(ret, ESP_OK);
|
||||
|
||||
audio_player_state_t state = audio_player_get_state();
|
||||
TEST_ASSERT_EQUAL(state, AUDIO_PLAYER_STATE_SHUTDOWN);
|
||||
}
|
||||
|
||||
static esp_err_t bsp_audio_init(const i2s_std_config_t *i2s_config, i2s_chan_handle_t *tx_channel, i2s_chan_handle_t *rx_channel)
|
||||
{
|
||||
/* Setup I2S peripheral */
|
||||
i2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(CONFIG_BSP_I2S_NUM, I2S_ROLE_MASTER);
|
||||
chan_cfg.auto_clear = true; // Auto clear the legacy data in the DMA buffer
|
||||
ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, tx_channel, rx_channel));
|
||||
|
||||
/* Setup I2S channels */
|
||||
const i2s_std_config_t std_cfg_default = BSP_I2S_DUPLEX_MONO_CFG(22050);
|
||||
const i2s_std_config_t *p_i2s_cfg = &std_cfg_default;
|
||||
if (i2s_config != NULL) {
|
||||
p_i2s_cfg = i2s_config;
|
||||
}
|
||||
|
||||
if (tx_channel != NULL) {
|
||||
ESP_ERROR_CHECK(i2s_channel_init_std_mode(*tx_channel, p_i2s_cfg));
|
||||
ESP_ERROR_CHECK(i2s_channel_enable(*tx_channel));
|
||||
}
|
||||
if (rx_channel != NULL) {
|
||||
ESP_ERROR_CHECK(i2s_channel_init_std_mode(*rx_channel, p_i2s_cfg));
|
||||
ESP_ERROR_CHECK(i2s_channel_enable(*rx_channel));
|
||||
}
|
||||
|
||||
/* Setup power amplifier pin */
|
||||
const gpio_config_t io_conf = {
|
||||
.intr_type = GPIO_INTR_DISABLE,
|
||||
.mode = GPIO_MODE_OUTPUT,
|
||||
.pin_bit_mask = BIT64(BSP_POWER_AMP_IO),
|
||||
.pull_down_en = GPIO_PULLDOWN_DISABLE,
|
||||
.pull_up_en = GPIO_PULLDOWN_DISABLE,
|
||||
};
|
||||
ESP_ERROR_CHECK(gpio_config(&io_conf));
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static audio_player_callback_event_t expected_event;
|
||||
static QueueHandle_t event_queue;
|
||||
|
||||
static void audio_player_callback(audio_player_cb_ctx_t *ctx)
|
||||
{
|
||||
TEST_ASSERT_EQUAL(ctx->audio_event, expected_event);
|
||||
|
||||
// wake up the test so it can continue to the next step
|
||||
TEST_ASSERT_EQUAL(xQueueSend(event_queue, &(ctx->audio_event), 0), pdPASS);
|
||||
}
|
||||
|
||||
TEST_CASE("audio player states and callbacks are correct", "[audio player]")
|
||||
{
|
||||
audio_player_callback_event_t event;
|
||||
|
||||
/* Configure I2S peripheral and Power Amplifier */
|
||||
i2s_std_config_t std_cfg = {
|
||||
.clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(44100),
|
||||
.slot_cfg = I2S_STD_PHILIP_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_STEREO),
|
||||
.gpio_cfg = BSP_I2S_GPIO_CFG,
|
||||
};
|
||||
esp_err_t ret = bsp_audio_init(&std_cfg, &i2s_tx_chan, &i2s_rx_chan);
|
||||
TEST_ASSERT_EQUAL(ret, ESP_OK);
|
||||
|
||||
audio_player_config_t config = { .mute_fn = audio_mute_function,
|
||||
.write_fn = bsp_i2s_write,
|
||||
.clk_set_fn = bsp_i2s_reconfig_clk,
|
||||
.priority = 0,
|
||||
.coreID = 0 };
|
||||
ret = audio_player_new(config);
|
||||
TEST_ASSERT_EQUAL(ret, ESP_OK);
|
||||
|
||||
event_queue = xQueueCreate(1, sizeof(audio_player_callback_event_t));
|
||||
TEST_ASSERT_NOT_NULL(event_queue);
|
||||
|
||||
ret = audio_player_callback_register(audio_player_callback, NULL);
|
||||
TEST_ASSERT_EQUAL(ret, ESP_OK);
|
||||
|
||||
audio_player_state_t state = audio_player_get_state();
|
||||
TEST_ASSERT_EQUAL(state, AUDIO_PLAYER_STATE_IDLE);
|
||||
|
||||
extern const char mp3_start[] asm("_binary_gs_16b_1c_44100hz_mp3_start");
|
||||
extern const char mp3_end[] asm("_binary_gs_16b_1c_44100hz_mp3_end");
|
||||
|
||||
// -1 due to the size being 1 byte too large, I think because end is the byte
|
||||
// immediately after the last byte in the memory but I'm not sure - cmm 2022-08-20
|
||||
//
|
||||
// Suppression as these are linker symbols and cppcheck doesn't know how to ensure
|
||||
// they are the same object
|
||||
// cppcheck-suppress comparePointers
|
||||
size_t mp3_size = (mp3_end - mp3_start) - 1;
|
||||
ESP_LOGI(TAG, "mp3_size %zu bytes", mp3_size);
|
||||
|
||||
FILE *fp = fmemopen((void*)mp3_start, mp3_size, "rb");
|
||||
TEST_ASSERT_NOT_NULL(fp);
|
||||
|
||||
|
||||
|
||||
///////////////
|
||||
expected_event = AUDIO_PLAYER_CALLBACK_EVENT_PLAYING;
|
||||
ret = audio_player_play(fp);
|
||||
TEST_ASSERT_EQUAL(ret, ESP_OK);
|
||||
|
||||
// wait for playing event to arrive
|
||||
TEST_ASSERT_EQUAL(xQueueReceive(event_queue, &event, pdMS_TO_TICKS(100)), pdPASS);
|
||||
|
||||
// confirm state is playing
|
||||
state = audio_player_get_state();
|
||||
TEST_ASSERT_EQUAL(state, AUDIO_PLAYER_STATE_PLAYING);
|
||||
|
||||
|
||||
|
||||
///////////////
|
||||
expected_event = AUDIO_PLAYER_CALLBACK_EVENT_PAUSE;
|
||||
ret = audio_player_pause();
|
||||
TEST_ASSERT_EQUAL(ret, ESP_OK);
|
||||
|
||||
// wait for paused event to arrive
|
||||
TEST_ASSERT_EQUAL(xQueueReceive(event_queue, &event, pdMS_TO_TICKS(100)), pdPASS);
|
||||
|
||||
state = audio_player_get_state();
|
||||
TEST_ASSERT_EQUAL(state, AUDIO_PLAYER_STATE_PAUSE);
|
||||
|
||||
|
||||
|
||||
////////////////
|
||||
expected_event = AUDIO_PLAYER_CALLBACK_EVENT_PLAYING;
|
||||
ret = audio_player_resume();
|
||||
TEST_ASSERT_EQUAL(ret, ESP_OK);
|
||||
|
||||
// wait for paused event to arrive
|
||||
TEST_ASSERT_EQUAL(xQueueReceive(event_queue, &event, pdMS_TO_TICKS(100)), pdPASS);
|
||||
|
||||
|
||||
|
||||
///////////////
|
||||
expected_event = AUDIO_PLAYER_CALLBACK_EVENT_IDLE;
|
||||
|
||||
// the track is 16 seconds long so lets wait a bit here
|
||||
int sleep_seconds = 16;
|
||||
ESP_LOGI(TAG, "sleeping for %d seconds for playback to complete", sleep_seconds);
|
||||
vTaskDelay(pdMS_TO_TICKS(sleep_seconds * 1000));
|
||||
|
||||
// wait for idle event to arrive
|
||||
TEST_ASSERT_EQUAL(xQueueReceive(event_queue, &event, pdMS_TO_TICKS(100)), pdPASS);
|
||||
|
||||
state = audio_player_get_state();
|
||||
TEST_ASSERT_EQUAL(state, AUDIO_PLAYER_STATE_IDLE);
|
||||
|
||||
|
||||
|
||||
///////////////
|
||||
expected_event = AUDIO_PLAYER_CALLBACK_EVENT_SHUTDOWN;
|
||||
ret = audio_player_delete();
|
||||
TEST_ASSERT_EQUAL(ret, ESP_OK);
|
||||
|
||||
// wait for idle event to arrive
|
||||
TEST_ASSERT_EQUAL(xQueueReceive(event_queue, &event, pdMS_TO_TICKS(100)), pdPASS);
|
||||
|
||||
state = audio_player_get_state();
|
||||
TEST_ASSERT_EQUAL(state, AUDIO_PLAYER_STATE_SHUTDOWN);
|
||||
|
||||
vQueueDelete(event_queue);
|
||||
|
||||
TEST_ESP_OK(i2s_channel_disable(i2s_tx_chan));
|
||||
TEST_ESP_OK(i2s_channel_disable(i2s_rx_chan));
|
||||
TEST_ESP_OK(i2s_del_channel(i2s_tx_chan));
|
||||
TEST_ESP_OK(i2s_del_channel(i2s_rx_chan));
|
||||
|
||||
ESP_LOGI(TAG, "NOTE: a memory leak will be reported the first time this test runs.\n");
|
||||
ESP_LOGI(TAG, "esp-idf v4.4.1 and v4.4.2 both leak memory between i2s_driver_install() and i2s_driver_uninstall()\n");
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
#
|
||||
#Component Makefile
|
||||
#
|
||||
|
||||
COMPONENT_ADD_LDFLAGS = -Wl,--whole-archive -l$(COMPONENT_NAME) -Wl,--no-whole-archive
|
||||
COMPONENT_EMBED_TXTFILES += gs-16b-1c-44100hz.mp3
|
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue