//----------------------------------------------------------------------------------------------------
//
//   PWAVplayer
//
//   A polyphone WAV player based on ESP32
//
//   (C) 2025,2026 colrhon.org
//   This program is released under the Creative Commons Public License, CC BY-NC-SA 4.0
// 
//   Firmware development with IDF-ESP, hardware is LilyGo TTGO T8 (ESP32-WROVER-E) Board, V1.8
//   Nov 2025: Ported to ESP32-S3
//
//   Some terminology used:
//   A sound file is a file containing a header and audio data.
//   A track is a sound file being currently played, possibly together with other tracks.
//   The mixer takes of each track the foremost sound sample and mixes them to become a DAC value.
//   The fifo-buffer holds DAC values; it is being fed by the mixer while being unfed by the feeder.
//   The feeder periodically sends a DAC value to the DAC.
//

#include <stdio.h>
#include <stdlib.h>
#include <dirent.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/stream_buffer.h"
#include <esp_system.h>
#include "esp_log.h"
#include "driver/sdmmc_host.h"
#include "driver/sdmmc_defs.h"
#include "driver/gptimer.h"
#include "driver/gpio.h"
#include "driver/sdspi_host.h"
#include "sdmmc_cmd.h"
#include "esp_vfs_fat.h"
#include "esp_check.h"
#include "esp_cpu.h"
#include "soc/gpio_struct.h"
#include "rom/ets_sys.h"
#include "esp_app_desc.h"
#include "esp_random.h"

// adjust
#define DEBUG
#include "platform.h"
#include "pgpio.h"
#include "pwav.h"

// Version
// to be extracted to file version.txt and automagically put into firmware by IDF
#define VERSION "0.9.6"
char *gversion = VERSION;

// Local defines
#define MAX_INT16 (32767)
#define MIN_INT16 (-32768)
#define MAX_UINT16 (65535)
#define FPATHLEN 200

// config data
extern uint16_t gconf[];
extern char *gconfsd;

// mount point of sd-card
extern const char mount_point[];

// job queue
StreamBufferHandle_t xpinevt;

static char *glogmk = "WAV";
#define LOG glogmk

// Special ids
#define ID_VERSION  11111
#define ID_I_GROUP  11112
#define ID_IL_GROUP 11113

// Statistics
static struct stac {
    uint32_t isrodac; // FifoOut delivered a value
    uint32_t isrmiss; // FifoOut did not deliver a value
    uint32_t mixxcnt; // Mixer mixed at least 1 sample
} stac;

// Sound file descriptor
#define NSATTR 4
typedef struct sfile {
    struct sfile *next;
    uint16_t id;
    uint8_t attr[NSATTR]; // 4 attributes, no order
    uint16_t vol;
    char fpath[FPATHLEN+1];
} Sfile;

// Sound group member
typedef struct gmember {
    struct gmember *next;
    uint16_t id; // id of sound
} Gmember;

// Sound group descriptor
typedef struct sgroup {
    struct sgroup *next;
    uint16_t id;
    uint8_t attr; // m (=1) or r (=2) flag
    uint8_t nom;  // number of members
    uint8_t nmp;  // next member to be played
    Gmember *first;
    char fname[FPATHLEN+1];
} Sgroup;

// Running track
#define BUFSZ 32 // best buffer size: 16 < BUFSZ < 100
#define NMODES 1
typedef struct track {
    struct track *next;
    int fh;         // file handle
    uint32_t tcnt;  // total samples
    uint32_t rcnt;  // samples remaining
    uint32_t stpos; // start position of pcm data in file
    int16_t buf[BUFSZ];
    uint16_t bufl;  // buffer fill level
    uint16_t bidx;  // buffer index 
    Sfile *sf;
    char mode[NMODES]; // 0=delete
} Track;


//----------------------------------------------------------------------------------------
//
// Status LED control
// For audio boards based on ESP32 S3 only

#ifdef ESP32_S3
extern StreamBufferHandle_t xledevt;

static uint16_t ledon = 0;

void LedSrcActivate(uint16_t state) {
    ledon = state;
}

static void SendStatus(uint16_t cmd, uint16_t arg) {
    Rxcmd xcmd;
    xcmd.cmd = cmd; xcmd.arg = arg;
    if (ledon) xStreamBufferSend(xledevt,&xcmd,sizeof(Rxcmd),1);
}
#else
#define SendStatus(a,b)
#endif

//----------------------------------------------------------------------------------------
//
// Set player sync signal functions
// Sync singnals are being generated by the soundcard and sent back to the MPU, used to synchronize voice fragments and sounds
// This is used by Williams Sys11 and Zaccaria Gen2
//

// array of sync signal functions
static void (*ssfp[SNDFB_MAX])(void);

