PROJECT/Speaker

Speaker 만들기 4편 (SD카드에있는 WAV파일을 OLED에 표시하고 재생)

원원 2026. 2. 10. 23:57

안녕하세요. 오늘은 SD카드에있는 SD카드에있는 WAV파일을 OLED에 표시하고 재생해보겠습니다.

3편에서 만들었던 회로도에서 OLED와 버튼1개를 추가했습니다.

OLED는 SSD1306을 이용해서 I2C 통신을하는 2.42인치 제품이고 해상도는 128x64입니다.
SSD1306을 사용하므로 라이브러리가 많이 있습니다.

코드를 만드는 과정입니다.
1. OLED에 글자 표시
2. 버튼 클릭하면 화살표 이동
3. SD카드에서 WAV파일 읽어서 화면표시하고 클릭하면 WAV파일재생


1.  OLED에 글자 표시

#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

/* SD CARD */
#define SD_SCK 18
#define SD_MISO 19
#define SD_MOSI 23
#define SD_CS 5

/* BUTTON */
#define BTN1 16
#define BTN2 17
#define BTN3 21

/* OLED */
#define OLED_SDA 27
#define OLED_SCL 14
#define OLED_ADDR 0x3C
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64

Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);

void setup()
{
  // UART SETTING
  Serial.begin(115200);

  // BUTTON INIT
  pinMode(BTN1, INPUT_PULLUP);
  pinMode(BTN2, INPUT_PULLUP);
  pinMode(BTN3, INPUT_PULLUP);

  // OLED INIT
  Wire.begin(OLED_SDA, OLED_SCL);
  if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR)) {
    Serial.println("SSD1306 init failed!");
    while (1) { delay(10); }
  }

  // OLED DISPLAY
  display.clearDisplay();
  display.setTextColor(SSD1306_WHITE);
  
  display.setTextSize(1);
  display.setCursor(0, 0);
  display.println("HELLO");

  display.setTextSize(2);
  display.setCursor(0, 0+8);
  display.println("WORLD");

  display.setTextSize(3);
  display.setCursor(0, 0+8+16);
  display.println("WOWON");
  display.display();
}

void loop()
{
  
}

Adafruit SSD1306 라이브러리를 이용합니다.
글자의 크기를 정하는 함수는 display.setTextsize()입니다. 크기가 1이면 y축의 크기가 8정도 먹습니다. 그래서 크기를 1로 사용하면 8*8=64니까 8줄정도 이용 가능합니다. 글자의크기가 2면 y축의 크기는 16정도 먹습니다.
MCU가 OLED와 I2C 통신을 수행하는 시점은 display.display() 호출 시입니다. 그 전까지는 MCU 내부 프레임버퍼에 화면 데이터를 저장합니다.



2. 버튼 클릭하면 화살표 이동

#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

/* BUTTON */
#define BTN1 16
#define BTN2 17
#define BTN3 21

/* OLED */
#define OLED_SDA 27
#define OLED_SCL 14
#define OLED_ADDR 0x3C
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64

int8_t oledLine = 0; // MAX : 8
uint8_t arrayBtn[3] = {BTN1,BTN2,BTN3};
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);


void oled_display(uint8_t btn)
{
  display.clearDisplay();
  display.setTextSize(1);
  display.setTextColor(SSD1306_WHITE);

  if(btn == BTN1)
  {
    oledLine++;
  }
  else
  {
    oledLine --;
  }

  if(oledLine < 0)
  {
    oledLine = 0;
  }
  if(oledLine > 7)
  {
    oledLine = 7;
  }

  display.setCursor(0, oledLine*8);
  display.println(">");
  display.display();

  Serial.print("CURRENT LINE : ");
  Serial.println(oledLine);
}

void setup()
{
  // UART SETTING
  Serial.begin(115200);

  // BUTTON INIT
  pinMode(BTN1, INPUT_PULLUP);
  pinMode(BTN2, INPUT_PULLUP);
  pinMode(BTN3, INPUT_PULLUP);

  // OLED INIT
  Wire.begin(OLED_SDA, OLED_SCL);
  if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR)) {
    Serial.println("SSD1306 init failed!");
    while (1) { delay(10); }
  }

  // INIT OLED DISPLAY
  display.clearDisplay();
  display.setTextSize(1);
  display.setTextColor(SSD1306_WHITE);
  display.setCursor(0, 0);
  display.println(">");
  display.display();
}

bool edgeFalling(bool cur, bool &prev)
{
  bool hit = (prev == HIGH && cur == LOW);
  prev = cur;
  return hit;
}
bool check_btn[3];
bool buffer_btn[3] = {true, true, true};

void loop()
{
  uint8_t c;
  for(c=0; c<3; c++)
  {
    check_btn[c] = digitalRead(arrayBtn[c]);
    
    if(edgeFalling(check_btn[c],buffer_btn[c]))
    {
      Serial.print("CLICK BTN : ");
      Serial.println(c);
      //BTN1: UP , BTN2 : DOWN
      if(arrayBtn[c] == BTN1 || arrayBtn[c] == BTN2)
      {
        oled_display(arrayBtn[c]);
      }
    }
  }
}

