Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ sdkconfig
sdkconfig.old
.DS_Store
compile_commands.json
build/
27 changes: 0 additions & 27 deletions Justfile

This file was deleted.

32 changes: 32 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# PocketMage app build recipe for PocketMuse.
#
# Point at the SDK checkout first:
# export PM_SDK_ROOT=/path/to/PocketMage_SDK
# or pass it per-invocation:
# make SDK_ROOT=/path/to/PocketMage_SDK

SDK_ROOT ?= $(PM_SDK_ROOT)
APP_NAME ?= $(notdir $(CURDIR))

# The pack target ships $(APP_NAME)_ICON.bin; regenerate it from the PNG
# asset whenever the name or the artwork changes. Plain `make` (the ELF only)
# needs no Pillow; `make pack` does.
$(APP_NAME)_ICON.bin: assets/icon.png tools/make_icon.py
python3 tools/make_icon.py assets/icon.png $@

APP_SRCS := \
src/main.cpp \
src/muse.cpp \
src/library.cpp \
src/playlist.cpp \
src/ui_player.cpp \
src/ui/ui_browser.cpp \
src/ui/ui_nowplaying.cpp \
src/ui/ui_playlist.cpp \
src/ui/ui_helpers.cpp

APP_CPPFLAGS += -Iinclude

include $(SDK_ROOT)/tools/app.mk

icon: $(APP_NAME)_ICON.bin
Binary file removed PocketMuse.tar
Binary file not shown.
118 changes: 83 additions & 35 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,32 +1,40 @@
# PocketMuse

Music player app for the PocketMage PDA. Streams MP3, (AAC and WAV soon)
from SD card over I2S single-pin PDM output through the piezo buzzer GPIO.
Music player app for the PocketMage PDA. Streams MP3 from SD through the OS
audio service, using the built-in PWM piezo or a USB Audio Class speaker.
Supports library browsing, now-playing with album art, playlist management,
shuffle, and loop modes. Runs as an OTA application slot on the PocketMage OS.
shuffle, and loop modes. Runs as a dynamically loaded ELF app on PocketMage OS.

## Build

Requires PlatformIO, `just`, and the standard ESP32-S3 toolchain.
PocketMuse is an ELF app built against the PocketMage SDK.
Build it with the `pm` CLI from the SDK checkout:

```sh
just build
pm build --pack
```

This runs two scripts in sequence:
That compiles `src/`, links `build/PocketMuse.app.elf` plus the
`build/PocketMuse.elfo` overlay, and packs them into `build/PocketMuse.tar`
for installation through the OS app manager. Packaging requires the SDK's
`elf_overlay_relink` and `elf_overlay_emit` tools on `PATH`.

1. `scripts/build_firmware.py` - run `pio run -e OTA_APP`.
2. `scripts/package_ota.py` - wrap `firmware.bin` into `PocketMuse.tar` for
OTA distribution.

The default environment is `OTA_APP`. Set `env` in the `Justfile` or pass
`ENV=<name>` to build a different PlatformIO environment.
For an ELF-only clean build from this checkout:

```sh
just upload # build + upload via USB
just monitor # build + upload + serial monitor
export SDK_ROOT="$(cd ../PocketMage_PDA/Code/PocketMageOS/lib/PocketMage_SDK && pwd)"
make SDK_ROOT="$SDK_ROOT" clean
make SDK_ROOT="$SDK_ROOT" elf
```

The SDK build recipe locates the installed Espressif Xtensa C++ toolchain.
Build the matching host from `PocketMage_PDA/Code/PocketMageOS` with
`pio run -e PM_BETA`. The host must export the playback-request, image-region,
SD-stream, drawing, and activity APIs used by this app.

`mp3` files live in `/music/` at the root of the SD card; the app creates that
directory along with `/music/.muse/` on first launch.

## Key bindings

| Input | Action |
Expand All @@ -35,50 +43,90 @@ just monitor # build + upload + serial monitor
| UP / DOWN | Volume (Now Playing) |
| SPACE | Play / pause |
| ENTER | Select / play |
| P | Cycle source (Lib/Fav/Pl) |
| P | Cycle source in browser; show queue from Now Playing |
| F | Toggle favorite |
| S | Toggle shuffle |
| L | Cycle loop (None/One/All) |
| D | Delete from playlist |
| B | Back to browser |
| D | Remove selected favorite / playlist entry; keep the music file |
| U | Output: Auto / Piezo / USB |
| B | Switch browser / Now Playing; return from queue |
| ? | Help overlay |
| ESC / A | Exit to PocketMage OS |

Touch scroll on the slider controls volume in all modes.

## Dependencies
## Playback and storage

