SuperMakeSomething / mini-video-player

Code + Design Files for the Mini Video Player project featured in Super Make Something Episode 24
57 stars 13 forks source link

SC01 PLUS Failed to read audio #8

Open modi12jin opened 1 year ago

modi12jin commented 1 year ago

miniVideoPlayer.ino

/*
 * require libraries:
 * https://github.com/lovyan03/LovyanGFX.git
 * https://github.com/earlephilhower/ESP8266Audio.git
 * https://github.com/bitbank2/JPEGDEC.git
 */

#define FPS 20
#define MJPEG_BUFFER_SIZE (480 * 270 * 2 / 10)

#include <WiFi.h>
#include <FS.h>
#include <SD_MMC.h>
#include <driver/i2s.h>

#define SD_CS 41
#define SDMMC_CMD 40
#define SDMMC_CLK 39
#define SDMMC_D0 38

#define I2C_SDA 6
#define I2C_SCL 5
#define TP_INT 7
#define TP_RST -1

#include "LovyanGFX_Driver.h"
LGFX tft;

//MP3音频
#include <AudioFileSourceFS.h>
#include <AudioGeneratorMP3.h>
#include <AudioOutputI2S.h>
static AudioGeneratorMP3 *mp3 = NULL;
static AudioFileSourceFS *aFile = NULL;
static AudioOutputI2S *out = NULL;

//MJPEG视频
#include "MjpegClass.h"
static MjpegClass mjpeg;

static unsigned long total_show_video = 0; //总节目视频

int noFiles = 0; // SD卡根目录下的媒体文件数量
String videoFilename;
String audioFilename;

int fileNo = 1;             // 首先播放哪个(视频、音频)文件对的变量
bool buttonPressed = false; // 文件更改按钮按下变量
bool fullPlaythrough = true;
unsigned long lastDebounceTime = 0;
unsigned long debounceDelay = 50;

File root;

void IRAM_ATTR incrFileNo()
{
  if ((millis() - lastDebounceTime) > debounceDelay)
  {
    fileNo += 1;
    if (fileNo > noFiles) //循环一圈 
    {
      fileNo = 0;
    }
    buttonPressed = true;
    lastDebounceTime = millis();
    fullPlaythrough = false;
    Serial.print("Button Pressed! Current Debounce Time: ");
    Serial.println(lastDebounceTime);
  }
}

void setup()
{
  WiFi.mode(WIFI_OFF);
  Serial.begin(115200);

  pinMode(TP_INT, INPUT_PULLUP);
  attachInterrupt(TP_INT, incrFileNo, RISING);

  // 初始化音频
  out = new AudioOutputI2S(0, 1, 128);
  mp3 = new AudioGeneratorMP3();
  aFile = new AudioFileSourceFS(SD_MMC);

  // 初始化视频
  tft.init();
  tft.initDMA();
  tft.startWrite();
  tft.fillScreen(TFT_BLACK);

  SD_MMC.setPins(SDMMC_CLK, SDMMC_CMD, SDMMC_D0);
  if (!SD_MMC.begin("/root", true)) /* 1-bit SD bus mode */
  {
    Serial.println("ERROR: File system mount failed!");
    tft.println("ERROR: File system mount failed!");
  }
  else
  {
    root = SD_MMC.open("/");
    noFiles = getNoFiles(root);
    Serial.print("Found ");
    Serial.print(noFiles);
    Serial.println(" in root directory!");
    Serial.println("Starting playback!");
  }
}

// 是音视频对
bool isAudioVideoPair(File const &entry, File dir)
{
  String name = entry.path();
  if (!name.endsWith(".mjpeg"))
    return false;

  if (SD_MMC.exists(name.substring(0, name.length() - 6) + ".mp3"))
    return true;

  return false;
}

// 获取无文件
int getNoFiles(File dir)
{
  while (true)
  {
    File entry = dir.openNextFile();
    if (!entry)
    {
      // no more files
      break;
    }
    if (entry.isDirectory() || !isAudioVideoPair(entry, dir))
    {
      // Skip file if in subfolder
      entry.close(); // Close folder entry
    }
    else
    {
      entry.close();
      ++noFiles;
    }
  }
  return noFiles;
}

//获取文件名
void getFilenames(File dir, int fileNo)
{
  int fileCounter = 0;
  while (true)
  {
    File entry = dir.openNextFile();
    if (!entry)
    {
      // 没有更多文件
      break;
    }
    if (entry.isDirectory() || !isAudioVideoPair(entry, dir))
    {
      // 如果文件位于子文件夹中则跳过文件
      entry.close(); // 关闭文件夹目录
    }
    else //有效的音频/视频对
    {
      ++fileCounter;
      if (fileCounter == fileNo)
      {
        videoFilename = entry.path();
        audioFilename = videoFilename.substring(0, videoFilename.length() - 6) + ".mp3";
        Serial.print("Loading video: ");
        Serial.println(videoFilename);
        Serial.print("Loading audio: ");
        Serial.println(audioFilename);
      }
      entry.close();
    }
  }
}

//播放视频
void playVideo(String videoFilename, String audioFilename)
{
  int next_frame = 0;
  int skipped_frames = 0;
  unsigned long total_play_audio = 0;
  unsigned long total_read_video = 0;
  unsigned long total_decode_video = 0;
  unsigned long start_ms, curr_ms, next_frame_ms;

  int brightPWM = 0;

  Serial.println("In playVideo() loop!");

  if (mp3 && mp3->isRunning())
    mp3->stop();
  if (!aFile->open(audioFilename.c_str()))
    Serial.println(F("Failed to open audio file"));
  Serial.println("Created aFile!");

  File vFile = SD_MMC.open(videoFilename);
  Serial.println("Created vFile!");

  uint8_t *mjpeg_buf = (uint8_t *)malloc(MJPEG_BUFFER_SIZE);
  if (!mjpeg_buf)
  {
    Serial.println(F("mjpeg_buf malloc failed!"));
  }
  else
  {
    // init Video
    mjpeg.setup(&vFile, mjpeg_buf, drawMCU, false, true); // MJPEG SETUP -> bool setup(Stream *input, uint8_t *mjpeg_buf, JPEG_DRAW_CALLBACK *pfnDraw, bool enableMultiTask, bool useBigEndian)
  }

  if (!vFile || vFile.isDirectory())
  {
    Serial.println(("ERROR: Failed to open " + videoFilename + ".mjpeg file for reading"));
    tft.println(("ERROR: Failed to open " + videoFilename + ".mjpeg file for reading"));
  }
  else
  {
    // init audio
    if (!mp3->begin(aFile, out))
      Serial.println(F("Failed to start audio!"));
    start_ms = millis();
    curr_ms = start_ms;
    next_frame_ms = start_ms + (++next_frame * 1000 / FPS);

    while (vFile.available() && buttonPressed == false)
    {
      // Read video
      mjpeg.readMjpegBuf();
      total_read_video += millis() - curr_ms;
      curr_ms = millis();
      if (millis() < next_frame_ms) // check show frame or skip frame
      {
        // Play video
        mjpeg.drawJpg();
        total_decode_video += millis() - curr_ms;
      }
      else
      {
        ++skipped_frames;
        Serial.println(F("Skip frame"));
      }
      curr_ms = millis();
      // Play audio
      if (mp3->isRunning() && !mp3->loop())
      {
        mp3->stop();
      }

      total_play_audio += millis() - curr_ms;
      while (millis() < next_frame_ms)
      {
        vTaskDelay(1);
      }
      curr_ms = millis();
      next_frame_ms = start_ms + (++next_frame * 1000 / FPS);
    }
    if (fullPlaythrough == false)
    {
      mp3->stop();
    }
    buttonPressed = false; // reset buttonPressed boolean
    int time_used = millis() - start_ms;
    int total_frames = next_frame - 1;
    Serial.println(F("MP3 audio MJPEG video end"));
    vFile.close();
    aFile->close();
  }
  if (mjpeg_buf)
  {
    free(mjpeg_buf);
  }
}

