Skip to content

Instantly share code, notes, and snippets.

@Yuikawa-Akira
Last active August 31, 2025 23:40
Show Gist options
  • Save Yuikawa-Akira/fbaafee9219ec3f9a38495dc4390f18f to your computer and use it in GitHub Desktop.
Save Yuikawa-Akira/fbaafee9219ec3f9a38495dc4390f18f to your computer and use it in GitHub Desktop.
music_roller
#include <unit_rolleri2c.hpp>
#include <FastLED.h>
#include <SD.h>
#include <M5Unified.h>
#include <M5UnitRCA.h>
// wavファイル
const int maxFile = 100;
String fileList[maxFile];
int fileCount = 0;
int currentFileIndex = 0; // 現在選択されているファイルのインデックス
static constexpr const gpio_num_t SDCARD_CSPIN = GPIO_NUM_4;
static constexpr const size_t buf_num = 3;
static constexpr const size_t buf_size = 1024; // モノラル再生なら可
static uint8_t wav_data[buf_num][buf_size];
// フェーダーの設定
const int ANALOG_PIN = 13;
const int MAX_VOLUME = 255;
const int MIN_VOLUME = 0;
const int LED_PIN = 14;
const int NUM_LEDS = 14;
CRGB leds[NUM_LEDS];
// 再生状態を管理するグローバル変数
static bool isPlaying = false;
static bool isPaused = false;
static bool isReverse = false;
static File currentFile;
static int32_t data_len = 0;
static int32_t currentPos = 0;
static bool flg_16bit = false;
static uint32_t sample_rate = 0;
static bool isStereo = false;
static uint32_t ratio = 0;
struct __attribute__((packed)) wav_header_t {
char RIFF[4];
uint32_t chunk_size;
char WAVEfmt[8];
uint32_t fmt_chunk_size;
uint16_t audiofmt;
uint16_t channel;
uint32_t sample_rate;
uint32_t byte_per_sec;
uint16_t block_size;
uint16_t bit_per_sample;
};
struct __attribute__((packed)) sub_chunk_t {
char identifier[4];
uint32_t chunk_size;
uint8_t data[1];
};
// 実行状態を表す列挙型
enum class RollerStatus {
STOPPED, // 停止中
PLAYING, // 再生中
PAUSED, // 一時停止
ACCELERATING, // 加速中(外力による)
DECELERATING, // 減速中(外力による)
REVERSING // 逆転
};
// 現在のRoller485の状態を格納する構造体
struct RollerState {
RollerStatus status = RollerStatus::STOPPED; // 現在のステータス
float speedRatio = 0.0; // 設定速度に対する比率
};
RollerState rollerState; // グローバル変数として定義
// Create a UNIT_ROLLERI2C object
UnitRollerI2C RollerI2C;
// roller設定
int32_t targetRPM = 10000;
int32_t targetRPMx = 24000;
uint32_t p = 3000000; // 1500000
uint32_t i = 1000; // 1000
uint32_t d = 40000000; // 40000000
int32_t speedTolerance = 600; // 速度の許容誤差
// 外部ディスプレイ 処理能力的に厳しいか?
M5UnitRCA gfx_rca;
// M5UnitRCA gfx_rca ( 216 // logical_width
// , 144 // logical_height
// , 256 // output_width
// , 160 // output_height
// , M5UnitRCA::signal_type_t::PAL // signal_type
// , 26 // GPIO pin
// );
// signal_type: can be selected from NTSC / NTSC_J / PAL / PAL_M / PAL_N.
// GPIO pin: can be selected from 25 / 26
static bool prepareSdWav(const char* filename) {
currentFile = SD.open(filename);
if (!currentFile) { return false; }
wav_header_t wav_header;
currentFile.read((uint8_t*)&wav_header, sizeof(wav_header_t));
if (memcmp(wav_header.RIFF, "RIFF", 4)
|| memcmp(wav_header.WAVEfmt, "WAVEfmt ", 8)
|| wav_header.audiofmt != 1
|| wav_header.bit_per_sample < 8
|| wav_header.bit_per_sample > 16
|| wav_header.channel == 0
|| wav_header.channel > 2) {
currentFile.close();
return false;
}
currentFile.seek(offsetof(wav_header_t, audiofmt) + wav_header.fmt_chunk_size);
sub_chunk_t sub_chunk;
currentFile.read((uint8_t*)&sub_chunk, 8);
while (memcmp(sub_chunk.identifier, "data", 4)) {
if (!currentFile.seek(sub_chunk.chunk_size, SeekMode::SeekCur)) { break; }
currentFile.read((uint8_t*)&sub_chunk, 8);
}
if (memcmp(sub_chunk.identifier, "data", 4)) {
currentFile.close();
return false;
}
// 再生に必要な情報をグローバル変数に保存
data_len = sub_chunk.chunk_size;
currentPos = 0;
flg_16bit = (wav_header.bit_per_sample >> 4);
sample_rate = wav_header.sample_rate;
isStereo = (wav_header.channel > 1);
isPlaying = true; // 再生開始準備完了
isReverse = false; // 順再生から開始
return true;
}
void file_read() {
// SDカードマウント待ち
int time_out = 0;
while (false == SD.begin(SDCARD_CSPIN, SPI, 25000000)) {
if (time_out++ > 6) return;
M5.Lcd.println("SD Wait...");
delay(500);
}
File root = SD.open("/wav");
if (root) {
File file = root.openNextFile();
while (file) {
if (!file.isDirectory()) {
String filename = file.name();
if (filename.indexOf(".wav") != -1) {
fileList[fileCount] = "/wav/" + filename;
fileCount++;
if (maxFile <= fileCount) {
break;
}
}
}
file = root.openNextFile();
}
root.close();
}
}
void updateLcd() {
M5.Lcd.clear();
M5.Lcd.setCursor(0, 0);
M5.Lcd.setTextSize(2);
M5.Lcd.setTextScroll(true);
// 1つ前のファイル名を表示
if (fileCount > 0) {
int prevIndex = (currentFileIndex == 0) ? (fileCount - 1) : (currentFileIndex - 1);
M5.Lcd.println("Prev: " + fileList[prevIndex]);
} else {
M5.Lcd.println("Prev: (none)");
}
M5.Lcd.println("---");
// 現在のファイル名を表示
if (fileCount > 0) {
M5.Lcd.println("Current: " + String(currentFileIndex) + " " + fileList[currentFileIndex]);
} else {
M5.Lcd.println("Current: No files found.");
}
M5.Lcd.println("---");
// 1つ後のファイル名を表示
if (fileCount > 0) {
int nextIndex = (currentFileIndex == fileCount - 1) ? 0 : (currentFileIndex + 1);
M5.Lcd.println("Next: " + fileList[nextIndex]);
} else {
M5.Lcd.println("Next: (none)");
}
}
void setup(void) {
auto cfg = M5.config();
cfg.external_spk = true;
cfg.internal_spk = false;
M5.begin();
//Serial.begin(115200); // デバッグ用
M5.Lcd.clear();
M5.Lcd.setCursor(0, 0);
M5.Lcd.setTextSize(2);
M5.Lcd.setTextColor(ORANGE);
M5.Lcd.setTextScroll(true);
pinMode(ANALOG_PIN, INPUT);
FastLED.addLeds<NEOPIXEL, LED_PIN>(leds, NUM_LEDS);
for (int i = 0; i < NUM_LEDS; i++) {
leds[i] = CRGB::Orange;
}
RollerI2C.begin(&Wire, 0x65, 32, 33, 400000);
delay(1000);
RollerI2C.setMode(ROLLER_MODE_SPEED);
RollerI2C.setSpeedPID(p, i, d);
RollerI2C.setOutput(0);
RollerI2C.setSpeed(targetRPM);
RollerI2C.setSpeedMaxCurrent(5000);
RollerI2C.setRGBMode(ROLLER_RGB_MODE_USER_DEFINED);
delay(500);
RollerI2C.setRGB(TFT_ORANGE);
if (M5.getBoard() == m5gfx::boards::board_M5StackCore2) { // 想定はCore2
{
auto spk_cfg = M5.Speaker.config();
spk_cfg.i2s_port = I2S_NUM_1;
spk_cfg.sample_rate = 96000;
spk_cfg.pin_data_out = 2;
spk_cfg.pin_bck = 19;
spk_cfg.pin_ws = 0; // LRCK
spk_cfg.stereo = true;
spk_cfg.buzzer = false;
spk_cfg.use_dac = false;
spk_cfg.magnification = 16;
M5.Speaker.config(spk_cfg);
}
} else {
auto spk_cfg = M5.Speaker.config();
spk_cfg.i2s_port = I2S_NUM_1;
spk_cfg.sample_rate = 96000;
spk_cfg.pin_data_out = 15;
spk_cfg.pin_bck = 13;
spk_cfg.pin_ws = 0; // LRCK
spk_cfg.stereo = true;
spk_cfg.buzzer = false;
spk_cfg.use_dac = false;
spk_cfg.magnification = 16;
M5.Speaker.config(spk_cfg);
}
M5.Speaker.begin();
M5.Speaker.setVolume(0);
file_read();
updateLcd(); // 初期表示
}
void loop(void) {
M5.update();
// フェーダーからの値を読み取り、音量を設定
int analog_value = analogRead(ANALOG_PIN);
int volume_value = map(analog_value, 0, 4095, MIN_VOLUME, MAX_VOLUME);
M5.Speaker.setVolume(volume_value);
FastLED.setBrightness(volume_value / 4); // 明るすぎるので
FastLED.show();
// ローラー情報
//static int32_t lastEncoderValue = 0;
//int32_t currentEncoderValue = RollerI2C.getPosReadback();
int32_t currentRPM = RollerI2C.getSpeedReadback();
// ステータス判定ロジック
if (currentRPM < -speedTolerance) {
rollerState.status = RollerStatus::REVERSING;
} else {
rollerState.status = RollerStatus::PLAYING;
ratio = sample_rate;
}
// 速度比率の計算
rollerState.speedRatio = abs((float)currentRPM) / (float)targetRPM;
// rpmをそのまま再生速度に反映すると音痴になるのでしきい値を設ける
if (rollerState.speedRatio < 0.9 || 1.10 < rollerState.speedRatio) {
ratio = sample_rate * rollerState.speedRatio;
Serial.println(ratio); // ←消すとなんかおかしくなる
} else {
ratio = sample_rate;
}
// BtnA: ファイル番号デクリメント (ループ)
if (M5.BtnA.wasPressed() && !isPlaying) {
if (fileCount > 0) {
currentFileIndex = (currentFileIndex == 0) ? (fileCount - 1) : (currentFileIndex - 1);
updateLcd();
}
}
// BtnC: ファイル番号インクリメント (ループ)
if (M5.BtnC.wasPressed() && !isPlaying) {
if (fileCount > 0) {
currentFileIndex = (currentFileIndex == fileCount - 1) ? 0 : (currentFileIndex + 1);
updateLcd();
}
}
// ボタンB: 再生
if (M5.BtnB.wasPressed()) {
if (!isPlaying && fileCount > 0) {
RollerI2C.setOutput(1);
delay(1000);
M5.Lcd.println("Start Playing");
if (prepareSdWav(fileList[currentFileIndex].c_str())) {
currentPos = 0;
}
ratio = sample_rate;
}
}
// BtnB長押し: 停止
if (M5.BtnB.wasHold()) {
if (isPlaying) {
isPlaying = false; // 再生ループを終了
M5.Speaker.end();
updateLcd();
}
}
// 早送り
if (M5.BtnC.isPressed() && isPlaying) {
RollerI2C.setSpeed(targetRPMx);
} else if (!isReverse) {
RollerI2C.setSpeed(targetRPM);
}
// 逆再生
if (rollerState.status == RollerStatus::REVERSING || (M5.BtnA.isPressed() && isPlaying)) {
if (isPlaying) {
isReverse = true;
if (rollerState.status == RollerStatus::REVERSING) {
} else {
RollerI2C.setSpeed(-targetRPMx);
}
}
} else {
isReverse = false;
}
if (M5.BtnA.wasReleased() && isPlaying) {
RollerI2C.setSpeed(targetRPM);
}
/*
// 一時停止?
if (rollerState.status == RollerStatus::PAUSED) {
if (isPlaying) {
isPaused = true;
Serial.println("PAUSED");
M5.Speaker.stop(); // 一時停止時にスピーカー出力を停止
}
} else {
isPaused = false;
}
*/
// 再生方向切り替え時のポインタ位置調整
if (isReverse) {
currentFile.seek(currentPos - buf_size, SeekMode::SeekSet);
} else {
currentFile.seek(currentPos, SeekMode::SeekSet);
}
int interval = 0;
// 再生速度の下限
if (ratio < sample_rate * 0.2) {
ratio = sample_rate * 0.2;
}
static size_t idx = 0;
if (isPlaying && !isPaused) {
size_t len_read = 0;
/*
M5.Lcd.fillRect(0, 190, 320, 50, M5.Lcd.getRawColor()); // 描画エリアをクリア
int progress_bar_width = 300;
int progress_bar_height = 20;
int progress_bar_x = (320 - progress_bar_width) / 2;
int progress_bar_y = 190;
float progress = (float)currentPos / (float)data_len;
int filled_width = progress * progress_bar_width;
M5.Lcd.drawRect(progress_bar_x, progress_bar_y, progress_bar_width, progress_bar_height, WHITE);
M5.Lcd.fillRect(progress_bar_x, progress_bar_y, filled_width, progress_bar_height, BLUE);
*/
if (!isReverse) {
if (currentPos >= data_len) {
isPlaying = false;
} else {
len_read = data_len - currentPos > buf_size ? buf_size : data_len - currentPos;
currentFile.read(wav_data[idx], len_read);
currentPos += len_read;
}
} else { // 逆再生の場合
if (currentPos <= 0) {
isPlaying = false;
} else {
len_read = currentPos > buf_size ? buf_size : currentPos;
currentFile.seek(currentPos - len_read, SeekMode::SeekSet);
currentFile.read(wav_data[idx], len_read);
currentPos -= len_read;
// ビット深度に応じてサンプル単位でデータを反転させる
if (flg_16bit) {
int16_t* p = (int16_t*)wav_data[idx];
size_t sample_count = len_read / sizeof(int16_t);
for (size_t i = 0; i < sample_count / 2; ++i) {
int16_t temp = p[i];
p[i] = p[sample_count - 1 - i];
p[sample_count - 1 - i] = temp;
}
} else {
// 8ビットはそのままバイト単位で反転
uint8_t* p = wav_data[idx];
for (size_t i = 0; i < len_read / 2; ++i) {
uint8_t temp = p[i];
p[i] = p[len_read - 1 - i];
p[len_read - 1 - i] = temp;
}
}
}
}
if (isPlaying) {
if (flg_16bit) {
M5.Speaker.playRaw((const int16_t*)wav_data[idx], len_read >> 1, ratio, isStereo, 1, 0);
} else {
M5.Speaker.playRaw((const uint8_t*)wav_data[idx], len_read, ratio, isStereo, 1, 0);
}
idx = idx < (buf_num - 1) ? idx + 1 : 0;
delay(interval);
} else {
currentFile.close();
}
} else {
RollerI2C.setOutput(0);
rollerState.status = RollerStatus::STOPPED;
}
//Serial.println(currentEncoderValue - lastEncoderValue);
//lastEncoderValue = currentEncoderValue;
//delay(2);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment