Zum Inhalt

wav_player.tc

WAV Player — I2S audio output with optional WM8960 codec

Source on GitHub

// WAV Player — I2S audio output with optional WM8960 codec
// Works with any I2S DAC (MAX98357A, PCM5102, UDA1334, etc.)
// If WM8960 is detected on I2C, volume control is enabled
//
// Commands: WAVtone <freq>  — play sine tone for 1 second
//           WAVplay <path>  — play 16-bit mono/stereo WAV file
//
// Upload 16-bit WAV files to device filesystem

// Default I2S pins — changeable via WebUI (persist across reboot)
persist int pin_bclk = 10;
persist int pin_lrclk = 18;
persist int pin_dout = 17;
persist int pin_rate = 16000;
persist int dac_vol = 255;     // 0-255: codec=HW volume, no codec=SW scaling

// Audio buffer — global for heap allocation
#define BUF_SAMPLES 512
int pcm[BUF_SAMPLES];

// WAV header fields
int wav_rate;
int wav_bits;
int wav_channels;
int wav_data_size;

// State
int playing = 0;
int codec_ok = 0;
int wm_bus = -1;   // detected I2C bus (-1 = no codec)

// Request state for TaskLoop (set by Command/Every50ms, consumed by TaskLoop)
// req_action: 0=idle, 1=tone, 2=play WAV
int req_action = 0;
int req_freq = 1000;
char req_file[64];

// ═══════════════════════════════════════════════════════════
// WM8960 codec driver (optional — only if detected on I2C)
// ═══════════════════════════════════════════════════════════

#define WM_ADDR  0x1A

// WM8960 uses 7-bit register address + 9-bit data in a 2-byte I2C write
void wmWrite(int reg, int data) {
    int b1 = (reg << 1) | ((data >> 8) & 1);
    int b2 = data & 0xFF;
    i2cWrite8(WM_ADDR, b1, b2, wm_bus);
}

int wmInit() {
    // Scan both I2C buses for WM8960
    if (i2cExists(WM_ADDR, 0)) {
        wm_bus = 0;
    } else if (i2cExists(WM_ADDR, 1)) {
        wm_bus = 1;
    } else {
        return -1;
    }

    // Claim I2C address
    i2cSetDevice(WM_ADDR, wm_bus);
    i2cSetActiveFound(WM_ADDR, "WM8960", wm_bus);

    // Reset all registers
    wmWrite(0x0F, 0x0000);
    delay(10);

    // Power management 1: VREF, VMIDSEL=50k, ADC L+R, AINL+AINR, MICB
    wmWrite(0x19, (1<<8)|(1<<7)|(1<<6)|(1<<5)|(1<<4)|(1<<3)|(1<<2)|(1<<1));
    // Power management 2: DAC L+R, LOUT1, ROUT1, SPK L+R
    wmWrite(0x1A, (1<<8)|(1<<7)|(1<<6)|(1<<5)|(1<<4)|(1<<3));
    // Power management 3: LOMIX, ROMIX, LMIC, RMIC
    wmWrite(0x2F, (1<<5)|(1<<4)|(1<<3)|(1<<2));

    // Clocking 1: default (MCLK source, no dividers)
    wmWrite(0x04, 0x0000);
    // ADC/DAC control 1: no de-emphasis, no mute
    wmWrite(0x05, 0x0000);
    // Audio interface: I2S format, 16-bit word length, slave mode
    wmWrite(0x07, 0x0022);

    // Headphone output volume L+R: +6dB (127=0x7F) with volume update
    wmWrite(0x02, 0x017F);
    wmWrite(0x03, 0x017F);
    // Speaker output volume L+R: +6dB (0x7F) with volume update
    wmWrite(0x28, 0x017F);
    wmWrite(0x29, 0x017F);
    // Class D control: enable BOTH L+R speakers
    wmWrite(0x31, 0x00C0);
    // Class D boost: max DC and AC gain (+13.5dB)
    wmWrite(0x33, 0x003B);

    // DAC volume L+R: 0dB (0xFF) with volume update
    wmWrite(0x0A, 0x01FF);
    wmWrite(0x0B, 0x01FF);

    // Output mixer: DAC to output ONLY (bit8=LD2LO/RD2RO)
    // Do NOT set bit7 (LI2LO/RI2RO) — routes floating input to output
    wmWrite(0x22, (1<<8));
    wmWrite(0x25, (1<<8));

    // Input volume L+R
    wmWrite(0x00, 0x0127);
    wmWrite(0x01, 0x0127);
    // ADC volume L+R
    wmWrite(0x15, 0x01C3);
    wmWrite(0x16, 0x01C3);
    // Disable bypass
    wmWrite(0x2D, 0x0000);
    wmWrite(0x2E, 0x0000);
    // Input boost
    wmWrite(0x20, 0x0020 | (1<<8) | (1<<3));
    wmWrite(0x21, 0x0020 | (1<<8) | (1<<3));

    addLog("WM8960: initialized");
    return 0;
}