void RegSyncSig(uint16_t ind, void (*funp)(void)) {
    if (ind > SNDFB_MAX) return;
    if (ind == SNDFB_RESET) for (int i = 0; i < SNDFB_MAX; i++) ssfp[i] = NULL;
    else ssfp[ind] = funp;
}

static inline void CondCall(uint16_t fun) {
    if (ssfp[fun]) ssfp[fun]();
}

//----------------------------------------------------------------------------------------
//
// WAV file
//

// Strip header from WAV file
// quick but not bombensicher
//

static int StripWavHeader(int fh, uint32_t *len, uint32_t *stpos) {
#   define QBUF 400
    char buf[QBUF];
    int n = read(fh,buf,QBUF);
    if (n != QBUF) return 1; // cannot read header
    if (strncmp(buf,"RIFF",4) != 0) return 2; // wrong file format
    if (strncmp(&buf[8],"WAVE",4) != 0) return 3; // wrong file content
    for (int i=0; i<QBUF; i++) {
        if ((buf[i]=='d') && (buf[i+1]=='a') && (buf[i+2]=='t') && (buf[i+3]=='a')) {
            uint32_t *p = (void *)&(buf[i+4]);
            *len = *p;
            lseek(fh,i+8,SEEK_SET);
            *stpos = i+8;
            return 0;
        }
    }
    return 4; // no 'data' chunk found
}


//----------------------------------------------------------------------------------------
//
// Chain of running tracks
//

static Track *tchain = NULL;


static int HasAttribute(Sfile *s, char attr) {
    for (int i = 0; i < NSATTR; i++) if (s->attr[i] == attr) return 1;
    return 0;
}

// Create and insert a new track
//
static Track *NewTrack(Sfile *s) {
    if (s == NULL) return NULL;
    // open file
    int fh = open(s->fpath,O_RDONLY);
    if (fh < 0) {
        ESP_LOGE(LOG,"cannot open file %s",s->fpath);
        return NULL;
    }
    uint32_t tlen,stpos;
    int err = StripWavHeader(fh,&tlen,&stpos);
    if (err != 0) {
        ESP_LOGE(LOG,"not a valid WAV file, error %d -  %s",err,s->fpath);
        return NULL;
    }
    Track *t = (void *)malloc(sizeof(Track));
    t->next = tchain;
    tchain = t;
    t->fh = fh;
    t->tcnt = t->rcnt = tlen/2;
    t->stpos = stpos;
    t->bufl = 0;
    t->bidx = 0;
    t->sf = s; // refer soundfile
    for (int k = 0; k < NMODES; k++) t->mode[k] = 0;

    // sync signal to MPU
    if (HasAttribute(t->sf,'l')) CondCall(SNDFB_LOOP);
    else CondCall(SNDFB_BEGIN);
    return t;
}

// Step through the track chain and remove all tracks which are marked for deletion
//
static void DeleteTracks(void) {
    for (Track **p = &tchain; *p != NULL; ) {
        Track *t = *p;
        if (t->mode[0] == 1) {
            close(t->fh);
            *p = t->next;
            free(t);
        }
        else {
            p = &(t->next);
        }
    }
}

// Mark tracks for deletion
//

static void MarkTracksById(int16_t id) {
    // id < 0: hard kill, mark all tracks
    for (Track *p = tchain; p != NULL; p = p->next) {
        if (id < 0) p->mode[0] = 1;
        else if (p->sf->id == (uint16_t)id) p->mode[0] = 1;
    }
}

static void MarkTracksByAttr(uint8_t inv, char *attr) {
    // inv == 0: mark all tracks having one of the attr
    // inv > 0: mark all tracks not having one of the attr 
    for (Track *p = tchain; p != NULL; p = p->next) {
        uint16_t vv = 0;
        for (char *q = attr; *q != 0; q++) if (HasAttribute(p->sf,*q)) vv++;
        if (inv) {
            if (vv == 0) p->mode[0] = 1;
        } else {
            if (vv > 0) p->mode[0] = 1;
        }
    }
}

static void PrintTracks(void) {
    ets_printf("Track list:\n");
    for (Track *p = tchain; p != NULL; p = p->next) {
        ets_printf("  Track: %s\n",p->sf->fpath);
    }
}

static int NoTrack(void) {
    return(tchain == NULL);
}

// Return true if there are no finite tracks, i.e. ignore looping tracks
// 
static int NoFiniteTracks(void) {
    for (Track *p = tchain; p != NULL; p = p->next) {
        if (! HasAttribute(p->sf,'l')) return 0;
    }
    return 1;
}


//----------------------------------------------------------------------------------------
//
// Fifo buffer
//

