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
4 changes: 3 additions & 1 deletion platformio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -659,7 +659,9 @@ board = lolin_s3_mini ;; -S3 mini, 4MB flash 2MB PSRAM
platform = ${esp32s3.platform}
platform_packages = ${esp32s3.platform_packages}
upload_speed = 921600
custom_usermods = audioreactive
custom_usermods =
audioreactive
FSEQ
build_unflags = ${common.build_unflags}
build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=\"ESP32-S3_4M_qspi\"
-DARDUINO_USB_CDC_ON_BOOT=1 ;; -DARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB")
Expand Down
59 changes: 59 additions & 0 deletions usermods/FSEQ/fseq_effect.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
#pragma once

#include "wled.h"
#include "fseq_player.h"

//
// FSEQ Player effect – renders FSEQ frame data into the active SEGMENT.
//
// File selection follows the same pattern as the built-in Image effect:
// set the **segment name** to the .fseq filename on the SD card
// (e.g. "show.fseq"). The effect prepends "/" and opens the file.
//
// Check1 enables loop mode (repeat the sequence forever).
//

// Stored per-effect so we can detect when the segment name changes.
static char _fseq_lastName[WLED_MAX_SEGNAME_LEN + 2] = "";

static void mode_fseq_player(void) {
// Build the target filename from the segment name
const char *segName = SEGMENT.name;

// No name set – show static colour
if (!segName || segName[0] == '\0') {
if (FSEQPlayer::isPlaying()) FSEQPlayer::clearLastPlayback();
SEGMENT.fill(SEGCOLOR(0));
return;
}

// Detect segment-name change → (re)load the file
if (strncmp(_fseq_lastName, segName, WLED_MAX_SEGNAME_LEN) != 0) {
strncpy(_fseq_lastName, segName, WLED_MAX_SEGNAME_LEN);
_fseq_lastName[WLED_MAX_SEGNAME_LEN] = '\0';

// Build "/<name>" path
char path[WLED_MAX_SEGNAME_LEN + 2];
path[0] = '/';
strncpy(path + 1, segName, WLED_MAX_SEGNAME_LEN);
path[WLED_MAX_SEGNAME_LEN + 1] = '\0';

bool loop = SEGMENT.check1;
FSEQPlayer::loadRecording(path, 0.0f, loop);
}

if (!FSEQPlayer::isPlaying()) {
SEGMENT.fill(SEGCOLOR(0));
return;
}

// Keep loop state in sync with the UI checkbox
FSEQPlayer::setLooping(SEGMENT.check1);

// Render the current frame into the segment
FSEQPlayer::renderFrameToSegment();
}

static const char _data_FX_MODE_FSEQ_PLAYER[] PROGMEM =
"FSEQ Player@,,,,,Loop;;!;1";

68 changes: 23 additions & 45 deletions usermods/FSEQ/fseq_player.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,9 @@
#include "SD_MMC.h"
#endif

// Static member definitions moved from header to avoid multiple definition
// errors
const char UsermodFseq::_name[] PROGMEM = "usermod FSEQ sd card";

#ifdef WLED_USE_SD_SPI
int8_t UsermodFseq::configPinSourceSelect = 5;
int8_t UsermodFseq::configPinSourceClock = 18;
int8_t UsermodFseq::configPinPoci = 19;
int8_t UsermodFseq::configPinPico = 23;
#endif
// Static member definitions
const char UsermodFseq::_name[] PROGMEM = "FSEQ";
uint8_t UsermodFseq::fseqEffectId = 0;

