Initial public release of the 2024A software.

This commit is contained in:
Joe Kearney 2025-01-25 14:04:42 -06:00
parent 7b9ad3edfd
commit 303e9e1dad
361 changed files with 60083 additions and 2 deletions

View file

@ -0,0 +1 @@
c8ac1998e9af863bc41b57e592f88d1a5791a0f891485122336ddabbf7a65033

View 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
)

View 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

View 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.

View file

@ -0,0 +1,80 @@
# Audio player component for esp32
[![cppcheck-action](https://github.com/chmorgan/esp-audio-player/actions/workflows/cppcheck.yml/badge.svg)](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.

View file

@ -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

View 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

View 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;
}

View 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);

View 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;
}

View 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;
}

View 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);

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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");
}

View file

@ -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