// Fifo is a ring buffer holding precalculated DAC values.
// For a smooth data stream to the DAC a buffer size of 1024 is required.
// 
//#define FIFO_SIZE 512 // must be a value 2^n (512, 1024, 2048, ..)
#define FIFO_SIZE 1024 // must be a value 2^n (512, 1024, 2048, ..)
#define FIFO_MASK (FIFO_SIZE-1)

struct Fifo {
    uint16_t data[FIFO_SIZE];
    uint16_t read;   // points to oldest entry
    uint16_t write;  // points to next empty slot
} fifo = {{}, 0, 0};

// Put value into the buffer, returns 0 if buffer is full
//
static inline uint8_t FifoIn(uint16_t mxval) {
    uint16_t next = ((fifo.write + 1) & FIFO_MASK);
    if (fifo.read == next) return 0;
    fifo.data[fifo.write] = mxval;
    fifo.write = next;
    return 1;
}

// Get a value from the buffer, return 0 if buffer empty
//
static inline uint8_t FifoOut(uint16_t *p) {
    if (fifo.read == fifo.write) return 0;
    *p = fifo.data[fifo.read];
    fifo.read = (fifo.read+1) & FIFO_MASK;
    return 1;
}

static inline uint8_t FifoFull() {
    uint16_t next = ((fifo.write + 1) & FIFO_MASK);
    return (fifo.read == next);
}

// Return the filling level of the buffer
//
static inline uint16_t FifoLevel(void) {
    if (fifo.write >= fifo.read) return fifo.write-fifo.read;
    else return FIFO_SIZE-(fifo.read-fifo.write);
}


//----------------------------------------------------------------------------------------
//
// Mixer
//

// return 1 to cause deletion afterwards
//
static void StartBackgroundSound(void);
//
static int TryCloseTrack(Track *p) {
    if (HasAttribute(p->sf,'i')) {
        // try start another background sound
        // this will only be successful if the ID_IL_GROUP is not empty
        StartBackgroundSound();
    }
    else if (HasAttribute(p->sf,'l')) {
        // file marked with attribute 'l' (loop)
        lseek(p->fh,p->stpos,SEEK_SET);
        p->rcnt = p->tcnt;
        p->bufl = 0;
        p->bidx = 0;
        return 0;
    }
    // close down
    p->mode[0] = 1;
    return 1;
}

// Step through the track chain and mix one sample of each track
// There are different methods to mix, see configuration 'mix'
// Return 1 if chain contains an entry to be deleted, 0 otherwise
//
static uint8_t RunMixer() {

    if (FifoFull()) return 0; // nothing to do

    int cnt = 0;
    uint8_t touch = 1;
    uint8_t dflag = 0;
    int32_t tval = 0;
    for (Track *p = tchain; p != NULL; p = p->next) {
        if (p->mode[0]) dflag = 1;
        else if (p->rcnt == 0) dflag = TryCloseTrack(p);
        else {
            int32_t val = 0;
            if (p->bidx < p->bufl) {
                val = (int32_t)p->buf[p->bidx];
                p->bidx++;
                p->rcnt--;
            }
            else {
                // refill buffer from file
                ssize_t bcnt = read(p->fh,&(p->buf[0]),BUFSZ*sizeof(int16_t));
                if (bcnt == 0) { // eof
                    dflag = TryCloseTrack(p);
                }
                else if (bcnt < 0) { // read error
                    p->mode[0] = 1;
                    dflag = 1;
                }
                else { // continue
                    p->bufl = bcnt/sizeof(int16_t);
                    val = (int32_t)p->buf[0];
                    p->bidx = 1;
                    p->rcnt--;
                }
            }

            // adjust volume
            if (p->sf->vol != 100) val = (val * p->sf->vol)/100;

            // mixer method
            switch (gconf[CONF_MIX]) {
            case CONF_MIX_SUM:
                tval += val;
                break;
            case CONF_MIX_DIV2:
                tval += val/2;
                break;
            case CONF_MIX_SQRT:
                tval += val;
                cnt++;
                break;
            }

            // statistics
            if (touch) {
                stac.mixxcnt++;
                touch = 0;
            }
        }
    }

    // xxxxx may remove this mixing method
    if (gconf[CONF_MIX] == CONF_MIX_SQRT) {
        switch (cnt) {
        case 0: case 1:  // do nothing
            break;
        case 2:  // div by 1.4
            tval = (10*tval)/14;
            break;
        case 3:  // div by 1.7
            tval = (10*tval)/17;
            break;
        default: // div by 2
            tval = tval/2;
            break;
        }
    }
    
    // clip to int16_t range, add 1/2 range to make all positive,
    // then convert from int32 to uint16 and insert in Fifo
    //
    if (tval > MAX_INT16) tval = MAX_INT16; // clip volume
    if (tval < MIN_INT16) tval = MIN_INT16; // clip volume
    tval += -(MIN_INT16);
    FifoIn((uint16_t)tval);
    return dflag;
}