버튼이 3개 있는데 버튼을 클릭하면 화살표 (>)를 화면에서 이동하게 됩니다.
여기서 중요한건 "버튼을 클릭할때"를 감지해야 합니다. 인터럽트를 이용할수도 있었지만 인터럽트를 이용하지않았습니다. 클릭하지않았을때 버튼의 상태는 HIGH이고 버튼을 클릭하면 LOW가 됩니다.(falling edge)
falling edge를 구별해주는 함수가 edgeFalling입니다. check_btn은 버튼의 현재상태가 들어가게 되고 buffer_btn은 이전에 읽은 버튼의 상태가 들어가게 됩니다. 그래서 현재 버튼의 상태가 LOW이고 이전 버튼의 상태가 HIGH이면 버튼이 클릭된 상태로 인식합니다.
버튼이 클릭됐으면 oled_display 함수를 호출해서 oledLine을 이동시키고 화면에 >를 표시합니다. 글자크기가 1이므로 y축은 8씩 이동시키게 됩니다.

0


3. SD카드에서 WAV파일 읽어서 화면표시하고 클릭하면 WAV파일재생

#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <SD.h>
#include <SPI.h>
#include <ESP_I2S.h>

#define BTN1 16
#define BTN2 17
#define BTN3 21

#define OLED_SDA 27
#define OLED_SCL 14
#define OLED_ADDR 0x3C
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64

#define SD_SCK  18
#define SD_MISO 19
#define SD_MOSI 23
#define SD_CS   5

#define BCLK 26
#define LRC  25
#define DIN  22

#define MAX_WAV_FILES 80
#define OLED_LINES 8

Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);
I2SClass i2s;
File f;

uint8_t arrayBtn[3] = {BTN1, BTN2, BTN3};
bool check_btn[3];
bool buffer_btn[3] = {true, true, true};

String wavList[MAX_WAV_FILES];
int16_t wavCount = 0, wav_top = 0, wav_idx = 0;
int8_t oledLine = 0;

bool playing = false;
bool cancel_req = false;

typedef struct {
  uint16_t audioFormat;
  uint16_t numChannels;
  uint32_t sampleRate;
  uint16_t bitsPerSample;
  uint32_t dataOffset;
  uint32_t dataSize;
  uint32_t bytesPerSec;
  uint32_t totalMs;
} wav_info_t;

wav_info_t curWav = {};

bool edgeFalling(bool cur, bool &prev){ bool hit=(cur==LOW && prev==HIGH); prev=cur; return hit; }
bool isWavFile(const String &name){ String s=name; s.toLowerCase(); return s.endsWith(".wav"); }
String fitOled(const String &s){ const int n=18; if((int)s.length()<=n) return s; return s.substring(0,n-3)+"..."; }

void sd_scan_wav(){
  wavCount=0;
  File root=SD.open("/");
  if(!root || !root.isDirectory()) return;
  while(true){
    File e=root.openNextFile();
    if(!e) break;
    if(!e.isDirectory()){
      String n=String(e.name());
      if(isWavFile(n) && wavCount<MAX_WAV_FILES) wavList[wavCount++]=n;
    }
    e.close();
  }
  root.close();
  wav_top=0; wav_idx=0; oledLine=0;
}

void oled_draw_list(){
  display.clearDisplay();
  display.setTextSize(1);
  display.setTextColor(SSD1306_WHITE);
  if(wavCount<=0){
    display.setCursor(0,0); display.println("No .wav in /"); display.display(); return;
  }
  for(int i=0;i<OLED_LINES;i++){
    int idx=wav_top+i; if(idx>=wavCount) break;
    display.setCursor(0,i*8); display.print((i==oledLine)?">":" ");
    display.setCursor(8,i*8); display.println(fitOled(wavList[idx]));
  }
  display.display();
}

void oled_move(uint8_t btn){
  if(wavCount<=0 || playing) return;
  if(btn==BTN1){ if(wav_idx<wavCount-1) wav_idx++; }
  else if(btn==BTN2){ if(wav_idx>0) wav_idx--; }
  if(wav_idx<wav_top) wav_top=wav_idx;
  if(wav_idx>=wav_top+OLED_LINES) wav_top=wav_idx-(OLED_LINES-1);
  oledLine=(int8_t)(wav_idx-wav_top);
  oled_draw_list();
}

i2s_data_bit_width_t bitWidthFrom(uint16_t bps){
  switch(bps){
    case 16: return I2S_DATA_BIT_WIDTH_16BIT;
    case 24: return I2S_DATA_BIT_WIDTH_24BIT;
    case 32: return I2S_DATA_BIT_WIDTH_32BIT;
    default: return I2S_DATA_BIT_WIDTH_16BIT;
  }
}