File FSEQPlayer::recordingFile;
String FSEQPlayer::currentFileName = "";
Expand All @@ -29,8 +22,6 @@ uint8_t FSEQPlayer::colorChannels = 3;
int32_t FSEQPlayer::recordingRepeats = RECORDING_REPEAT_DEFAULT;
uint32_t FSEQPlayer::now = 0;
uint32_t FSEQPlayer::next_time = 0;
uint16_t FSEQPlayer::playbackLedStart = 0;
uint16_t FSEQPlayer::playbackLedStop = uint16_t(-1);
uint32_t FSEQPlayer::frame = 0;
uint16_t FSEQPlayer::buffer_size = 48;
FSEQPlayer::FileHeader FSEQPlayer::file_header;
Expand Down Expand Up @@ -86,25 +77,26 @@ void FSEQPlayer::printHeaderInfo() {

void FSEQPlayer::processFrameData() {
uint32_t packetLength = file_header.channel_count;
uint16_t lastLed =
min((uint32_t)playbackLedStop, (uint32_t)playbackLedStart + (packetLength / 3));
char frame_data[48]; // fixed size; buffer_size is always 48
uint16_t segLen = SEGLEN; // number of virtual pixels in the current segment
uint16_t maxLeds = min((uint32_t)segLen, packetLength / 3);
char frame_data[48]; // fixed size; buffer_size is always 48
CRGB *crgb = reinterpret_cast<CRGB *>(frame_data);
uint32_t bytes_remaining = packetLength;
uint16_t index = playbackLedStart;
while (index < lastLed && bytes_remaining > 0) {
uint16_t index = 0;
while (index < maxLeds && bytes_remaining > 0) {
uint16_t length = (uint16_t)min(bytes_remaining, (uint32_t)sizeof(frame_data));
recordingFile.readBytes(frame_data, length);
bytes_remaining -= length;
for (uint16_t offset = 0; offset < length / 3; offset++) {
setRealtimePixel(index, crgb[offset].r, crgb[offset].g, crgb[offset].b,
0);
if (++index > lastLed)
SEGMENT.setPixelColor(index, RGBW32(crgb[offset].r, crgb[offset].g, crgb[offset].b, 0));
if (++index >= maxLeds)
break;
}
}
strip.show();
realtimeLock(3000, REALTIME_MODE_FSEQ);
// Fill remaining segment pixels with black if the file has fewer channels
for (uint16_t i = index; i < segLen; i++) {
SEGMENT.setPixelColor(i, BLACK);
}
next_time = now + file_header.step_time;
}

Expand All @@ -127,8 +119,7 @@ bool FSEQPlayer::stopBecauseAtTheEnd() {
return false;
}

DEBUG_PRINTLN("Finished playing recording, disabling realtime mode");
realtimeLock(10, REALTIME_MODE_INACTIVE);
DEBUG_PRINTLN("Finished playing recording");
clearLastPlayback();
return true;
}
Expand All @@ -150,33 +141,23 @@ void FSEQPlayer::playNextRecordingFrame() {
processFrameData();
}

void FSEQPlayer::handlePlayRecording() {
void FSEQPlayer::renderFrameToSegment() {
now = millis();
if (realtimeMode != REALTIME_MODE_FSEQ)
if (!isPlaying())
return;
if (now < next_time)
return;
playNextRecordingFrame();
}

void FSEQPlayer::loadRecording(const char *filepath,
uint16_t startLed,
uint16_t stopLed,
float secondsElapsed,
bool loop)
{
if (recordingFile.available()) {
clearLastPlayback();
}
playbackLedStart = startLed;
playbackLedStop = stopLed;
if (playbackLedStart == uint16_t(-1) || playbackLedStop == uint16_t(-1)) {
Segment sg = strip.getSegment(-1);
playbackLedStart = sg.start;
playbackLedStop = sg.stop;
}
DEBUG_PRINTF("FSEQ load animation on LED %d to %d\n", playbackLedStart,
playbackLedStop);
DEBUG_PRINTF("FSEQ load animation: %s\n", filepath);
if (fileOnSD(filepath)) {
DEBUG_PRINTF("Read file from SD: %s\n", filepath);
recordingFile = SD_ADAPTER.open(filepath, "rb");
Expand All @@ -190,8 +171,7 @@ void FSEQPlayer::loadRecording(const char *filepath,
if (currentFileName.startsWith("/"))
currentFileName = currentFileName.substring(1);
} else {
DEBUG_PRINTF("File %s not found (%s)\n", filepath,
USED_STORAGE_FILESYSTEMS);
DEBUG_PRINTF("File %s not found on SD or FS\n", filepath);
return;
}
if ((uint64_t)recordingFile.available() < sizeof(file_header)) {
Expand Down Expand Up @@ -232,9 +212,6 @@ void FSEQPlayer::loadRecording(const char *filepath,
file_header.step_time, FSEQ_DEFAULT_STEP_TIME);
file_header.step_time = FSEQ_DEFAULT_STEP_TIME;
}
if (realtimeOverride == REALTIME_OVERRIDE_ONCE) {
realtimeOverride = REALTIME_OVERRIDE_NONE;
}
frame = (uint32_t)((secondsElapsed * 1000.0f) / file_header.step_time);
if (frame >= file_header.frame_count) {
frame = file_header.frame_count - 1;
Expand All @@ -249,9 +226,6 @@ void FSEQPlayer::loadRecording(const char *filepath,
}

void FSEQPlayer::clearLastPlayback() {
for (uint16_t i = playbackLedStart; i < playbackLedStop; i++) {
setRealtimePixel(i, 0, 0, 0, 0);
}
frame = 0;
recordingFile.close();
currentFileName = "";
Expand All @@ -261,6 +235,10 @@ bool FSEQPlayer::isPlaying() {
return recordingFile && frame < file_header.frame_count;
}

void FSEQPlayer::setLooping(bool loop) {
recordingRepeats = loop ? RECORDING_REPEAT_LOOP : RECORDING_REPEAT_DEFAULT;
}

String FSEQPlayer::getFileName() { return currentFileName; }

float FSEQPlayer::getElapsedSeconds() {
Expand Down
10 changes: 5 additions & 5 deletions usermods/FSEQ/fseq_player.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,17 +31,19 @@ class FSEQPlayer {
};

static void loadRecording(const char *filepath,
uint16_t startLed,
uint16_t stopLed,
float secondsElapsed = 0.0f,
bool loop = false);
static void handlePlayRecording();
static void clearLastPlayback();
static void syncPlayback(float secondsElapsed);
static bool isPlaying();
static void setLooping(bool loop);
static String getFileName();
static float getElapsedSeconds();

// Called from the WLED effect function – renders the current frame into
// the active SEGMENT using SEGMENT.setPixelColor().
static void renderFrameToSegment();

private:
FSEQPlayer() {}

Expand All @@ -54,8 +56,6 @@ class FSEQPlayer {
static int32_t recordingRepeats;
static uint32_t now;
static uint32_t next_time;
static uint16_t playbackLedStart;
static uint16_t playbackLedStop;
static uint32_t frame;
static uint16_t buffer_size;
static FileHeader file_header;
Expand Down
21 changes: 0 additions & 21 deletions usermods/FSEQ/sd_manager.cpp

This file was deleted.

21 changes: 0 additions & 21 deletions usermods/FSEQ/sd_manager.h

This file was deleted.

52 changes: 23 additions & 29 deletions usermods/FSEQ/usermod_fpp.h
Original file line number Diff line number Diff line change
Expand Up @@ -486,14 +486,32 @@ void sendPingPacket(IPAddress destination = IPAddress(255, 255, 255, 255)) {
case CTRL_PKT_BLANK:
DEBUG_PRINTLN(F("[FPP] Received UDP blank packet"));
FSEQPlayer::clearLastPlayback();
realtimeLock(10, REALTIME_MODE_INACTIVE);
break;
default:
DEBUG_PRINTLN(F("[FPP] Unknown UDP packet type"));
break;
}
}

// Switch the main segment to the FSEQ Player effect and set its name
// to the given filename so the effect knows which file to play.
void activateFseqEffect(const String &fileName) {
uint8_t fxId = UsermodFseq::fseqEffectId;
if (fxId == 0) return; // effect not registered

Segment &seg = strip.getMainSegment();
// Strip leading '/' for the segment name (effect prepends it)
const char *nameStr = fileName.c_str();
if (nameStr[0] == '/') nameStr++;
seg.setName(nameStr);

if (seg.mode != fxId) {
seg.setMode(fxId);
seg.check1 = true; // enable looping for sync playback
stateChanged = true;
}
}

// Process sync command with detailed debug output
void ProcessSyncPacket(uint8_t action, String fileName,
float secondsElapsed) {
Expand All @@ -510,21 +528,17 @@ void sendPingPacket(IPAddress destination = IPAddress(255, 255, 255, 255)) {

switch (action) {
case 0: // SYNC_PKT_START
FSEQPlayer::loadRecording(fileName.c_str(), 0, strip.getLength(),
secondsElapsed);
activateFseqEffect(fileName);
FSEQPlayer::loadRecording(fileName.c_str(), secondsElapsed);
break;
case 1: // SYNC_PKT_STOP
FSEQPlayer::clearLastPlayback();
realtimeLock(10, REALTIME_MODE_INACTIVE);
break;
case 2: // SYNC_PKT_SYNC
DEBUG_PRINTLN(F("[FPP] ProcessSyncPacket: Sync command received"));
DEBUG_PRINTF("[FPP] Sync Packet - FileName: %s, Seconds Elapsed: %.2f\n",
fileName.c_str(), secondsElapsed);
if (!FSEQPlayer::isPlaying()) {
DEBUG_PRINTLN(F("[FPP] Sync: Playback not active, starting playback."));
FSEQPlayer::loadRecording(fileName.c_str(), 0, strip.getLength(),
secondsElapsed);
activateFseqEffect(fileName);
FSEQPlayer::loadRecording(fileName.c_str(), secondsElapsed);
} else {
FSEQPlayer::syncPlayback(secondsElapsed);
}
Expand Down Expand Up @@ -682,26 +696,6 @@ void sendPingPacket(IPAddress destination = IPAddress(255, 255, 255, 255)) {
request->send(200, "application/json", json);
});

// Endpoint to start FSEQ playback
server.on("/fpp/connect", HTTP_GET, [this](AsyncWebServerRequest *request) {
if (!request->hasArg("file")) {
request->send(400, "text/plain", "Missing 'file' parameter");
return;
}
String filepath = request->arg("file");
if (!filepath.startsWith("/")) {
filepath = "/" + filepath;
}
// Use FSEQPlayer to start playback
FSEQPlayer::loadRecording(filepath.c_str(), 0, strip.getLength());
request->send(200, "text/plain", "FPP connect started: " + filepath);
});
// Endpoint to stop FSEQ playback
server.on("/fpp/stop", HTTP_GET, [this](AsyncWebServerRequest *request) {
FSEQPlayer::clearLastPlayback();
realtimeLock(10, REALTIME_MODE_INACTIVE);
request->send(200, "text/plain", "FPP connect stopped");
});

// Initialize UDP listener for synchronization and ping
if (!udpStarted && (WiFi.status() == WL_CONNECTED)) {
Expand Down
Loading