//
// Experimental
// Add a new track to the pre-calculated DAC values
// Accesses fifo-buffer w/out abstraction => needs improvement, tbd 
//
static void CorrFifo(Track *p) {
    uint16_t k;
    int16_t buf[FIFO_SIZE];

    if (p == NULL) return;
    k = FifoLevel();
    ssize_t bcnt = read(p->fh,&(buf[0]),k*sizeof(int16_t)); 
    int32_t tval;
    uint16_t j = fifo.read;
    for (uint16_t i = 0; i < bcnt/sizeof(uint16_t); i++) {
        tval = fifo.data[j];
        switch (gconf[CONF_MIX]) {
        case CONF_MIX_SUM:
            tval += buf[i];
            break;
        case CONF_MIX_DIV2:
            tval += buf[i]/2;
            break;
        case CONF_MIX_SQRT:
            tval += buf[i]/2; //cheating
            break;
        }
        if (tval > MAX_UINT16) tval = MAX_UINT16; // clip volume
        if (tval < 0) tval = 0; // clip volume
        fifo.data[j] = (uint16_t)tval;
        j = (j+1)&FIFO_MASK;
    }
    p->rcnt -= bcnt/sizeof(uint16_t);
}


//----------------------------------------------------------------------------------------
//
// DAC
//
// DAC transfer based on bitbanging GPIO
//
// The bitbang version is much faster then the SPI bus version, see below
// Duration of interrupt: 2.7us (vs 12.5us with SPI)
//
// Versions using SPI3 and spi_device_polling_transmit() turned out to have
// too much overhead transmitting small data chunks; initiating the transfer
// with spi_device_polling_transmit() takes 8us, exit (after transfer) another 3us,
// a total of 11us overhead, see scope screenshot sd-01.png
// Also see
// - https://esp32.com/viewtopic.php?t=10546
// - https://esp32.com/viewtopic.php?t=24774
// - https://esp32.com/viewtopic.php?t=25417
// - https://esp32.com/viewtopic.php?t=8720
// all same problem, no solution

static void InitExtDAC(void) {

    // configure GPIO for output
    gpio_config_t io_conf = {0};
    io_conf.intr_type = GPIO_INTR_DISABLE;
    io_conf.mode = GPIO_MODE_OUTPUT;
    io_conf.pin_bit_mask = (1ULL << DAC_MOSI)|(1ULL << DAC_CLK)|(1ULL << DAC_CS);
    io_conf.pull_down_en = GPIO_PULLDOWN_DISABLE;
    io_conf.pull_up_en = GPIO_PULLUP_DISABLE; // remove
    ESP_ERROR_CHECK(gpio_config(&io_conf));

    gpio_set_level(DAC_CS,1);
    gpio_set_level(DAC_MOSI,0);
    gpio_set_level(DAC_CLK,0);
}

#ifdef ESP32_WROVER
static inline void LoadExtDAC(uint16_t val) {

// execution time for LoadExtDAC():
// - using high level function gpio_set_level() => 12us => too slow
// - output to register GPIO.out_w1tc.val => 3.4us 
// Period is 22.5us (44.1kHz sample rate)
    
//  TP_SET();
    GPIO.out_w1tc = 1UL << DAC_CS; // CS low
    for (uint8_t i=0; i<16; i++) { 
        if (val & 0x8000) GPIO.out_w1ts = 1UL << DAC_MOSI;
        else GPIO.out_w1tc = 1UL << DAC_MOSI;
        GPIO.out_w1ts = 1UL << DAC_CLK;
        GPIO.out_w1tc = 1UL << DAC_CLK;
        val = val<<1;
    }
    GPIO.out_w1ts = 1UL << DAC_CS; // CS high
    GPIO.out_w1tc = 1UL << DAC_MOSI;
    GPIO.out_w1tc = 1UL << DAC_CLK;
//  TP_CLR();
}
#endif

#ifdef ESP32_S3
static inline void LoadExtDAC(uint16_t val) {

// execution time for LoadExtDAC():
// - using high level function gpio_set_level() => 12us => too slow
// - output to register GPIO.out_w1tc.val => 3.4us 
// Period is 22.5us (44.1kHz sample rate)
    
// porting to ESP32-S3
// GPIO.out_w1tc covers bit 0..31
// setting bit 32..63 has to done in the next word, GPIO.out1_w1tc
// this results in a offset of 32 in the gpio number
#define CORRB(x) ((x)-32) 

// TP_SET();
    GPIO.out1_w1tc.val = 1UL << CORRB(DAC_CS); // CS low
    for (uint8_t i=0; i<16; i++) { 
        if (val & 0x8000) GPIO.out_w1ts = 1UL << DAC_MOSI;
        else GPIO.out_w1tc = 1UL << DAC_MOSI;
        GPIO.out1_w1ts.val = 1UL << CORRB(DAC_CLK);
        GPIO.out1_w1tc.val = 1UL << CORRB(DAC_CLK);
        val = val<<1;
    }
    GPIO.out1_w1ts.val = 1UL << CORRB(DAC_CS); // CS high
    GPIO.out_w1tc = 1UL << DAC_MOSI;
    GPIO.out1_w1tc.val = 1UL << CORRB(DAC_CLK);
// TP_CLR();
}
#endif