// Set DAC output volume — maps slider 0-255 to usable register range
// WM8960 DAC reg: 255=0dB, each step=0.5dB, below ~200 is inaudible
// Slider 0=mute, 1-255 maps to register 200-255 (-27.5dB to 0dB)
void wmSetVolume(int vol) {
    if (vol <= 0) {
        wmWrite(0x0A, 0x0100);
        wmWrite(0x0B, 0x0100);
        return;
    }
    int reg = 200 + (vol * 55 / 255);
    if (reg > 255) reg = 255;
    wmWrite(0x0A, 0x0100 | reg);
    wmWrite(0x0B, 0x0100 | reg);
}

// Software volume (no-codec mode only): scale pcm[] by dac_vol/256
void applyVolume(int len) {
    if (codec_ok) return;     // codec uses hardware DAC register
    if (dac_vol >= 255) return;
    int i;
    for (i = 0; i < len; i++) {
        pcm[i] = (pcm[i] * dac_vol) / 256;
    }
}

// ═══════════════════════════════════════════════════════════
// WAV file playback (called from TaskLoop — no instruction limit)
// ═══════════════════════════════════════════════════════════

int parseWavHeader(int f) {
    char hdr[12];
    int n = fileRead(f, hdr, 12);
    if (n < 12) return -1;

    // Check RIFF/WAVE
    if (hdr[0] != 'R' || hdr[1] != 'I' || hdr[2] != 'F' || hdr[3] != 'F') return -1;
    if (hdr[8] != 'W' || hdr[9] != 'A' || hdr[10] != 'V' || hdr[11] != 'E') return -1;

    // Walk through chunks to find "fmt " and "data"
    int found_fmt = 0;
    int found_data = 0;
    char ck[8];
    char fmt[16];
    char skip[64];

    while (found_fmt == 0 || found_data == 0) {
        n = fileRead(f, ck, 8);
        if (n < 8) return -1;
        int csz = (ck[4] & 0xFF) | ((ck[5] & 0xFF) << 8) | ((ck[6] & 0xFF) << 16) | ((ck[7] & 0xFF) << 24);

        if (ck[0] == 'f' && ck[1] == 'm' && ck[2] == 't' && ck[3] == ' ') {
            n = fileRead(f, fmt, 16);
            if (n < 16) return -1;
            int audio_fmt = (fmt[0] & 0xFF) | ((fmt[1] & 0xFF) << 8);
            if (audio_fmt != 1) return -1;
            wav_channels = (fmt[2] & 0xFF) | ((fmt[3] & 0xFF) << 8);
            wav_rate = (fmt[4] & 0xFF) | ((fmt[5] & 0xFF) << 8) | ((fmt[6] & 0xFF) << 16) | ((fmt[7] & 0xFF) << 24);
            wav_bits = (fmt[14] & 0xFF) | ((fmt[15] & 0xFF) << 8);
            found_fmt = 1;
            // Skip remainder of fmt chunk
            int rem = csz - 16;
            while (rem > 0) {
                int rd = 64;
                if (rd > rem) rd = rem;
                fileRead(f, skip, rd);
                rem = rem - rd;
            }
        } else if (ck[0] == 'd' && ck[1] == 'a' && ck[2] == 't' && ck[3] == 'a') {
            wav_data_size = csz;
            found_data = 1;
        } else {
            // Skip unknown chunk
            int rem = csz;
            while (rem > 0) {
                int rd = 64;
                if (rd > rem) rd = rem;
                fileRead(f, skip, rd);
                rem = rem - rd;
            }
        }
    }

    return 0;
}