bool wav_open_selected(){
  if(wavCount<=0) return false;

  String path="/"+wavList[wav_idx];
  if(f) f.close();
  f = SD.open(path.c_str(), FILE_READ);
  if(!f) return false;

  uint8_t header[44];
  if(f.read(header,44)!=44){ f.close(); return false; }

  curWav.audioFormat   = header[20] | (header[21]<<8);
  curWav.numChannels   = header[22] | (header[23]<<8);
  curWav.sampleRate    = (uint32_t)header[24] | ((uint32_t)header[25]<<8) | ((uint32_t)header[26]<<16) | ((uint32_t)header[27]<<24);
  curWav.bitsPerSample = header[34] | (header[35]<<8);

  uint32_t dataSize = (uint32_t)header[40] | ((uint32_t)header[41]<<8) | ((uint32_t)header[42]<<16) | ((uint32_t)header[43]<<24);

  curWav.dataOffset = 44;
  curWav.dataSize   = dataSize;
  curWav.bytesPerSec = curWav.sampleRate * curWav.numChannels * (curWav.bitsPerSample/8);
  curWav.totalMs = (curWav.bytesPerSec==0)?0:(uint32_t)((uint64_t)curWav.dataSize*1000ULL/curWav.bytesPerSec);

  if(curWav.audioFormat!=1) { f.close(); return false; }
  if(!(curWav.numChannels==1 || curWav.numChannels==2)) { f.close(); return false; }
  if(!(curWav.bitsPerSample==16 || curWav.bitsPerSample==24 || curWav.bitsPerSample==32)) { f.close(); return false; }

  i2s.setPins(BCLK, LRC, DIN);
  if(!i2s.begin(I2S_MODE_STD, curWav.sampleRate, bitWidthFrom(curWav.bitsPerSample),
                (curWav.numChannels==1)?I2S_SLOT_MODE_MONO:I2S_SLOT_MODE_STEREO)){
    f.close(); return false;
  }

  f.seek(curWav.dataOffset);
  return true;
}

void draw_play_ui(uint32_t rem_ms, uint32_t done_bytes){
  display.clearDisplay();
  display.setTextSize(1);
  display.setTextColor(SSD1306_WHITE);

  display.setCursor(0,0);
  display.print("PLAY ");
  display.println(fitOled(wavList[wav_idx]));

  uint32_t s = rem_ms/1000, m=s/60; s%=60;
  display.setCursor(0,16);
  display.print("Remain ");
  if(m<10) display.print("0"); display.print(m);
  display.print(":");
  if(s<10) display.print("0"); display.print(s);

  int x=0,y=32,w=128,h=10;
  display.drawRect(x,y,w,h,SSD1306_WHITE);
  int fill = (curWav.dataSize==0)?0:(int)((uint64_t)done_bytes*(w-2)/curWav.dataSize);
  if(fill<0) fill=0; if(fill>w-2) fill=w-2;
  display.fillRect(x+1,y+1,fill,h-2,SSD1306_WHITE);

  display.setCursor(0,48);
  display.println("BTN3: Cancel");

  display.display();
}

void stop_play(){
  cancel_req=false;
  playing=false;
  if(f) f.close();
  i2s.end();
  oled_draw_list();
}

void start_play(){
  cancel_req=false;
  if(!wav_open_selected()){ oled_draw_list(); return; }
  playing=true;

  static uint8_t buf[4096];
  uint32_t done=0, last_ui=0;
  bool prev3=true;

  uint32_t ignore_cancel_until = millis() + 250;

  while(playing && f.available()){
    bool cur3 = digitalRead(BTN3);
    if(millis() > ignore_cancel_until) {
      if(edgeFalling(cur3, prev3)) cancel_req=true;
    } else {
      prev3 = cur3;
    }
    if(cancel_req) break;

    int len = f.read(buf, sizeof(buf));
    if(len>0) i2s.write(buf, len);
    done += (len>0)?(uint32_t)len:0;

    uint32_t now=millis();
    if(now-last_ui>=120){
      last_ui=now;
      uint32_t rem_bytes = (done>=curWav.dataSize)?0:(curWav.dataSize-done);
      uint32_t rem_ms = (curWav.bytesPerSec==0)?0:(uint32_t)((uint64_t)rem_bytes*1000ULL/curWav.bytesPerSec);
      draw_play_ui(rem_ms, done);
    }
  }

  stop_play();
}

void setup(){
  Serial.begin(115200);

  pinMode(BTN1,INPUT_PULLUP);
  pinMode(BTN2,INPUT_PULLUP);
  pinMode(BTN3,INPUT_PULLUP);

  Wire.begin(OLED_SDA, OLED_SCL);
  if(!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR)) while(1) delay(10);

  SPI.begin(SD_SCK, SD_MISO, SD_MOSI, SD_CS);
  if(!SD.begin(SD_CS, SPI)) while(1) delay(10);

  sd_scan_wav();
  oled_draw_list();
}

void loop(){
  for(uint8_t c=0;c<3;c++){
    check_btn[c]=digitalRead(arrayBtn[c]);
    if(edgeFalling(check_btn[c],buffer_btn[c])){
      if(arrayBtn[c]==BTN1 || arrayBtn[c]==BTN2) oled_move(arrayBtn[c]);
      if(arrayBtn[c]==BTN3){
        if(!playing) start_play();
      }
      delay(30);
    }
  }
}

BTN3을 클릭하면 wav파일이 재생되고 남은시간이 표시되게됩니다.

0