//----------------------------------------------------------------------------------------
//
// DAC Feeder (ISR)
// Calling period 22.7us (44.1kHz)
// In case fifo-buffer is empty, simply leave DAC to hold its current value 1 tick longer

static bool LoadDAC12_cb(gptimer_handle_t timer, const gptimer_alarm_event_data_t *edata, void *user_ctx) {
    uint16_t mxval = 0;
    if (FifoOut(&mxval)) {
        // 12bit, DAC MCP4821
#       define DAC_CONFIG_BITS 0x3000;
        mxval = (mxval>>4) | DAC_CONFIG_BITS;
        LoadExtDAC(mxval);
        stac.isrodac++;
    }
    else stac.isrmiss++;
    return false;
}

static bool LoadDAC16_cb(gptimer_handle_t timer, const gptimer_alarm_event_data_t *edata, void *user_ctx) {
    uint16_t mxval = 0;
    if (FifoOut(&mxval)) {
        // 16bit
        LoadExtDAC(mxval);
        stac.isrodac++;
    }
    else stac.isrmiss++;
    return false;
}

// Setup periodic interrupt @ 44.1 kHz
//
static void SetupDACFeeder(void) {

    gptimer_handle_t gptimer = NULL;
    gptimer_config_t timer_config = {
        .clk_src = GPTIMER_CLK_SRC_DEFAULT, // select the default clock source
        .direction = GPTIMER_COUNT_UP,      // counting direction is up
//        .resolution_hz = 1 * 1000 * 1000,   // resolution is 1 MHz, i.e., 1 tick equals 1 microsecond
        .resolution_hz = 1 * 1000 * 1058,   // 1 tick equals 0.9452 microsecond
    };
    // create a timer instance
    ESP_ERROR_CHECK(gptimer_new_timer(&timer_config, &gptimer));
    

    gptimer_alarm_config_t alarm_config = {
        .reload_count = 0,      // when the alarm event occurs, the timer will automatically reload to 0
        .alarm_count = 24,      // 44.1 kHz @ res 0.9452 us
        .flags.auto_reload_on_alarm = true, // Enable auto-reload function
    };
    // set the timer's alarm action
    ESP_ERROR_CHECK(gptimer_set_alarm_action(gptimer, &alarm_config));

    // set different callbacks for 12bit or 16bit DAC
    gptimer_event_callbacks_t cbs = {};
    if (gconf[CONF_DAC] == CONF_DAC_12) cbs.on_alarm = LoadDAC12_cb;
    else cbs.on_alarm = LoadDAC16_cb;
    
    // register timer event callback functions
    ESP_ERROR_CHECK(gptimer_register_event_callbacks(gptimer, &cbs, NULL));
    ESP_ERROR_CHECK(gptimer_enable(gptimer));
    ESP_ERROR_CHECK(gptimer_start(gptimer));
}


//----------------------------------------------------------------------------------------
//
// Sound Groups
//

// chain of all groups
static Sgroup *gchain = NULL;

static Sgroup *FindGroup(uint16_t gnum) {
    Sgroup *p = gchain;
    for (; p != NULL; p = p->next) if (p->id == gnum) return p;
    return NULL;
}

// groups for startup sounds and looping background music
//
static Sgroup *CreateSpecialGroups() {
    // create a group for the i flagged sound files (startup sounds)
    Sgroup *p = malloc(sizeof(Sgroup));
    p->id = ID_I_GROUP;
    p->attr = (uint8_t)'m';
    p->nom = 0;
    p->nmp = 0;
    p->first = NULL;
    strcpy(p->fname,"i-group");
    p->next = NULL;
    // create a group for the i & l flagged sound files (background music)
    Sgroup *q = malloc(sizeof(Sgroup));
    q->id = ID_IL_GROUP;
    q->attr = (uint8_t)'m';
    q->nom = 0;
    q->nmp = 0;
    q->first = NULL;
    strcpy(q->fname,"il-group");
    q->next = p;
    return q;
}

static void AppendGmember(Gmember **rp, uint16_t gmem) {
    Gmember *p = malloc(sizeof(Gmember));
    p->id = gmem;
    p->next = NULL;
    Gmember **q = rp;
    for (; *q != NULL; q = &((*q)->next));
    *q = p;
}