void doPlayWav(char path[]) {
    int f = fileOpen(path, "r");
    if (f < 0) {
        addLog("WAV: file not found");
        return;
    }

    if (parseWavHeader(f) < 0) {
        addLog("WAV: invalid header");
        fileClose(f);
        return;
    }

    if (wav_bits != 16 || (wav_channels != 1 && wav_channels != 2)) {
        addLog("WAV: need 16-bit mono/stereo");
        fileClose(f);
        return;
    }

    // Always output at pin_rate — upsample if WAV rate is lower
    int ratio = pin_rate / wav_rate;
    if (ratio < 1) ratio = 1;
    if (ratio > 4) ratio = 4;

    int err = i2sBegin(pin_bclk, pin_lrclk, pin_dout, pin_rate);
    if (err < 0) {
        addLog("WAV: i2sBegin failed");
        fileClose(f);
        return;
    }

    if (codec_ok) wmSetVolume(dac_vol);

    char msg[64];
    sprintf(msg, "WAV: %dHz ratio %d %dch %d bytes", wav_rate, ratio, wav_channels, wav_data_size);
    addLog(msg);
    playing = 1;

    int bytes_per_frame = wav_channels * 2;  // 2 or 4 bytes per frame
    int remaining = wav_data_size / bytes_per_frame;  // remaining in frames
    int read_chunk = BUF_SAMPLES / ratio;  // read fewer samples, expand to fill buffer

    while (remaining > 0) {
        int chunk = read_chunk;
        if (chunk > remaining) chunk = remaining;

        // Native C read + int16-to-int32 conversion
        int frames = fileReadPCM16(f, pcm, chunk, wav_channels);
        if (frames <= 0) break;

        // Upsample in-place (backwards to avoid overwrite)
        if (ratio > 1) {
            int i;
            for (i = frames - 1; i >= 0; i--) {
                int j;
                for (j = 0; j < ratio; j++) {
                    pcm[i * ratio + j] = pcm[i];
                }
            }
            frames = frames * ratio;
        }

        applyVolume(frames);
        i2sWrite(pcm, frames);
        remaining = remaining - chunk;
    }

    fileClose(f);
    i2sStop();
    playing = 0;
    addLog("WAV: done");
}

// Generate a sine wave test tone (called from TaskLoop — no instruction limit)
void doPlayTone(int freq, int duration_ms, int volume) {
    int err = i2sBegin(pin_bclk, pin_lrclk, pin_dout, pin_rate);
    if (err < 0) {
        addLog("i2sBegin failed");
        return;
    }

    if (codec_ok) wmSetVolume(dac_vol);
    playing = 1;

    int total = pin_rate * duration_ms / 1000;
    float phase = 0.0;
    float step = 6.2832 * freq / pin_rate;  // 2*PI * freq / sampleRate

    while (total > 0) {
        int chunk = BUF_SAMPLES;
        if (chunk > total) chunk = total;

        int i;
        for (i = 0; i < chunk; i++) {
            pcm[i] = sin(phase) * volume;
            phase = phase + step;
            if (phase >= 6.2832) phase = phase - 6.2832;
        }

        applyVolume(chunk);
        i2sWrite(pcm, chunk);
        total = total - chunk;
    }

    i2sStop();
    playing = 0;
}