// pixel drawing callback
static int drawMCU(JPEGDRAW *pDraw)
{
  unsigned long s = millis();
  if (tft.getStartCount() > 0)
  {
    tft.endWrite();
  }
  tft.pushImageDMA(pDraw->x, pDraw->y, pDraw->iWidth, pDraw->iHeight, (lgfx::swap565_t *)pDraw->pPixels);
  total_show_video += millis() - s;
  return 1;
}

void loop()
{
  root = SD_MMC.open("/");
  Serial.print("fileNo: ");
  Serial.println(fileNo);
  getFilenames(root, fileNo);//获取文件名
  playVideo(videoFilename, audioFilename);//播放视频

  if (fullPlaythrough == true) // 检查 fullPlaythrough 布尔值以避免 fileNo 的双重增量
  {
    fileNo = fileNo + 1;  // 增加 fileNo 以播放下一个视频
    if (fileNo > noFiles) // 如果超出文件数,则重置计数器
    {
      fileNo = 1;
    }
  }
  else // 重置 fullPlaythrough 布尔值
  {
    fullPlaythrough = true;
  }
}

LovyanGFX.h

#include <LovyanGFX.hpp>

class LGFX : public lgfx::LGFX_Device {

  lgfx::Panel_ST7796 _panel_instance;

  lgfx::Bus_Parallel8 _bus_instance;

  lgfx::Light_PWM _light_instance;

public:
  LGFX(void) {
    {
      auto cfg = _bus_instance.config();

      cfg.port = 0;
      cfg.freq_write = 40000000;
      cfg.pin_wr = 47;  // WR を接続しているピン番号
      cfg.pin_rd = -1;  // RD を接続しているピン番号
      cfg.pin_rs = 0;   // RS(D/C)を接続しているピン番号
      cfg.pin_d0 = 9;   // D0を接続しているピン番号
      cfg.pin_d1 = 46;  // D1を接続しているピン番号
      cfg.pin_d2 = 3;   // D2を接続しているピン番号
      cfg.pin_d3 = 8;   // D3を接続しているピン番号
      cfg.pin_d4 = 18;  // D4を接続しているピン番号
      cfg.pin_d5 = 17;  // D5を接続しているピン番号
      cfg.pin_d6 = 16;  // D6を接続しているピン番号
      cfg.pin_d7 = 15;  // D7を接続しているピン番号

      _bus_instance.config(cfg);               // 設定値をバスに反映します。
      _panel_instance.setBus(&_bus_instance);  // バスをパネルにセットします。
    }

    {                                       // 表示パネル制御の設定を行います。
      auto cfg = _panel_instance.config();  // 表示パネル設定用の構造体を取得します。

      cfg.pin_cs = -1;    // CSが接続されているピン番号   (-1 = disable)
      cfg.pin_rst = 4;    // RSTが接続されているピン番号  (-1 = disable)
      cfg.pin_busy = -1;  // BUSYが接続されているピン番号 (-1 = disable)

      // ※ 以下の設定値はパネル毎に一般的な初期値が設定さ BUSYが接続されているピン番号 (-1 = disable)れていますので、不明な項目はコメントアウトして試してみてください。

      cfg.memory_width = 320;    // ドライバICがサポートしている最大の幅
      cfg.memory_height = 480;   // ドライバICがサポートしている最大の高さ
      cfg.panel_width = 320;     // 実際に表示可能な幅
      cfg.panel_height = 480;    // 実際に表示可能な高さ
      cfg.offset_x = 0;          // パネルのX方向オフセット量
      cfg.offset_y = 0;          // パネルのY方向オフセット量
      cfg.offset_rotation = 1;   //值在旋转方向的偏移0~7(4~7是倒置的)
      cfg.dummy_read_pixel = 8;  // 在读取像素之前读取的虚拟位数
      cfg.dummy_read_bits = 1;   // 读取像素以外的数据之前的虚拟读取位数
      cfg.readable = false;      // 如果可以读取数据,则设置为 true
      cfg.invert = true;         // 如果面板的明暗反转,则设置为 true
      cfg.rgb_order = false;     // 如果面板的红色和蓝色被交换,则设置为 true
      cfg.dlen_16bit = false;    // 对于以 16 位单位发送数据长度的面板,设置为 true
      cfg.bus_shared = false;    // 如果总线与 SD 卡共享,则设置为 true(使用 drawJpgFile 等执行总线控制)

      _panel_instance.config(cfg);
    }

    {                                       // バックライト制御の設定を行います。(必要なければ削除)
      auto cfg = _light_instance.config();  // バックライト設定用の構造体を取得します。

      cfg.pin_bl = 45;      // バックライトが接続されているピン番号
      cfg.invert = false;   // バックライトの輝度を反転させる場合 true
      cfg.freq = 44100;     // バックライトのPWM周波数
      cfg.pwm_channel = 1;  // 使用するPWMのチャンネル番号

      _light_instance.config(cfg);
      _panel_instance.setLight(&_light_instance);  // バックライトをパネルにセットします。
    }

    setPanel(&_panel_instance);  // 使用するパネルをセットします。
  }
};