static void AddGroupMember(uint16_t gnum, uint16_t gmem) {
    for (Sgroup *p = gchain; p != NULL; p = p->next) {
        if (p->id == gnum) {
            AppendGmember(&(p->first),gmem);
            p->nom++;
            return;
        }
    }
    // silently discard if not found
}

static Sgroup *NewGroup(char *fname) {
    int gnum;
    char  atc;
    Gmember *gmembers = NULL;
    uint8_t nom = 0;
    
    char buffer[FPATHLEN+1];
    strcpy(buffer,fname);
    char *sep = "-";
    char *tok = strtok(buffer,sep);
    if (tok) {
        int d;
        if (sscanf(tok,"%1d%1d%1d%1d",&d,&d,&d,&d) != 4) goto abort;
        if (sscanf(tok,"%d",&gnum) != 1) goto abort;
    }
    else goto abort;
    tok = strtok(NULL,sep);
    if (tok) {
        if (sscanf(tok,"%c",&atc) != 1) goto abort;
    }
    else goto abort;
    tok = strtok(NULL,sep);
    if (tok) {
        int gmem;
        if (sscanf(tok,"%d",&gmem) == 1) {
            AppendGmember(&gmembers,gmem);
            nom++;
            tok = strtok(NULL,sep);
            while (tok) {
                if (sscanf(tok,"%d",&gmem) == 1) {
                    AppendGmember(&gmembers,gmem);
                    nom++;
                    tok = strtok(NULL,sep);
                }
                else break;
            }
        }
        else {
            printf("Empty group => tbd\n");
            // try open the file to read the members
            // tbd
        }
    }
    else goto abort;

    Sgroup *p = malloc(sizeof(Sgroup));
//    strncpy(p->fname,fname,FPATHLEN);
    strcpy(p->fname,fname);
    p->id = gnum;
    p->attr = (uint8_t)atc;
    p->nom = nom;
    p->nmp = 0;
    p->first = gmembers;
    return p;
    
abort:
    // not a valid group expression
    return NULL;
}

static void ResetGroups(void) {
    for (Sgroup *p = gchain; p != NULL; p = p->next) p->nmp = 0;
}

static void IncNextMember(uint16_t gnum) {
    for (Sgroup *p = gchain; p != NULL; p = p->next) {
        if (p->id == gnum) {
            p->nmp = (p->nmp+1) % p->nom;
            return;
        }
    }
}


//----------------------------------------------------------------------------------------
//
// Sound files
//

// chain of all sound files
static Sfile *schain = NULL;

static Sfile *NewSound(char *fpath, uint16_t id, uint8_t a[], uint16_t vol) {
    ESP_LOGI(LOG,"insert sound file %s",fpath);
    Sfile *s = (void *)malloc(sizeof(Sfile));
    s->id = id;
    for (int i = 0; i < NSATTR; i++) s->attr[i] = a[i];
    s->vol = vol;
    if (s->vol > 100) s->vol = 100;
    strncpy(s->fpath,fpath,FPATHLEN);
    s->fpath[FPATHLEN] = 0;
    s->next = NULL;

    // adjust volume
    if (HasAttribute(s,'v')) s->vol = (s->vol * gconf[CONF_VOLV]) / 100;
    else s->vol = (s->vol * gconf[CONF_VOLS]) / 100;

    return s;
}


//----------------------------------------------------------------------------------------
//
// Read catalog of entries (sound files and groups) from SD
// Setup sound chain (schain) and sound group chain (gchain)
//

// Create and insert an entry into schain (sounds) or gchain (groups)
// An entry is either a sound file or a sound group
//
static int16_t InsertEntry(char *fpath, char *fname) {
    char buf[300];
    strcpy(buf,fpath);
    strcat(buf,fname);
    ESP_LOGI(LOG,"consider file %s",buf);

    uint16_t id;
    uint8_t a[4];
    uint16_t vol;

    // file extension
    char *ext = strrchr(fname,'.');
    if (ext == NULL) return -1;
    
    // try sound file
    // Example for the name of a sound file:
    // 0012-xbxx-100-ring-my-bell.wav 
    
    int n = sscanf(fname,"%hu-%c%c%c%c-%hu-%*s",&id,&(a[0]),&(a[1]),&(a[2]),&(a[3]),&vol);
    if ((n == 6) && (strcmp(ext,".wav") == 0)) { 
        // this is a sound file
        Sfile *s = NewSound(buf,id,a,vol);
        if (s) {
            s->next = schain;
            schain = s;

            // yyyyy if file has an i or i&l attribute, add it to its special group
            if (HasAttribute(s,'i')) {
                if (HasAttribute(s,'l')) AddGroupMember(ID_IL_GROUP,id);
                else AddGroupMember(ID_I_GROUP,id);
            }
            return s->id;
        }
        return -1;
    }

    // try sound group
    // Example of a group entry:
    // 0009-m-15-71-12-exit-lane-left.grp
    
    n = sscanf(fname,"%hu-%c-%*s",&id,&(a[0]));
    if ((n == 2) && (strcmp(ext,".grp") == 0)) {
        // this is a sound group
        Sgroup *g = NewGroup(fname);
        if (g) {
            g->next = gchain;
            gchain = g;
            return g->id;
        }
        return -1;
    }
    return -1;
}

