작은 OLED 디스플레이에서 막대 그래프가 음악과 소리에 반응하는 모습을 보세요. 최저 주파수는 그래프 왼쪽, 최고 주파수는 오른쪽에 표시됩니다.
구성
연결
마이크 GAIN 핀 연결로 감도 조절: VCC를 마이크 GAIN에 연결 → 40dB (저감도), GND를 마이크 GAIN에 연결 → 50dB (중감도), 마이크 GAIN을 플로팅(미연결) → 60dB (고감도).
아날로그 핀 0 A0을 마이크 앰프 OUT에 연결
3.3V, GND, SDA (또는 아날로그 4 A4), SCL (아날로그 5 A5)을 I2C SSD1306에 연결
전원 버스에 470µF 콘덴서를 VCC-GND 사이에 연결
저주파 노이즈가 문제인가요?
측정 시스템 근처에서 작동하는 모든 전자 기기에서 노이즈가 발생합니다. 모든 전자 기기는 고정 주파수와 전압의 교류(AC)로 작동하며, 국가에 따라 50Hz 또는 60Hz가 전력선 주파수입니다. 패러데이 케이지로 쉽게 제거 가능합니다. 회로를 상자에 넣고 상자를 그라운드에 연결하세요. 회로도 잘 접지하세요.
아두이노 나노 33 BLE 핀아웃
아두이노 코어 AnalogRead와 아두이노 나노 33 BLE로는 초당 약 35,000 샘플만 얻을 수 있어 FFT로 17.5kHz 이상 주파수 처리는 불가능합니다. 클럭 읽기와 비지 웨이트 루프 내 비교 연산 등을 고려해 안전하게 8kHz 상한으로 잡습니다. 이 이상 주파수를 차단하는 간단한 RC 로우패스 필터를 사용합니다.
R=180Ω, C=0.1µF → 차단 주파수 fc=8842Hz
본 프로젝트: 2kΩ 저항 + 4.7nF 콘덴서 → fc=17000Hz
최대 샘플링 레이트 대략 계산
일반 Arduino analogRead()는 오디오 샘플링에 너무 느립니다. 최대 샘플링 레이트를 계산해 봅시다.
아두이노 코어 AnalogRead와 아두이노 나노 33 BLE로 얼마나 많은 샘플을 얻을 수 있는지 추정합니다.
아래 작은 프로그램을 돌려 추정해 보세요.
/*
* AnalogRead 최대 샘플링 레이트 대략 계산
*/
#define ANALOG_PIN A0
#define SAMPLES 1000000.0
int newTime;
int analogValue;
void setup() {
Serial.begin(9600);
Serial.println("Sampling... please, wait. ");
}
void loop() {
newTime = micros();
for (int i = 0; i < (int)SAMPLES; i++) {
analogValue = analogRead(ANALOG_PIN);
}
float elapsedTimePerSampleUs = (micros() - newTime) / SAMPLES;
Serial.print("Time per sample (us): ");
Serial.println(elapsedTimePerSampleUs);
Serial.print("Max sampling freq(hz): ");
Serial.println(pow(10,6) / elapsedTimePerSampleUs);
}
아두이노 코어 AnalogRead와 아두이노 나노 33 BLE로 초당 약 35,000 샘플 획득 가능. 나이퀴스트 정리(2배 샘플링 필요)에 따라 17kHz 이상 주파수는 처리 불가. 로우패스 필터로 그 이상 차단.
클럭 읽기와 비지 웨이트 루프 비교 연산 등을 고려해 8kHz 상한으로 안전하게 설정.
// 샘플링
for (int i = 0; i < SAMPLES; i++) {
unsigned long newTime = micros();
int value = analogRead(ANALOG_PIN);
vReal[i] = value;
vImag[i] = 0;
while (micros() < (newTime + sampling_period_us)) {
yield();
}
}
사용자 인터페이스
화면이 11개 주파수 존(Zone)으로 나뉘며, 인간의 청각 인식과 유사한 로그 스케일로 주파수를 그룹화합니다.
인간 가청 주파수는 약 20Hz~20kHz. (영아는 20kHz 이상도 들리지만 성인 평균은 15~17kHz가 상한.)
존 3~13 (11개 존)을 표현에 사용.
각 막대는 해당 대역의 최대값을 점선으로, 평균값을 채워진 직사각형으로 표시.
값은 휴리스틱하게 얻은 미리 선택된 레퍼런스를 기준으로 데시벨(dB) 단위로 표시.
작동 원리
오디오 샘플을 고속 푸리에 변환(FFT)으로 주파수 스펙트럼으로 변환.
스펙트럼을 14개 존으로 나누고 그룹화한 뒤 11개 존만 dB로 표현.
마이크 모듈 상세
일렉트렛 마이크
일렉트렛은 준영구적으로 대전된 유전체입니다. 세라믹 소재를 가열해 자기장에 놓고 냉각시키며 만듭니다. 영구 자석의 정전기 버전이라 할 수 있습니다. 일렉트렛 마이크에서 이 소재 조각을 커패시터 유전체 일부로 쓰고, 마이크 다이어프램이 한 쪽 극판 역할을 합니다. 음압이 다이어프램을 움직이면 음압에 비례해 정전용량 변함. 유전체에 내장된 고정 전하 때문에 커패시터 양단 전압도 변합니다. 이 일렉트렛 커패시터가 내장 FET 앰프 입력에 연결됩니다. 일렉트렛 마이크는 작고 감도 우수, 광대역 주파수 응답, 저가격 장점.
MAX9814 마이크 앰프
MAX9814는 자동 이득 제어(AGC), 저잡음 마이크 바이어스 갖춘 마이크 앰프. 브레이크아웃 보드로 몇 가지 설정 가능.
세 가지 앰프 설정: 40dB, 50dB, 60dB. 기본 60dB, Gain 핀을 VCC나 그라운드에 점퍼로 연결해 40dB/50dB로 변경 가능.
내부 저잡음 마이크 바이어스 1.25V, 2Vpp. 앰프 출력은 1.25V DC 바이어스 위 최대 2Vpp라 3.3V 입력 ADC와 쉽게 연결 가능.
어택/릴리스 비율도 기본 1:4000에서 1:2000 또는 1:500으로 변경 가능.
라인 입력 직결 시 블로킹/커플링 콘덴서로 DC 성분 차단 필요. 1µF 이상 직렬 콘덴서 사용 (시스템 따라 33µF~100µF 시도). 커플링 회로는 AC 신호만 통과시키고 DC 차단해 오디오 출력 왜곡 방지. 효과는 삽입 손실, 등가 직렬 저항, 직렬 공진 주파수 등 주파수 의존 파라미터에 달림.
전원: 2.7V~5.5V
감도(Sensitivity)
감도는 아날로그 출력 전압이나 디지털 출력 값과 입력 음압 비율로, 마이크 핵심 스펙입니다. 음향 도메인 단위를 전기 도메인 단위로 매핑해 알려진 입력에 대한 출력 크기 결정.
마이크 감도는 표준 음향 입력(1kHz 사인파, 94dB SPL, 1Pa)에 대한 전기적 출력 반응. 감도 값이 높을수록 고정 음향 입력에 대한 출력이 큼. 데시벨(dB) 단위 감도는 보통 음수라 절대값이 작을수록 고감도.
아날로그 마이크 감도는 보통 dBV(1.0V rms 기준 데시벨) 단위.
레퍼런스 레벨과 주파수 응답
마이크 주파수 응답은 주파수 스펙트럼 전반에 걸친 출력 레벨. 상/하한 주파수는 1kHz 기준 출력보다 3dB 낮은 지점. 1kHz 레퍼런스 레벨은 관례상 0dB로 정규화.
0.91인치 OLED I2C 디스플레이 128x32 픽셀
대각선 약 1인치 작은 디스플레이. 128x32 개별 백색 OLED 픽셀, 컨트롤러 칩이 각 픽셀 온/오프 제어. 백라이트 불필요 → 전력 소모 감소.
여기 쓴 모델은 4핀만 있고 I2C 프로토콜로 아두이노와 통신. RESET 핀 없음. 아두이노 우노 I2C 핀에 연결:
아두이노 나노: SDA(A4), SCL(A5)
OLED 제어용 adafruit_SSD1306.h, adafruit_GFX.h 라이브러리 필요. 설치 방법은 아래 참조.
아다프루트 라이브러리 사용. 아다프루트에서 뭐라도 사서 지원해 주세요.
다음 단계
아두이노 나노 33 BLE는 아날로그 읽기 성능을 더 끌어올릴 수 있습니다. 다음 버전에선 nRF52840에 최적화된 FFT와 아날로그 입력을 사용할 예정입니다.
/* Sound Spectrum Visualizer for Arduino Nano 33 BLE
* Arduino based sound visualizer
* @author Enrique Albertos
* Hardware requirements:
* - Arduino or Arduino-compatible boards.
* - ssd1306 oled I2C 128x32 display
* - Electret Microphone Amplifier with max9814
* - Optional: battery for portable use (else power through USB)
* Software requirements:
* - arduinoFFT https://github.com/kosme/arduinoFFT
* - Adafruit_GFX https://github.com/adafruit/Adafruit-GFX-Library
* - Adafruit_SSD1306 https://github.com/adafruit/Adafruit_SSD1306
* Connections:
* - Mic GAIN: * VCC to mic GAIN for 40dB * GND to mic GAIN for 50dB * floating mic GAIN for 60dB
* - Analog pin 0 to mic amp output
* - +3.3V, GND, SDA (or analog 4) and SCL (analog 5) to I2C SSD1306
* @uses arduinoFFT https://github.com/kosme/arduinoFFT
* @uses Adafruit_GFX https://github.com/adafruit/Adafruit-GFX-Library
* @uses Adafruit_SSD1306 https://github.com/adafruit/Adafruit_SSD1306
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*/
#include "arduinoFFT.h"
#include
#include
#define SAMPLES 1024 // 2의 거듭제곱
#define SAMPLING_FREQ 24000 // 12kHz Fmax = sampleF/2
#define AMPLITUDE 100 // 감도
#define FREQUENCY_BANDS 14
#define SCREEN_WIDTH 128 // OLED 디스플레이 너비 (픽셀)
#define SCREEN_HEIGHT 32 // OLED 디스플레이 높이 (픽셀)
#define BARWIDTH 11
#define BARS 11
#define ANALOG_PIN A0
// I2C(SDA, SCL)로 연결된 SSD1306 디스플레이 선언
#define OLED_RESET -1 // 리셋 핀 번호 (아두이노 리셋 핀 공유 시 -1)
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
double vImag[SAMPLES];
double vReal[SAMPLES];
unsigned long sampling_period_us;
arduinoFFT fft = arduinoFFT(vReal, vImag, SAMPLES, SAMPLING_FREQ);
// 배경 노이즈 제거용 레퍼런스 (휴리스틱하게 결정)
float reference = log10(50.0);
double coutoffFrequencies[FREQUENCY_BANDS];
void setup() {
// SSD1306_SWITCHCAPVCC = 3.3V 내부에서 디스플레이 전압 생성
if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { // 128x32 주소 0x3C
for (;;); // 진행 안 함, 무한 루프
}
// 디스플레이 설정
display.clearDisplay();
display.display();
display.setRotation(0);
display.invertDisplay(false);
sampling_period_us = (1.0 / SAMPLING_FREQ) * pow(10.0, 6);
// 차단 주파수 계산, 로그 스케일 베이스 basePot
double basePot = pow(SAMPLING_FREQ / 2.0, 1.0 / FREQUENCY_BANDS);
coutoffFrequencies[0] = basePot;
for (int i = 1; i < FREQUENCY_BANDS; i++) {
coutoffFrequencies[i] = basePot * coutoffFrequencies[i - 1];
}
// 주파수 대역 구분용 점선 그리기
for (int i = 0; i < BARS - 1; i++) {
for (int j = 0; j < SCREEN_HEIGHT; j += 4) {
display.writePixel((i + 1) * BARWIDTH + 2, j, SSD1306_WHITE);
}
}
display.drawRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT, SSD1306_WHITE);
}
int oldHeight[20];
int oldMax[20];
double maxInFreq;
void loop() {
// 샘플링
for (int i = 0; i < SAMPLES; i++) {
unsigned long newTime = micros();
int value = analogRead(ANALOG_PIN);
vReal[i] = value;
vImag[i] = 0;
while (micros() < (newTime + sampling_period_us)) {
yield();
}
}
// FFT 계산
fft.DCRemoval();
fft.Windowing(FFT_WIN_TYP_HAMMING, FFT_FORWARD);
fft.Compute(FFT_FORWARD);
fft.ComplexToMagnitude();
double median[20];
double max[20];
int index = 0;
double hzPerSample = (1.0 * SAMPLING_FREQ) / SAMPLES;
double hz = 0;
double maxinband = 0;
double sum = 0;
int count = 0;
for (int i = 2; i < (SAMPLES / 2); i++) {
count++;
sum += vReal[i];
if (vReal[i] > max[index]) {
max[index] = vReal[i];
}
if (hz > coutoffFrequencies[index]) {
median[index] = sum / count;
sum = 0.0;
count = 0;
index++;
max[index] = 0;
median[index] = 0;
}
hz += hzPerSample;
}
// 대역별 중앙값과 최대값 계산
if (sum > 0.0) {
median[index] = sum / count;
if (median[index] > maxinband) {
maxinband = median[index];
}
}
int bar = 0;
for (int i = FREQUENCY_BANDS - 1; i >= 3; i--) {
int newHeight = 0;
int newMax = 0;
// 실제 데시벨 계산
if (median[i] > 0 && max[i] > 0) {
newHeight = 20.0 * (log10(median[i]) - reference);
newMax = 20.0 * (log10(max[i]) - reference);
}
// 최소/최대 레벨 보정
if (newHeight < 0 || newMax < 0) {
newHeight = 1;
newMax = 1;
}
if (newHeight >= SCREEN_HEIGHT - 2) {
newHeight = SCREEN_HEIGHT - 3;
}
if (newMax >= SCREEN_HEIGHT - 2) {
newMax = SCREEN_HEIGHT - 3;
}
int barX = bar * BARWIDTH + 5;
// 이전 중앙값 레벨 지우기
if (oldHeight[i] > newHeight) {
display.fillRect(barX, newHeight + 1, 7, oldHeight[i], SSD1306_BLACK);
}
// 이전 최대값 레벨 지우기
if (oldMax[i] > newHeight) {
for (int j = oldMax[i]; j > newHeight; j -= 2) {
display.drawFastHLine(barX, j, 7, SSD1306_BLACK);
}
}
// 새 최대값 레벨 그리기 (점선)
for (int j = newMax; j > newHeight; j -= 2) {
display.drawFastHLine(barX, j, 7, SSD1306_WHITE);
}
// 새 중앙값 레벨 그리기 (채워진 막대)
display.fillRect(barX, 1, 7, newHeight, SSD1306_WHITE);
oldMax[i] = newMax;
oldHeight[i] = newHeight;
bar++;
}
display.drawFastHLine(0, SCREEN_HEIGHT - 1, SCREEN_WIDTH, SSD1306_WHITE);
display.display();
}