// ═══════════════════════════════════════════════════════════
// TaskLoop — runs in FreeRTOS task, no instruction limit
// ═══════════════════════════════════════════════════════════

void TaskLoop() {
    while (1) {
        if (req_action == 1) {
            int f = req_freq;
            req_action = 0;
            doPlayTone(f, 1000, 16000);
        }
        if (req_action == 2) {
            req_action = 0;
            doPlayWav(req_file);
        }
        delay(50);
    }
}

// ═══════════════════════════════════════════════════════════
// WebUI — pin config, volume, test buttons
// ═══════════════════════════════════════════════════════════
int tone_btn;
int tone2_btn;
int play_btn;

void WebUI() {
    webPageLabel(1, "I2S Audio");
    webPulldown(pin_bclk, "BCLK Pin", "@getfreepins");
    webPulldown(pin_lrclk, "LRCLK Pin", "@getfreepins");
    webPulldown(pin_dout, "DOUT Pin", "@getfreepins");
    webNumber(pin_rate, 8000, 48000, "Sample Rate");
    webSlider(dac_vol, 0, 255, "Volume");
    webButton(tone_btn, "Test 1kHz");
    webButton(tone2_btn, "Test 440Hz");
    webButton(play_btn, "Play /doorbell.wav");
}

// Poll webButton toggles
void Every50ms() {
    if (playing) return;
    if (tone_btn) {
        tone_btn = 0;
        req_freq = 1000;
        req_action = 1;
    }
    if (tone2_btn) {
        tone2_btn = 0;
        req_freq = 440;
        req_action = 1;
    }
    if (play_btn) {
        play_btn = 0;
        char file[] = "/doorbell.wav";
        strcpy(req_file, file);
        req_action = 2;
    }
}

void WebCall() {
    char buf[64];
    if (playing) {
        webSend("{s}I2S Audio{m}Playing{e}");
    } else if (codec_ok) {
        sprintf(buf, "{s}WM8960{m}Ready (vol %d){e}", dac_vol);
        webSend(buf);
    } else {
        webSend("{s}I2S Audio{m}Ready (no codec){e}");
    }
}

// Command callback: WAVtone <freq> | WAVplay <path>
void Command(char cmd[]) {
    char sub[16];
    char arg[64];
    int sp = strFind(cmd, " ");
    if (sp < 0) {
        strcpy(sub, cmd);
        arg[0] = 0;
    } else {
        strSub(sub, cmd, 0, sp);
        strSub(arg, cmd, sp + 1, strlen(cmd) - sp - 1);
    }

    char c_tone[] = "TONE";
    char c_play[] = "PLAY";
    if (strcmp(sub, c_tone) == 0) {
        int freq = atoi(arg);
        if (freq < 100) freq = 1000;
        req_freq = freq;
        req_action = 1;
        responseCmnd(sub);
    }
    if (strcmp(sub, c_play) == 0) {
        strcpy(req_file, arg);
        req_action = 2;
        responseCmnd(sub);
    }
}

void OnExit() {
    i2sStop();
    if (wm_bus >= 0) I2cResetActive(WM_ADDR, wm_bus);
}

int main() {
    // Apply defaults if persist values are zero (first run)
    if (pin_bclk == 0) pin_bclk = 10;
    if (pin_lrclk == 0) pin_lrclk = 18;
    if (pin_dout == 0) pin_dout = 17;
    if (pin_rate == 0) pin_rate = 16000;
    if (dac_vol == 0) dac_vol = 255;

    // Register WAV commands: WAVtone <freq>, WAVplay <path>
    addCommand("WAV");

    // Try to initialize WM8960 codec — optional, works without it
    if (wmInit() == 0) {
        codec_ok = 1;
    } else {
        addLog("I2S: no codec, direct DAC mode");
    }

    char msg[48];
    sprintf(msg, "I2S: BCLK=%d LRCLK=%d DOUT=%d", pin_bclk, pin_lrclk, pin_dout);
    addLog(msg);
    return 0;
}