Note: these blue boxes are instructions to you to make the documentation, and should not be included in your final submission!
1b. A photo that is a good overall view of the project. This image should be one of the "well-shot" images described below, or a cropped subset of one of them.
Front view of the audio time capsule, using Koss Porta Pro headphones as audio output.
Imagine you're walking on a beach with a friend. You hear the sounds of waves crashing, seagulls chirping in the distance. By holding the lever of the audio time capsule, you can record a snippet of this moment, and the capsule fills up a bit. Perhaps the next day you're at a bonfire, and capture the sounds of crackling wood and laughter. Each audio snippet is stitched back to back as the device fills up, like a casette. Once it's full, you break off a plastic part that exposes the audio jack, and you are then able to listen to the 'tape'. After breaking the seal, you can't record audio to it anymore, and the audio is premanently trapped into the device. You could listen to those moments again and again, or gift it to a friend as a memory.
1d. At least four good still images of the final project. If it's possible for you to take higher-quality image, like using a DSLR (here's the class DSLR photography guide) that's fine; but camera phone pictures are ok, too if they're carefully shot and good quality. Images:
Overall photo for proportion and scale (at least 1)
Detail photo of any part that you'd like to highlight (up to 3)
Images showing the thing being used (up to 3)
1 inch wide! Very skinty
The OLED screen ideally was supposed to display what track is playing, and how filled up the device is.
This is the lever arm button, that gives a nice satisfying click when recording.
Plugged in, audio playing in use.
Controls overview. Here you can see how I needed to overlay a smaller proto ontop of the flexy protoboard to keep the resistors more organized. The resistors were for mic channels and buttons.
1e. Moving image (one or more), a .gif, .mp4, or .mov file. Your movie can be as short as a second or two if that's all that needed to show the interactivity of your device, but should go longer as is necessary. It can feature voice narration if you like, but does not need to.
Upload instructions: Do not merely link to an outside hosting service like YouTube and do not embed a YouTube or other video service frame into your page. Rather, the videos are inserted by uploading a video file to this Google Drive folder and then under the Insert tab on the right, selecting "Drive," then clicking on the Shared drive called "60-223 f25." By keeping video files in that Shared drive, we ensure that this documentation page will remain intact for the long term.
If the movie is not audibly narrated, a caption should describe the action of the movie so that a blind person would still be able to understand what is happening.
(Note that videos don't have captions or alt-text options on Google Sites, so you just need to write your own text boxes below them.)
VERSION 3 - FINAL - COMPACT FEATHER FEATHER RP240 x VS1053B AUDIO BOARD
In this video, I show how the compacted mp3 player can play music. However, this compact version does not functionally record audio or can play or pause the music. It can change volume. The reason is unknown. Version 2 is the same wiring setup, but breadboarded to test functionality, where everything works.
VERSION 2 - FEATHER RP240 x VS1053B AUDIO BOARD
This version is a breadboarded test version with all the same components as the final v3 compacted version. It was successful in testing for all funtions:
Recording audio files to the SD card
Playing back audio files (mp3, wav, oog)
Changing volume
Pausing and playing back music
My question I wanted to answer for this version was mainly how to make sure SPI communication works, learning how to upload to an RP2040 (a bit different than with Arduino's chip), and picking controller components for what the user will interface with. Which buttons feel good? I settled on a lever looking button for the 'record' trigger on the v3.
V1 - ARDUINO UNO x ADAFRUIT MUSIC SHIELD
This prototype was a first go at getting familiar with the VS1053B audio library for the Arduino IDE, since I knew my final proto would use the same audio chip, just on a smaller board. It also taught me how to bend wires cleanly and develop an approach and intuition for compacting items in a pleasing way.
2. Process images and review
Your process section should have at least 4 photos each of which is captioned, and ~100–300 words describing any aspect(s) of your process you'd like your readers to know about. Did something surprise you with how well or how terribly it went? Did something take much longer than expected? Did you get lucky and something worked out unexpectedly? Tell us about it.
Add captions and alt text to your images so your reader understands their relevance and meaning.
Flexible protoboard is really great for weaving over/under wiring for compact setups. However, it can be a headache to solder since you can't see underneath the board in my setup here. Next time I'll be soldering onto a solid board and make sure the underside has all the pinholes cleanly connected before flat-packing it with the rest of the assembly. [INSERT IMAGE OF COMPLETED FLEX PCB - ITS HEINOUS]
Keeping my schematic clearly drawn infront of me at all times was a lifesaver. 10 minutes of drawing saved hours.
Cursor helped me understand a lot of concepts around audio in electronics. Here it was explaining the impact of buffer size and sample rate in audio recording. Reducing the sample rate helped somewhat with the loud 'brrrr' or thundering crunchyness of the mic, but very little. I discovered the adafruit mic just doesn't have the quality that will offer clean voice playback, and I'll need to upgrade in the future.
oooo spider stand helped me solder better
My process of wiring wires directly to the RP2040 feather. Our claw grips to hold up parts didn't work great, so I just rested the board on a bolt head and did my best.
Lastly, one huge thing I took away from this project that I do not need to use CircuitPython. While MicroPython (ajdacent) I'm told is very powerful and I'm already familiar with python logic, the examples I found for audio were few and far, and Cursor's variables and language felt unreadable compared to the library adafruit already built in C for the VS1053B audio board. I realized you don't need to use CircuitPython with the feather, and normal C with Arduino IDE was the most straightforward. There isn't really visual feedback on the board that there was a successful upload, but you need to tune into the serial monitor to make sure code is running.
Address all of the prompts below. It is best to address all of these topics in a natural piece of prose. However, if you prefer, you may write four disjoint paragraphs, each of which is addressing a prompt. (The first way is better.) In total, this section should be ~300–500 words.
A reminder that as outlined in the course syllabus, you are strictly prohibited from using any generative AI service or writing assistant in your reflection/discussion.
Response to comments gathered during the in-class crit. Quote (verbatim) from *at least* two written critiques that you received (positive, negative, or otherwise) from the in-class crit, and respond to them. (You may agree or disagree—in either case, explain your position.) You can access the written critique feedback by going to the class's Shared Drive and opening "11/5/25 Project 2 Final Critique feedback".
Self critique pertaining to the project. Are you happy with how the project came out? Did it satisfy your own goals? This *should not* simply be a recital of your process, but rather a meaningful reflection.
What you learned about your own abilities and/or limitations through the process of working on this project. These could be technical in nature (i.e. "I found that coding this particular behavior was surprisingly difficult"), or not (i.e. "I enjoyed making cardboard forms very much, and I think it will be a useful prototyping medium for me in the future"). What would you do differently next time? What would your advice to your past self be? Did you get hung up at a particular point in progress, or fail to see an easy workaround to a problem? Did you find a creative spark you didn't anticipate? Etc.?
Next steps. Do you expect to build another iteration of this project? If so, describe what you're planning to do. If not, describe what you *would* do if you were to build another iteration, based on the experience you had with this first one.
One person said that they "would love to see what the housing would look like for this, maybe have it to have a clip of some kind so it can hook onto a belt and is designed intentionally to be traveled with like a Walkman." I'll admit that I didn't have the time to consider the housing and wanted to use this version 3 final prototype as a first shot at figuring out what are the limits to how compact I can get this device. As for the housing, I'm thinking of something that alludes to the 2000s or 2005 era Sony MP3 players, something that has a clear shell and maybe an OLED inner, or that exposes some of the inner workings of the device, with many round corners and edges. I want it to feel very friendly and orb-like, capsule-like.
I know there's a lot of things in my approach to soldering the wiring that I know I'm going to change for the next iteration, so taking the time to give it my best shot with a simple sketch and on-the-fly decision-making on how wires get wrapped and protoboards get layered gave me the information I need to solder in a better order of operations for a cleaner, more painless build. I do believe this was the most compact I could get. I feel like I definitely met my goals with figuring out how hard it is and how well I can compact something. I gave a really big effort on that and also a really big effort on understanding the RP2040 and all the different ways that you can interface with it. I used Circuit Python, tried Micro Python, had some success with it, but then ended up reverting to using C with the Arduino IDE because that had the native library and was also a lot more readable and understandable for me. I feel like I could confidently start up another project using this chip and Adafruit's Feather series, so I think that was a really big point of growth too. This project felt like a sprint, so branching out to other microcontrollers like an ESP32 feels achievable despite not knowing too much about how. I also improved my vibecoding skills a lot, mainly using cursor but also some claudecode in the CLI and asking it to write key context in obsidian .md files for teaching me about what I'm doing and also for itself to remember what's going on in the projects and key rules/constraints I set for claude to not go off-the-walls with false information.
Next time, I would definitely try and attempt to improve, but I would continue to try and not let myself get hung up if one approach to the problem isn't working. I spent probably half a day trying to get CircuitPython to work because that's how I thought the RP2040 was supposed to work. Yes, it is intended to be used with CircuitPython, but it just wasn't working for me. There was no existing audio library for the audio chip I was using in MicroPython anyways, so it just didn't make sense. Sometimes you have to let things go and move on. I also wish I had time for the housing, but since the timeline is so short and I plan to improve this project and use it in my design studio class, I have time to work on the form later. I think that it was smart time budgeting on my behalf when I thought about the context of my entire classwork situation. I'm really glad that I didn't choose to make an e-reader, because yes, that was something I was excited about, but by doing this project, it saves me time in my studio and it was also super super fun. Getting extra feedback early on (since my studio project is due end of semester) was incredible as well.
As for next steps, my plan is to:
Buy a much better mic
Make sure the compact or redo the compact v3 because some functions aren't working
Design the housing
Potentially collab with Perry if he wants to design a PCB and I do the housing, or give KiCad an attempt if I have time ... might be a long shot though lol
Another person said "The concept is so great, I love the concept of saving something ephemeral in audio instead of visually like we usually do." I find it so cool to hear overwhelming support for the idea of an audio time capsule, and I feel like it could be something so unique and special—I'm so curious how people might use it in context like on a trip, whenever going out with friends, or even documenting a long project in a team and wanting to save some memories from that journey.
/*
Audio Time Capsule — Adafruit Feather RP2040 + VS1053b breakout
by Viviana Staicu
AI was used to support the making of this project's code using Opus 4.6 via Cursor.
On boot: plays the first audio file (.OGG/.MP3/.WAV) found on the SD card
through LOUT / ROUT / AGND on the VS1053b.
Buttons:
GPIO 7 — pause / resume playback (some boards print “5” near this pin; use the
RP2040 GPIO number, not the silk text alone)
D13 — toggle recording on/off (armed only after boot delay — see below)
OLED 128×64 (I2C, 4-pin: VCC GND SDA SCL):
VCC → 3V (Feather is 3.3 V logic; use 3V unless your module docs say 5 V only)
GND → GND
SDA → SDA (GPIO 2 on Feather RP2040)
SCL → SCL (GPIO 3)
Wiring (Feather RP2040 → VS1053b breakout):
XCS → D11 VS1053 command chip-select
XDCS → D10 VS1053 data chip-select
DREQ → D12 VS1053 data-request (high = ready)
SDCS → D9 SD card chip-select on the VS1053 breakout
SPI → SCK / MO / MI (hardware SPI0)
XRST → not connected (breakout pull-up keeps it high)
Audio out: LOUT, ROUT, AGND → 3.5 mm jack
Volume pot (0–100 % loudness):
Wiper → an **ADC** pin only: A0 (GP26), A1 (GP27), A2 (GP28), or A3 (GP29).
The silk “D8” / GPIO8 pin has **no** analog input on Feather RP2040 — if you
wired the wiper there, move it to A0 (or A1–A3) and set VOLUME_POT_PIN below.
Ends of pot: 3V3 and GND.
SD card must contain:
v44k1q05.img (VS1053 OGG recording plugin — copy from
Adafruit_VS1053_Library/examples/record_ogg/)
Libraries: Adafruit VS1053, SD, Adafruit NeoPixel, Wire, Adafruit GFX, Adafruit SSD1306
(Recording: NeoPixel blinks red. Playback: smooth rainbow loop while audio plays.)
*/
#include <SPI.h>
#include <Wire.h>
#include <SD.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <Adafruit_VS1053.h>
#include <Adafruit_NeoPixel.h>
// I2C OLED 128×64 (SSD1306). If the screen stays black, try OLED_I2C_ADDR 0x3D.
#define OLED_I2C_ADDR 0x3C
#define OLED_WIDTH 128
#define OLED_HEIGHT 64
Adafruit_SSD1306 oled(OLED_WIDTH, OLED_HEIGHT, &Wire, -1);
bool oledOk = false;
unsigned long lastOledPaintMs = 0;
const unsigned long OLED_PAINT_MS = 250;
// Feather RP2040 onboard NeoPixel: data on GPIO17, power gated on GPIO16
#ifndef PIN_NEOPIXEL
#define PIN_NEOPIXEL 17
#endif
#ifndef NEOPIXEL_POWER
#define NEOPIXEL_POWER 16 // Feather RP2040: high = NeoPixel supply on
#endif
Adafruit_NeoPixel statusPixel(1, PIN_NEOPIXEL, NEO_GRB + NEO_KHZ800);
unsigned long recBlinkLastMs = 0;
bool recBlinkOn = false;
const unsigned long REC_BLINK_MS = 350;
// NeoPixel ambient rainbow while playing (HSV hue 0…65535)
uint16_t ambientHue = 0;
unsigned long ambientLastMs = 0;
const unsigned long AMBIENT_MIN_STEP_MS = 22;
const unsigned long AMBIENT_FULL_CYCLE_MS = 16000; // ms for one full hue loop
static bool playbackAmbientLedOn = false;
// ── Pin definitions ──────────────────────────────────────────────────
#define VS1053_CS 11
#define VS1053_DCS 10
#define VS1053_DREQ 12
#define VS1053_RESET -1
#define CARDCS 9
#define REC_BUTTON 13 // record start/stop
#define PLAY_BUTTON 7 // pause/resume (silk may say “5”; this board uses GPIO 7)
// Analog volume: only A0–A3 work on Feather RP2040 (not GPIO8/D8).
#define VOLUME_POT_PIN A0
// Ignore record button until this time (ms) — avoids accidental start at power-up
const unsigned long RECORD_ARM_MS = 3500;
unsigned long recordEnabledAtMs = 0;
// ── VS1053 FilePlayer (inherits recording methods too) ───────────────
Adafruit_VS1053_FilePlayer player(VS1053_RESET, VS1053_CS, VS1053_DCS,
VS1053_DREQ, CARDCS);
// ── Recording state ──────────────────────────────────────────────────
File recordingFile;
bool isRecording = false;
uint8_t recording_buffer[512];
char recpath[20];
int recNum = 1;
// ── Playback state ───────────────────────────────────────────────────
char playfile[13];
bool foundPlayFile = false;
// ── Button debounce ──────────────────────────────────────────────────
bool lastRecBtn = HIGH;
bool lastPlayBtn = HIGH;
unsigned long lastRecDebounce = 0;
unsigned long lastPlayDebounce = 0;
const unsigned long DEBOUNCE_MS = 300;
// VS1053 setVolume: 0 = loudest, 255 ≈ mute — map pot 0…100 % → atten 255…0
const int ADC_FULL = 4095; // after analogReadResolution(12)
const uint8_t VOLUME_HYST = 2; // ignore tiny jitter (in raw atten steps)
const unsigned long VOLUME_POLL_MS = 20;
static int lastVolAtten = -1;
static unsigned long lastVolPoll = 0;
void updateVolumeFromPot() {
if (millis() - lastVolPoll < VOLUME_POLL_MS)
return;
lastVolPoll = millis();
uint32_t sum = 0;
for (uint8_t i = 0; i < 4; i++)
sum += analogRead(VOLUME_POT_PIN);
int raw = (int)(sum / 4);
// 0 % = mute (255), 100 % = full level (0)
int atten = map(raw, 0, ADC_FULL, 255, 0);
atten = constrain(atten, 0, 255);
if (lastVolAtten < 0 || abs(atten - lastVolAtten) >= VOLUME_HYST) {
lastVolAtten = atten;
player.setVolume((uint8_t)atten, (uint8_t)atten);
}
}
// ── Find the first playable file on the SD root ──────────────────────
bool findFirstAudioFile() {
File root = SD.open("/");
if (!root) return false;
while (true) {
File entry = root.openNextFile();
if (!entry) break;
if (entry.isDirectory()) { entry.close(); continue; }
const char *name = entry.name();
size_t len = strlen(name);
if (len > 4) {
const char *ext = name + len - 4;
if (strcasecmp(ext, ".ogg") == 0 ||
strcasecmp(ext, ".mp3") == 0 ||
strcasecmp(ext, ".wav") == 0) {
strncpy(playfile, name, sizeof(playfile) - 1);
playfile[sizeof(playfile) - 1] = '\0';
entry.close();
root.close();
return true;
}
}
entry.close();
}
root.close();
return false;
}
// ── Recording helpers ────────────────────────────────────────────────
void findNextRecFilename() {
while (recNum < 10000) {
snprintf(recpath, sizeof(recpath), "REC%04d.OGG", recNum);
if (!SD.exists(recpath)) return;
recNum++;
}
}
void startRecording() {
if (player.playingMusic || player.paused()) {
player.stopPlaying();
Serial.println("Playback stopped for recording.");
}
player.softReset();
delay(100);
findNextRecFilename();
if (SD.exists(recpath)) SD.remove(recpath);
recordingFile = SD.open(recpath, FILE_WRITE);
if (!recordingFile) {
Serial.print("[ERR] Could not open ");
Serial.println(recpath);
return;
}
if (!player.prepareRecordOgg((char *)"v44k1q05.img")) {
Serial.println("[ERR] OGG plugin load failed — is v44k1q05.img on the SD root?");
recordingFile.close();
return;
}
player.startRecordOgg(true);
isRecording = true;
recBlinkLastMs = millis();
recBlinkOn = true;
statusPixel.setPixelColor(0, statusPixel.Color(255, 0, 0));
statusPixel.show();
Serial.print("** RECORDING → ");
Serial.print(recpath);
Serial.println(" — press D13 to stop **");
paintOled();
}
void stopRecording() {
player.stopRecordOgg();
flushRecordingData();
recordingFile.close();
isRecording = false;
statusPixel.clear();
statusPixel.show();
recNum++;
Serial.print("Saved ");
Serial.println(recpath);
player.softReset();
delay(100);
// Recording / OGG plugin raised CLOCKF to 0xC000 — decoder needs 0x6000 again
player.sciWrite(VS1053_REG_CLOCKF, 0x6000);
delay(10);
lastVolAtten = -1;
lastVolPoll = 0; // bypass throttle so volume applies immediately
updateVolumeFromPot();
Serial.println("Ready. GPIO7 = play, D13 = record, pot = volume.");
paintOled();
}
void flushRecordingData() {
uint16_t wordsWaiting = player.recordedWordsWaiting();
while (wordsWaiting > 256) {
for (int i = 0; i < 256; i++) {
uint16_t w = player.recordedReadWord();
recording_buffer[i * 2] = w >> 8;
recording_buffer[i * 2 + 1] = w & 0xFF;
}
recordingFile.write(recording_buffer, 512);
wordsWaiting -= 256;
}
}
// ── Playback helpers ─────────────────────────────────────────────────
void beginPlayback() {
player.stopPlaying(); // clear SM_CANCEL, close any stale file handle
delay(20);
foundPlayFile = findFirstAudioFile();
if (!foundPlayFile) {
Serial.println("No .OGG/.MP3/.WAV on SD root (v44k1q05.img alone won't play).");
Serial.println("Copy a music file to the card for auto-play, or use D13 to record.");
paintOled();
return;
}
player.sciWrite(VS1053_REG_CLOCKF, 0x6000);
delay(10);
Serial.print("Playing: ");
Serial.println(playfile);
if (!player.startPlayingFile(playfile)) {
Serial.println("[ERR] startPlayingFile failed — check filename / SD.");
}
paintOled();
}
// ── Button edge detectors ────────────────────────────────────────────
bool recButtonPressed() {
bool state = digitalRead(REC_BUTTON);
if (state == LOW && lastRecBtn == HIGH &&
(millis() - lastRecDebounce > DEBOUNCE_MS)) {
lastRecDebounce = millis();
lastRecBtn = state;
return true;
}
lastRecBtn = state;
return false;
}
bool playButtonPressed() {
bool state = digitalRead(PLAY_BUTTON);
if (state == LOW && lastPlayBtn == HIGH &&
(millis() - lastPlayDebounce > DEBOUNCE_MS)) {
lastPlayDebounce = millis();
lastPlayBtn = state;
return true;
}
lastPlayBtn = state;
return false;
}
void updateAmbientPlaybackLed() {
unsigned long now = millis();
unsigned long dt = now - ambientLastMs;
if (dt < AMBIENT_MIN_STEP_MS)
return;
ambientLastMs = now;
ambientHue += (uint16_t)((65536UL * dt) / AMBIENT_FULL_CYCLE_MS);
// Slightly soft saturation so colors feel “ambient” vs full neon
statusPixel.setPixelColor(0, statusPixel.ColorHSV(ambientHue, 210, 255));
statusPixel.show();
}
void paintOled() {
if (!oledOk)
return;
oled.clearDisplay();
oled.setTextSize(1);
oled.setTextColor(SSD1306_WHITE);
oled.setCursor(0, 0);
if (isRecording) {
oled.println(F("RECORDING"));
oled.println(recpath);
} else if (!foundPlayFile) {
oled.println(F("No audio file"));
oled.println(F("on SD root"));
} else if (player.paused()) {
oled.println(F("PAUSED"));
oled.println(playfile);
} else if (player.playingMusic) {
oled.println(F("PLAYING"));
oled.println(playfile);
} else {
oled.println(F("STOPPED"));
oled.println(playfile);
}
oled.setCursor(0, 56);
oled.print(F("Vol "));
if (lastVolAtten >= 0) {
int pct = map(lastVolAtten, 255, 0, 0, 100);
oled.print(pct);
oled.println(F("%"));
} else {
oled.println(F("--"));
}
oled.display();
}
// ═════════════════════════════════════════════════════════════════════
void setup() {
Serial.begin(115200);
unsigned long t0 = millis();
while (!Serial && millis() - t0 < 3000) { delay(10); }
Serial.println("\n==========================================");
Serial.println(" OGG RECORDER + PLAYER — Feather RP2040");
Serial.println("==========================================");
pinMode(REC_BUTTON, INPUT_PULLUP);
pinMode(PLAY_BUTTON, INPUT_PULLUP);
analogReadResolution(12);
if (!player.begin()) {
Serial.println("VS1053 failed! Check wiring.");
while (1) delay(100);
}
Serial.println("VS1053 OK");
// FilePlayer.begin() does NOT mount the SD card — Adafruit examples always
// call SD.begin() separately after the codec is up.
if (!SD.begin(CARDCS)) {
Serial.println("SD card failed — insert FAT32 card, check SDCS wiring.");
while (1) delay(100);
}
Serial.println("SD card OK");
Wire.begin();
oledOk = oled.begin(SSD1306_SWITCHCAPVCC, OLED_I2C_ADDR);
if (!oledOk)
Serial.println("OLED not found — check I2C wiring / try OLED_I2C_ADDR 0x3D.");
else {
oled.clearDisplay();
oled.display();
Serial.println("OLED OK");
}
lastVolAtten = -1;
updateVolumeFromPot();
// DREQ interrupts + shared SPI on RP2040 often starve SD reads — poll feedBuffer
// in loop() instead (no useInterrupt).
beginPlayback();
recordEnabledAtMs = millis() + RECORD_ARM_MS;
delay(50);
lastRecBtn = digitalRead(REC_BUTTON);
lastPlayBtn = digitalRead(PLAY_BUTTON);
Serial.println("\nGPIO7 = pause / resume playback");
Serial.println("D13 = start / stop recording (enabled after boot delay)");
#if defined(NEOPIXEL_POWER)
pinMode(NEOPIXEL_POWER, OUTPUT);
digitalWrite(NEOPIXEL_POWER, HIGH);
#endif
statusPixel.begin();
statusPixel.setBrightness(40);
statusPixel.clear();
statusPixel.show();
ambientLastMs = millis();
}
void loop() {
updateVolumeFromPot();
// ── Record button (D13) — stop always allowed; start only after boot arm ──
if (recButtonPressed()) {
if (!isRecording) {
if (millis() < recordEnabledAtMs)
; // still booting — do not start recording
else
startRecording();
} else {
stopRecording();
}
}
// ── Play/pause (GPIO7) — ignored while recording ───────────────
if (!isRecording && playButtonPressed()) {
if (player.playingMusic) {
player.pausePlaying(true);
Serial.println("Paused.");
paintOled();
} else if (player.paused()) {
player.pausePlaying(false);
Serial.println("Resumed.");
paintOled();
} else {
// stopped() after track ended — not paused(); start fresh scan + play
beginPlayback();
}
}
// ── Feed playback (polling — required on RP2040 without DREQ IRQ) ──
if (!isRecording && player.playingMusic) {
player.feedBuffer();
}
// ── NeoPixel: recording blink / playback rainbow / off when idle ──
if (isRecording) {
playbackAmbientLedOn = false;
flushRecordingData();
unsigned long now = millis();
if (now - recBlinkLastMs >= REC_BLINK_MS) {
recBlinkLastMs = now;
recBlinkOn = !recBlinkOn;
if (recBlinkOn)
statusPixel.setPixelColor(0, statusPixel.Color(255, 0, 0));
else
statusPixel.setPixelColor(0, 0);
statusPixel.show();
}
} else if (player.playingMusic && !player.paused()) {
updateAmbientPlaybackLed();
playbackAmbientLedOn = true;
} else {
if (playbackAmbientLedOn) {
statusPixel.clear();
statusPixel.show();
playbackAmbientLedOn = false;
}
}
if (oledOk && millis() - lastOledPaintMs >= OLED_PAINT_MS) {
lastOledPaintMs = millis();
paintOled();
}
}