static void ReadCatalogFromSD() {
    char fpath0[30];
    sprintf(fpath0, "%s/%s/",mount_point,gconfsd);
    ESP_LOGI(LOG,"read catalog %s",fpath0);
    uint16_t ne = 0;
    DIR *dp;
    struct dirent *ep;
    dp = opendir (fpath0);
    if (dp != NULL) {
        while ((ep = readdir(dp)) != NULL) {
            if (InsertEntry(fpath0,ep->d_name) < 0) continue;
            ne++;
        }
        closedir(dp);
        ESP_LOGI(LOG,"read %u entries",ne);
    }
    else {
        ESP_LOGI(LOG,"read failed %s",fpath0);
    }
}

//----------------------------------------------------------------------------------------
//
// Start playing sound file
//

static Sfile *PickMember(Gmember *first, uint16_t pos) {
//  position is zero-based
    Gmember *m = first;
    for (int16_t i = 0; i < pos; i++) if (m) m = m->next;
    if (m == NULL) return NULL;
    for (Sfile *s = schain; s != NULL; s = s->next) if (s->id == m->id) return s; 
    return NULL;
}

static Sfile *LookupSound(uint16_t id) {
    Sfile *s;
    for (s = schain; s != NULL; s = s->next) if (s->id == id) return s; 
    Sgroup *p;
    for (p = gchain; p != NULL; p = p->next) {
        if (p->id == id) {
            if (p->nom == 0) return NULL;
            if (p->attr == 'm') {
                // random sound
                p->nmp = esp_random() % p->nom;
                s = PickMember(p->first,p->nmp);
                return s;
            }
            if (p->attr == 'r') {
                // next sound
                s = PickMember(p->first,p->nmp);
                p->nmp = (p->nmp+1) % p->nom;
                return s;
            }
            // attr not valid
            return NULL;
        }
    }
    // no match
    return NULL; 
}

// start a background sound
//
static void StartBackgroundSound() {
    Sfile *s = LookupSound(ID_IL_GROUP);
    if (s) NewTrack(s);
}

// initial start of sound files marked with attribute 'i'
//
static void StartInitSound() {
    Sfile *s = LookupSound(ID_I_GROUP);
    if (s == NULL) s = LookupSound(ID_IL_GROUP);
    if (s) {
        NewTrack(s);
        ets_printf("InitSound => sfile %d\n",s->id);
        }
}

// Insert sound as a track into the track list
// Abort silently if something goes wrong
//
static void StartSound(uint16_t id) {

    // ets_printf("Start sound %d\n",id);
    Sfile *s = LookupSound(id);
    if (s == NULL) {
        // sound file not found
        SendStatus('e',0); // error, red led
        return;
    }
    // ets_printf("Lookup sound %d\n",s->id);

    if (HasAttribute(s,'k')) {      // hard kill
        MarkTracksById(-1);         // kill all tracks
        NewTrack(s);
    }
    else if (HasAttribute(s,'c')) { // soft kill
        MarkTracksByAttr(1,"i");    // kill all tracks except init/background
        NewTrack(s);
    }
    else if (HasAttribute(s,'q')) { // kill finite sounds
        MarkTracksByAttr(1,"lv");   // kill all tracks except looping tracks and voice
        NewTrack(s);
    }
    else if (HasAttribute(s,'b')) { // break
        MarkTracksById((int16_t)id);
        NewTrack(s);
    }
    else {
        CorrFifo(NewTrack(s));
    }
}


//----------------------------------------------------------------------------------------
//
// WAV Player
//
//

static void PrintAllStruct(void);

static void ExecCommand(Rxcmd *k) {
    switch (k->cmd) {
    case 'p': // play sound
        SendStatus('a',0); // access, green led
        StartSound(k->arg);
        break;
    case 'k': // kill all sounds
        SendStatus('b',0); // kill, blue led
        MarkTracksById(-1);
        break;
    case 't': // kill all sounds but not the voices and reset nmp of all sound groups
        SendStatus('b',0); // kill, blue led
        MarkTracksByAttr(1,"v"); // kill all but not the voices
        ResetGroups();
        break;
    case 'n': // increase nmp of group
        IncNextMember(k->arg);
        break;
    case 'w': // kill particular sound
        MarkTracksById(k->arg);
        break;
    case 'm': // kill all looping sounds
        SendStatus('b',0); // kill, blue led
        MarkTracksByAttr(0,"l");
        break;
    default:
        break;
    }
}