- Auto selects a connected UAC speaker; Piezo forces the built-in output.
USB falls back to piezo when no compatible speaker is available. The USB
status chip appears only while USB is the active playback output.
- Volume and output preference are shared SDK settings in NVS (`PMAudio`). Favorites use
an absolute-path list; playlists are `.m3u` files under
`/music/.muse/playlists/`, with one absolute SD path per entry. Blank lines,
comments, LF, and CRLF are supported. Empty sources remain browsable.
- Natural EOF advances once. Repeat-one affects automatic progression;
manual next still skips. Shuffle stops after its cycle unless repeat-all
is enabled. Favorite edits outside the active source preserve the current
shuffle cycle; a missing playing track keeps the not-found index at `-1`.
- ID3v2.3/v2.4 tags and embedded JPEG/PNG cover regions are read by the SDK's
streaming media service. The OS decodes covers into a 1-bit image buffer. Unsupported/missing artwork
uses the app's fallback display.
- The OLED uses decoded PCM frame timing for elapsed playback. Total duration
comes from MPEG headers, including Xing/VBRI when present, with a bitrate
estimate when an exact duration is unavailable.
- Idle sleep belongs to the OS and uses its `TIMEOUT` setting. Native and ELF
input reset the same timer. Playback, open SD streams, USB audio presence,
USB mode, and OS timeout-inhibition flags keep the device awake.

## Architecture

PocketMuse calls the SDK C ABI; it does not link Arduino or audio/display
drivers. The host owns Espressif `esp_audio_codec`, `pwm_audio`, `usb_stream`,
JPEGDEC/PNGdec, GxEPD2, U8g2, SD access, and sleep management.

Playback queries, metadata/cover probing, image-region decoding, SD operations,
and canvas primitives are generated wrappers over native SDK services. The SDK
owns PCM gain, timing, shared audio settings, and metadata parsing. PocketMuse
owns its library, playlist, shuffle, and presentation state. The SDK retains
ABI 2 adapters for existing apps.

USB serial is the default connector role. Piezo and unattached Auto sessions
retain it; an attached accessory receives one bounded UAC probe. An unsupported
device can then be tried as a keyboard. App exit waits for audio cleanup while
its callbacks and code remain resident.

## Native tests

```sh
python3 tests/run.py
```

Requires Python 3 and a C++17 compiler with AddressSanitizer and UBSan. `CXX`
selects the compiler; `SDK_ROOT` overrides the adjacent SDK checkout. The
tests exercise playlist parsing/mutation, shuffle continuity, playback progression, source
cycling, output preference, playback request results, and idle-timer rollover
and inhibition. They use in-memory SD files and require no device.

- **PlatformIO** (`espressif32` platform, Arduino framework)
- **arduino-audio-tools** - audio pipeline (I2S, source callbacks, fading)
- **arduino-libhelix** - MP3/AAC decode
- **GxEPD2** (fork:
[`ashtf8/GxEPD2_Editable_useFastFullUpdate`](https://github.com/ashtf8/GxEPD2_Editable_useFastFullUpdate))
- **JPEGDEC** - album art decode
- **PNGdec** - album art decode
- **U8g2** - OLED display
USB enumeration/hotplug, audible output, album-art rendering, both displays,
and deep sleep still require validation on a PocketMage device.

## Project layout

```text
src/
main.cpp audio pipeline, app entry, setup/loop
main.cpp app entry, input and render loop
muse.cpp app-wide globals
library.cpp SD card track scanner (/music/)
playlist.cpp playlist manager (favorites, .m3u)
metacache.cpp ID3 metadata cache on SD
albumart.cpp ID3v2 APIC extraction, JPEG/PNG decode, 1-bit render
ui_player.cpp playback control, shuffle, duration compute
ui_player.cpp playback control, shuffle, shared media-service queries
ui/
ui_browser.cpp library/favorites/playlist browser
ui_nowplaying.cpp now-playing screen with album art
ui_playlist.cpp playlist queue view
ui_helpers.cpp header/footer/scrollbar/OLED/time formatting
include/ public headers
lib/
PocketMage/ shared PocketMage PDA framework (submodule)
scripts/
build_firmware.py PlatformIO build helper
package_ota.py firmware -> tar for OTA
tests/ native regression suites and in-memory SD support
design/ chosen display mockups
```

## License
Expand Down
4 changes: 4 additions & 0 deletions app.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
name=PocketMuse
version=0.2.0
author=NellowTCS
scope=all
Binary file added assets/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
43 changes: 0 additions & 43 deletions include/albumart.h

This file was deleted.

1 change: 0 additions & 1 deletion include/config.h

This file was deleted.

1 change: 0 additions & 1 deletion include/globals.h

This file was deleted.

30 changes: 18 additions & 12 deletions include/library.h
Original file line number Diff line number Diff line change
@@ -1,23 +1,29 @@
#pragma once
#include <stdint.h>
#include <stddef.h>
#include <vector>
#include <Arduino.h>

#include <cstddef>
#include <cstdint>

class Library {
public:
Library();
Library();
~Library();

int scan();
void clear();
int scan();
void clear();

int count() const { return (int)tracks_.size(); }
const String& path(int index) const;
int count() const { return count_; }
const char *path(int index) const;

private:
void scanDir_(const char* dirname, int depth);
void scanDir_(const char *dirname, int depth, int maxDepth);

static constexpr int kMaxDepth = 2;
static constexpr int kMaxDepth = 3;
static constexpr int kFallbackDepth = 1;
static constexpr int kMaxTracks = 400;

std::vector<String> tracks_;
const char **tracks_;
int tracks_cap_;
int count_;
};

Library &library();
20 changes: 0 additions & 20 deletions include/metacache.h

This file was deleted.

Loading
Loading