Serial port print information

ESP-ROM:esp32s3-20210327
Build:Mar 27 2021
rst:0x15 (USB_UART_CHIP_RESET),boot:0xa (SPI_FAST_FLASH_BOOT)
Saved PC:0x4209347e
SPIWP:0xee
mode:DIO, clock div:1
load:0x3fce3808,len:0x44c
load:0x403c9700,len:0xbe4
load:0x403cc700,len:0x2a68
entry 0x403c98d4
Found 3 in root directory!
Starting playback!
fileNo: 1
Loading video: /02.mjpeg
Loading audio: /02.mp3
In playVideo() loop!
Created aFile!
Created vFile!
Failed to start audio!
modi12jin commented 1 year ago

Files in SD card

2023-08-05_16-58

ffmpeg

ffmpeg -i input.mp4 -ar 44100 -ac 1 -ab 32k -filter:a loudnorm -filter:a "volume=-5dB" output.mp3
ffmpeg -i input.mp4 -vf "fps=20,scale=-1:272:flags=lanczos,crop=480:in_h:(in_w-480)/2:0" -q:v 9 output.mjpeg
modi12jin commented 1 year ago
#define I2S_DOUT 37
#define I2S_BCLK 36
#define I2S_LRC 35

  // 初始化音频
  out = new AudioOutputI2S(I2S_NUM_0);
  out->SetGain(0.5);          //设置音量0~1
  out->SetRate(44100);        //采样率
  out->SetChannels(1);//1单声道,2立体声
  out->SetBitsPerSample(16);  //指定每个样本的位数,通常为8或16
  out->SetPinout(I2S_BCLK, I2S_LRC, I2S_DOUT);
  mp3 = new AudioGeneratorMP3();
  aFile = new AudioFileSourceFS(SD_MMC);
ESP-ROM:esp32s3-20210327
Build:Mar 27 2021
rst:0x15 (USB_UART_CHIP_RESET),boot:0x8 (SPI_FAST_FLASH_BOOT)
Saved PC:0x420934d2
SPIWP:0xee
mode:DIO, clock div:1
load:0x3fce3808,len:0x44c
load:0x403c9700,len:0xbe4
load:0x403cc700,len:0x2a68
entry 0x403c98d4
Found 6 in root directory!
Starting playback!
fileNo: 1
Loading video: /02.mjpeg
Loading audio: /02.mp3
In playVideo() loop!
Created aFile!
Created vFile!
Skip frame
Skip frame
Skip frame
Skip frame
Skip frame
Skip frame
Skip frame
Skip frame
Skip frame
Skip frame
Skip frame
Skip frame
Skip frame