void WAVPlayer(void *pvParameters) {
    const esp_app_desc_t *ppd = esp_app_get_description();
    printf("WAVPlayer Version: %s\n",ppd->version);

    // conditionally setup sync signal to MPU
    CondCall(SNDFB_INIT);

    // spoken version
    char buf[100];
    sprintf(buf,"%s/spokenvers/version-%c-%c-%c.wav",mount_point,ppd->version[0],ppd->version[2],ppd->version[4]);
    int fh = open(buf,O_RDONLY);
    if (fh >= 0) {
        close(fh);
        uint8_t attr[NSATTR];
        for (int i = 0; i < NSATTR; i++) attr[i] = 'x';
        // Create an entry for the spoken version         
        schain = NewSound(buf,ID_VERSION,attr,100);
        StartSound(ID_VERSION);
    }
    
    gchain = CreateSpecialGroups();
    ReadCatalogFromSD();
    InitExtDAC();
    SetupDACFeeder();

    // play spoken version if linked into schain
    // this is done in its own loop to make sure version comes well before the startup sound
    uint8_t run = 1;
    while (run) {
        if (RunMixer()) DeleteTracks();
        if (FifoFull()) vTaskDelay(1);
        if (NoTrack()) run = 0;
    }
    
#if 0
    //StartSound(16); // you want to play, let's play 'i'
    //StartSound(18); // horn, loop 'l'
    //StartSound(5);  // kill 'k'
    //StartSound(6);  // break 'b'
    //StartSound(7);
    //StartSound(50);  // Fred Wesley, House Party
    //StartSound(101);  // Sinus 1kHz
    //PrintTracks();
#endif

    PrintAllStruct();
    
    // statistics
    stac.isrmiss = 0;
    stac.isrodac = 0;
    stac.mixxcnt = 0;
    StartInitSound();

    // main loop
    Rxcmd xcmd;
    run = 1;
    while (run) {
        if (RunMixer()) DeleteTracks();
        if (NoFiniteTracks()) CondCall(SNDFB_END);
        if (FifoFull()) vTaskDelay(1);
        if (xStreamBufferReceive(xpinevt,&xcmd,sizeof(Rxcmd),0) == sizeof(Rxcmd)) ExecCommand(&xcmd);
        
#if 0
        if (stac.mixxcnt == 200000) StartSound(1);
        if (stac.mixxcnt == 280000) StartSound(2);
        if (stac.mixxcnt == 300000) StartSound(3);
        if (stac.mixxcnt == 320000) StartSound(4);
        if (stac.mixxcnt == 600000) StartSound(5);
        //if ((stac.mixxcnt) > 650000) run=0; // exit
        //if (NoTrack()) run=0; // exit after last track ends
#endif
        
    }
    
}

void WAVDummy(void *pvParameters) {
    const esp_app_desc_t *ppd = esp_app_get_description();
    printf("WAV Dummy, Version: %s\n",ppd->version);

    // inactivate sync signal to MPU
    CondCall(SNDFB_OFF);

    Rxcmd xcmd;
    int run = 1;
    while (run) {
        if (xStreamBufferReceive(xpinevt,&xcmd,sizeof(Rxcmd),0) == sizeof(Rxcmd)){} // do nothing
        vTaskDelay(100);
    }
}

static void PrintStatistics(void) {
    ets_printf("Statistik:\n");
    ets_printf("  ISR odac: %lu (total von Fifo geliefert und an DAC gesendete Werte)\n", stac.isrodac);
    ets_printf("  ISR miss: %lu (total Anzahl Fifo leer)\n", stac.isrmiss);
    ets_printf("  Mix xcnt: %lu (total von Mixer gemischte Werte mit mind. 1 Track\n", stac.mixxcnt);
    ets_printf("  odac + miss = total DAC Feeder Aufrufe\n");
}

static void PrintAllStruct(void) {
    ets_printf("Soundfile list:\n");
    for (Sfile *s = schain; s != NULL; s = s->next) ets_printf("  Sound: %d (%s)\n",s->id,s->fpath);
    ets_printf("Group list:\n");
    for (Sgroup *s = gchain; s != NULL; s = s->next) {
        ets_printf("  Group: %d (%s)\n",s->id,s->fname);
        for (Gmember *q = s->first; q != NULL; q = q->next) {
            ets_printf("    Member: %d\n",q->id);
        }
    }
    ets_printf("End.\n");
}

