PianoHat for MIDI
2024.03.23 / 2024.05.15更新
YouTube でも紹介しています。画像をクリックすると再生できます。
USB-MIDI
2022年9月16日「USB-MIDI」では、MIDIキーボードコントローラーをUSB HOST SHIELDに接続しUNOに被せてMIDIコマンドを受け取り、
MUSIC MAKERに流し込むことで音を出していました。

今回はRaspberry Piの拡張ボードであるPIMORONI Piano HAT を入力デバイスとして使用します。
●Piano HAT
PIMORONI Piano HAT はラズベリーパイに被せて使う拡張ボードです。
Piano HAT アプリをインストールするとピアノ88鍵分の音声ファイル88個といつくかのドラムの音源がPianoHATのディレクトリに保存されます。
鍵盤を叩くと音が出て、右上の[OCTAVE▼]、[OCTAVE▲]で音域を切り替え、[INSTRUMENT]でピアノとドラムを切り替えることができます。
また、Piano HATのサンプルコードは、Pythonで記述されています。
ここでは、Piano HATとESP32をI2C接続して、Piano HATに搭載されているCAP1188センサーの情報を取得します。
●補足
以前
Zero 2 W と Bullseye
で取り上げましたが、Piano HATモジュールは、Python 2 用のライブラリに依存していて、Python3の環境では動作しません。
Pimoroni Piano HAT と DAWソフトウェア Rosegarden を連携するための、midi-sequencerでエラーがでていました。
AttributeError: module 'midi.sequencer' has no attribute 'SequencerHardware'
●CAP1188

Piano HATの鍵盤にはAdafruit CAP1188 8Key Capacitive Touch Sensor Breakout と同様に CAP1188 I2C/SPI 8-Channel Capacitive Sensor(静電容量タッチセンサ)が使われています。
人間の身体は導体として働くため、人の指先がタッチパネルに近づくと、指先とセンサー電極間に、コンデンサのような静電容量が発生します。
この静電容量の変化を検知するのが静電容量式センサーです。
1個のCAP1188チップは8チャンネルなので、Piano HATでは2個のCAP1188チップを使用しています。
Adafruit CAP1188 Breakout > Using with Arduino
●Adafruit QT Py ESP32-S2 WiFi Dev Board with STEMMA QT
マイコンボードには、Adafruit QT Py ESP32-S2 を使用しています。Piano HATの鍵盤をタッチして音を鳴らす際の音源は、
メモリ上に配置しているので、2MBのPSRAMを実装しているESP32-S2は便利です。
・ESP32-S2 240MHz
・4 MB Flash & 2 MB PSRAM
・2.4 GHz Wi-Fi (SoC)
・Two I2C ports
・Hardware UART
・Hardware SPI
・Hardware I2S on any pins
・3.3V regulator with 600mA peak output
Ref.Adafruit QT Py ESP32-S2
●Qt Py ESP32-S2 Pinout
●Piano HAT Pinout
Piano HAT at Raspberry Pi GPIO Pinout
・鍵盤上のLEDを光らせないのであれば、5V Power供給は不要です
・GPIO4,27のAlertピンのいずれかをGNDに接続すれば正常に動作します。
・GPIO17,22のResetピンは非接続で大丈夫です。
・2個のCAP1188のアドレスは 0x28 と 0x2b です。
0x28:C,C#,D,D#,E,F,F#,G
0x2b: G#,A,A#,B,C,Instrument,Octave -,Octave +
●Nano HAT Hacker
Piano HATは本来ラズベリーパイに被せて使うものなので、GPIOピンを引き出す必要があります。

Nano HAT Hacker は本来GPIO分岐に用いるのですが、これは便利です。

40pinピンヘッダーを2個はんだ付けして使います。
●組み立て
| UNO | - | レベル変換 | - | ESP32 | - | MusicMaker |
| | | | | TXD | - | RX |
| TXD | - | HV1-LV1 | - | A2 | | |
| RXD | - | HV2-LV2 | - | (A3) | | |
| 5V | - | 5V-3.3V | - | 3.3V | - | 3V |
| GND | - | GND-GND | - | GND | - | GND |
| PianoHat | | | | | | |
| SDA | - | --------- | - | SDA | | |
| SCL | - | --------- | - | SCL | | |
| 3.3 | - | --------- | - | 3.3V | | |
| GND | - | --------- | - | GND | | |
| | | | | | | Battery |
| 5V※ | - | --------- | - | 5V | - | 5V |
※PianoHatの5V端子はLED点灯に使用されますが、QT Pyの5V出力は脆弱なため、5V電源から直接供給します。

左側のUSB HOST SHIELDは今回関係ありません。
右側、ブレッドボードに縦に刺さっている基板上にMUSIC MAKERを取り付けています。

基板の背面に Adafruit QT Py ESP32-S2を取り付けていますi。

演奏の際には、MUSIC MAKERの3.5mm AUDIO OUT端子にアンプ内蔵のスピーカーなどを繋ぎます。
うぷ主は、LINE-IN端子のある充電式ラジオを使っています。
●gitHub
下記gitHubのサンプルコードを参考にして、ESP32用のArduinoコードを作成しました。

adafruit/Adafruit_CAP1188_Library
・Adafruit_CAP1188.cpp
・cap1188test.ino

pimoroni/cap1xxx
・cap1xxx.py

pimoroni/Piano-HAT
・pianohat.py
ソースコード
#include <Wire.h>
#define VS1053_BANK_DEFAULT 0x00
#define GM_ACOUSTIC_PIANO 0x00
#define VELOCITY_FFF 112
#define MIDI_NOTE_ON 0x90
#define MIDI_NOTE_OFF 0x80
#define MIDI_CHAN_MSG 0xB0
#define MIDI_CHAN_BANK 0x00
#define MIDI_CHAN_VOLUME 0x07
#define MIDI_CHAN_PROGRAM 0xC0
#define I2C_CAP1188_A 0x28
#define I2C_CAP1188_B 0x2B
#define CAP1188_RESET_A -1
#define CAP1188_RESET_B -1
#define R_SENSOR_THRESHOLD 0x30
#define R_LED_BEHAVIOUR_1 0x81
#define R_LED_BEHAVIOUR_2 0x82
#define R_LED_LINKING 0x72
#define R_SAMPLING_CONFIG 0x24
#define R_SENSITIVITY 0x1F
#define R_GENERAL_CONFIG 0x20
#define R_CONFIGURATION2 0x44
#define R_LED_DIRECT_RAMP 0x94
#define CAP1188_SENINPUTSTATUS 0x3
#define CAP1188_MTBLK 0x2A
#define CAP1188_LEDLINK 0x72
#define CAP1188_PRODID 0xFD
#define CAP1188_MANUID 0xFE
#define CAP1188_STANDBYCFG 0x41
#define CAP1188_REV 0xFF
#define CAP1188_MAIN 0x00
#define CAP1188_MAIN_INT 0x01
#define CAP1188_LEDPOL 0x73
uint8_t keyPrev[128];
uint8_t keyStatus[128];
#define command(Adr,Reg) I2C_Write_Byte(Adr, Reg, 0x00)
#define data(Adr,Data) I2C_Write_Byte(Adr, Data, 0x40)
uint8_t octave = 4;
int8_t subnote;
#define CHANNEL_0 0
void _delay(uint32_t msec) {
portTickType delay_ms = msec / portTICK_RATE_MS;
vTaskDelay(delay_ms);
}
void midiout_write(uint8_t value) {
Serial1.write(value);
}
void setChannelBank(uint8_t channel, uint8_t bank)
{
midiout_write(MIDI_CHAN_MSG | channel);
midiout_write((uint8_t)MIDI_CHAN_BANK);
midiout_write(bank);
}
void setInstrument(uint8_t channel, uint8_t instrument)
{
midiout_write(MIDI_CHAN_PROGRAM | channel);
midiout_write(instrument);
_delay(10);
}
void send(uint8_t status, uint8_t data1, uint8_t data2)
{
midiout_write(status);
midiout_write(data1);
midiout_write(data2);
}
void change(uint8_t status, uint8_t data)
{
midiout_write(status);
midiout_write(data);
}
void I2C_Write_Byte(uint8_t addr, uint8_t Cmd, uint8_t value)
{
Wire.beginTransmission(addr);
Wire.write(Cmd);
Wire.write(value);
Wire.endTransmission();
}
uint8_t readRegister(uint8_t addr, uint8_t Cmd)
{
int val;
Wire.beginTransmission(addr);
Wire.write(Cmd);
Wire.endTransmission();
Wire.requestFrom(addr, 1);
while (Wire.available() > 0) {
val = Wire.read();
}
return (uint8_t)val;
}
uint8_t cap1188_touched(uint8_t addr) {
uint8_t t = readRegister(addr, CAP1188_SENINPUTSTATUS);
if (t) {
I2C_Write_Byte(addr, CAP1188_MAIN, readRegister(addr, CAP1188_MAIN) & ~CAP1188_MAIN_INT);
}
return t;
}
boolean cap1188_setup(uint8_t addr, int8_t reset) {
uint8_t pid, mid, rev;
if (reset >=0 ) {
pinMode(reset, OUTPUT);
digitalWrite(reset, LOW);
_delay(100);
digitalWrite(reset, HIGH);
_delay(100);
digitalWrite(reset, LOW);
_delay(100);
}
// Useful debugging info
pid = readRegister(addr, CAP1188_PRODID);
mid = readRegister(addr, CAP1188_MANUID);
rev = readRegister(addr, CAP1188_REV);
if ((pid != 0x50) || (mid != 0x5D) || (rev != 0x83)) return false;
I2C_Write_Byte(addr, CAP1188_MTBLK, 0);
I2C_Write_Byte(addr, CAP1188_LEDLINK, 0xFF);
I2C_Write_Byte(addr, CAP1188_STANDBYCFG, 0x30);
for (int i=0;i<8;i++) I2C_Write_Byte(addr, R_SENSOR_THRESHOLD + i, 0b00000110);
I2C_Write_Byte(addr, R_LED_BEHAVIOUR_1, 0b00000000);
I2C_Write_Byte(addr, R_LED_BEHAVIOUR_2, 0b00000000);
I2C_Write_Byte(addr, R_LED_LINKING, 0b11111111);
I2C_Write_Byte(addr, R_SAMPLING_CONFIG, 0b00000000);
I2C_Write_Byte(addr, R_SENSITIVITY, 0b01100000);
I2C_Write_Byte(addr, R_GENERAL_CONFIG, 0b00111000);
I2C_Write_Byte(addr, R_CONFIGURATION2, 0b01100000);
I2C_Write_Byte(addr, R_LED_DIRECT_RAMP, 0);
memset(keyPrev,0x00,128);
return true;
}
boolean cap1188_init(TwoWire *wire)
{
wire->beginTransmission(I2C_CAP1188_A);
if (wire->endTransmission() != 0) {
return false;
} else {
if (!cap1188_setup(I2C_CAP1188_A, CAP1188_RESET_A)) {
return false;
}
}
wire->beginTransmission(I2C_CAP1188_B);
if (wire->endTransmission() != 0) {
return false;
} else {
if (!cap1188_setup(I2C_CAP1188_B, CAP1188_RESET_B)) {
return false;
} else {
return true;
}
}
}
void cap1188_keyon(int oct,int subnote)
{
int noteNo = 12 * (oct + 1) + subnote;
keyStatus[noteNo] = 1;
}
void cap1188_play()
{
for (int no=0; no<128; no++) {
if ((keyPrev[no]==1)&&(keyStatus[no]==0)) {
keyPrev[no] = 0;
noteOff(CHANNEL_0|0x80, no, 0);
} else if ((keyPrev[no]==0)&&(keyStatus[no]==1)) {
keyPrev[no] = 1;
noteOn(CHANNEL_0|0x90, no, VELOCITY_FFF);
}
}
}
void cap1188_status()
{
memset(keyStatus, 0x00,128);
uint8_t touchedA = cap1188_touched(I2C_CAP1188_A);
uint8_t touchedB = cap1188_touched(I2C_CAP1188_B);
if (touchedA & 0x01) cap1188_keyon(octave, 0);
if (touchedA & 0x02) cap1188_keyon(octave, 1);
if (touchedA & 0x04) cap1188_keyon(octave, 2);
if (touchedA & 0x08) cap1188_keyon(octave, 3);
if (touchedA & 0x10) cap1188_keyon(octave, 4);
if (touchedA & 0x20) cap1188_keyon(octave, 5);
if (touchedA & 0x40) cap1188_keyon(octave, 6);
if (touchedA & 0x80) cap1188_keyon(octave, 7);
if (touchedB & 0x01) cap1188_keyon(octave, 8);
if (touchedB & 0x02) cap1188_keyon(octave, 9);
if (touchedB & 0x04) cap1188_keyon(octave,10);
if (touchedB & 0x08) cap1188_keyon(octave,11);
if (touchedB & 0x10) cap1188_keyon(octave,12);
if (touchedB & 0x20) { if (octave > 1 ) octave--; while(cap1188_touched(I2C_CAP1188_B));};
if (touchedB & 0x40) { if (octave < 7 ) octave++; while(cap1188_touched(I2C_CAP1188_B));};
if (touchedB & 0x80) { while(cap1188_touched(I2C_CAP1188_B));};
cap1188_play();
}
void setChannelVolume(uint8_t channel, uint8_t vol)
{
midiout_write(MIDI_CHAN_MSG | channel);
midiout_write(MIDI_CHAN_VOLUME);
midiout_write(vol);
}
void noteOn(uint8_t command, uint8_t note, uint8_t velocity)
{
midiout_write(command);
midiout_write(note);
midiout_write(velocity);
_delay(10);
}
void noteOff(uint8_t command, uint8_t note, uint8_t velocity)
{
midiout_write(command);
midiout_write(note);
midiout_write(velocity);
_delay(10);
}
void setup()
{
Serial1.begin(31250); // midi out
Wire.begin();
cap1188_init(&Wire);
setChannelBank(CHANNEL_0, VS1053_BANK_DEFAULT);
setChannelVolume(CHANNEL_0, VELOCITY_FFF);
setInstrument(CHANNEL_0, GM_ACOUSTIC_PIANO);
}
void loop()
{
cap1188_status();
}
プログラム概要
MIDIコントローラー(キーボード)とPiano HAT との違いは、MIDIコマンドの検出方法です。
MIDIコントローラーでは、鍵盤が押されると、NOTE ONのコマンドが1回のみ送信され、鍵盤を離したときに1回、NOTE OFFのコマンドが送信されます。
待ち受け側のマイコン側では、受信したコマンドを評価して、MUSIC MAKERに送信します。
これに対して、Piano HATでは、マイコン側から、Piano HATに問い合わせを行い、ステータス情報を取得します。
PianoHatは2つのCAP1188タッチセンサが搭載されており、i2Cアドレスはと0x28と0x2Bです。
各々のレジスタ0x03には1バイト・ステータス値の各ビットが8つのキー(鍵盤)に対応しており、ビットに1が立っているものが押されたキーを表しています。
0x28:C,C#,D,D#,E,F,F#,G
0x2b: G#,A,A#,B,C,Instrument,Octave -,Octave +
void loop()
{
cap1188_status();
}
マイコン側はloop()処理により、CAP1188センサーの状態を取得しています。
void cap1188_status()
{
memset(keyStatus, 0x00,128);
uint8_t touchedA = cap1188_touched(I2C_CAP1188_A);
uint8_t touchedB = cap1188_touched(I2C_CAP1188_B);
if (touchedA & 0x01) cap1188_keyon(octave, 0);
if (touchedA & 0x02) cap1188_keyon(octave, 1);
if (touchedA & 0x04) cap1188_keyon(octave, 2);
if (touchedA & 0x08) cap1188_keyon(octave, 3);
if (touchedA & 0x10) cap1188_keyon(octave, 4);
if (touchedA & 0x20) cap1188_keyon(octave, 5);
if (touchedA & 0x40) cap1188_keyon(octave, 6);
if (touchedA & 0x80) cap1188_keyon(octave, 7);
if (touchedB & 0x01) cap1188_keyon(octave, 8);
if (touchedB & 0x02) cap1188_keyon(octave, 9);
if (touchedB & 0x04) cap1188_keyon(octave,10);
if (touchedB & 0x08) cap1188_keyon(octave,11);
if (touchedB & 0x10) cap1188_keyon(octave,12);
if (touchedB & 0x20) {
if (octave > 1 ) octave--;
while(cap1188_touched(I2C_CAP1188_B));
};
if (touchedB & 0x40) {
if (octave < 7 ) octave++;
while(cap1188_touched(I2C_CAP1188_B));
};
if (touchedB & 0x80) {
while(cap1188_touched(I2C_CAP1188_B));
};
cap1188_play();
}
void cap1188_keyon(int oct,int subnote)
{
int noteNo = 12 * (oct + 1) + subnote;
keyStatus[noteNo] = 1;
}
void cap1188_play()
{
int subnote, oct;
for (int no=0; no<128; no++) {
if ((keyPrev[no]==1)&&(keyStatus[no]==0)) {
keyPrev[no] = 0;
noteOff(CHANNEL_0|0x80, no, 0);
} else if ((keyPrev[no]==0)&&(keyStatus[no]==1)) {
keyPrev[no] = 1;
noteOn(CHANNEL_0|0x90, no, VELOCITY_STD);
}
}
}
鍵盤の状態が変化した場合にのみ、MUSIC MAKER にMIDIコマンドを送信しています。
●演奏

鍵盤をタッチすると、押された鍵盤上部のLEDが光り、スピーカーから音が流れます。
掲載ソースでは、アコースティック・ピアノGM音源固定ですが、
[INSTRUMENT]ボタンを押すたびにお気に入りの音色を変えるとよいかもしれません。
●応用
掲載ソースは最低限必要なコードになっていますが、
実際にはTelnet端末にMIDIコードを表示すると同時にログを保存しています。

鍵盤の状態のほかに音の間隔(デルタ・タイム)も算出しています。
デルタ・タイムがゼロの箇所は同時に複数の鍵盤を押しています。
記録したメロディーのお気に入り箇所を抜き出し、先頭のMIDIコードを用いて再現できるようにしています。
2924.05.15追記
●Qwiic端子の実装
・SCL,SDAにプルアップ抵抗を入れています
・5V端子は鍵盤上のLEDを光らせるためのもので、Qwiicの3.3Vを昇圧して5Vを供給しています
●ロジック・レベル変換モジュールを独立
Arduino UNO(5V) ←→ QT Py ESP32S2 (3.3V)
●ESP32汎用機との接続
ESP32 PROGRAM SELECTORを導入したことにより、複数のマイコンを集約しました。
|
Raspberry Pi(ラズベリー パイ)は、ARMプロセッサを搭載したシングルボードコンピュータ。イギリスのラズベリーパイ財団によって開発されている。
たいていのことは100日あれば、うまくいく。長田英知著
「時間がなくて、なかなか自分のやりたいことができない」
「一念発起して何かを始めても、いつも三日坊主で終わってしまう」
「色んなことを先延ばしにしたまま、時間だけが過ぎていく」
そこで本書では、そんな著者が独自に開発した、
まったく新しい目標達成メソッド「100日デザイン」について、
その知識と技術を、余すところなくご紹介します。
まんがで納得ナポレオン・ヒル 思考は現実化する
OLとして雑務をこなす日々に飽き足らず、科学者だった父が残した薬品を商品化すべく、起業を決意した内山麻由(27)。彼女はセミナーで知り合った謎の女性からサポートを得ながら、彼女と二人三脚でナポレオン・ヒルの成功哲学を実践し、さまざまな問題を乗り越えていく。
ヒル博士の<ゴールデンルール>に従い、仕事に、恋に全力疾走する彼女の、成功への物語。
今日は人生最悪で最高の日 1秒で世界を変えるたったひとつの方法 ひすいこたろう著
偉人の伝記を読むと、最悪な日は、不幸な日ではなく、新しい自分が始まる日であることがわかります。最悪な出来事は、自分の人生が、想像を超えて面白くなる兆しなのです。偉人伝を読むことで、このときの不幸があったおかげで、未来にこういう幸せがくるのかと、人生を俯瞰する視線が立ち上がるのです。
ご飯は私を裏切らない heisoku著
辛い現実から目を背けて食べるご飯は、いつも美味しく幸せを届けてくれる。
29歳、中卒、恋人いない歴イコール年齢。バイト以外の職歴もなく、短期バイトを転々とする日々。ぐるぐると思索に耽るけど、ご飯を食べると幸せになれる。奇才の新鋭・heisokuが贈るリアル労働グルメ物語!
【最新版Gemini 3に対応!】できるGemini (できるシリーズ)
Geminiを「最強の知的生産パートナー」として使いこなすための、実践的なノウハウを凝縮した一冊です。
基本的な操作方法から、具体的なビジネスシーンでの活用、日々の業務を自動化するGoogle Workspaceとの連携、さらには自分だけのオリジナルAIを作成する方法まで余すところなく解説します。
Rustプログラミング完全ガイド 他言語との比較で違いが分かる!
Rustの各手法や考え方を幅広く解説!
500以上のサンプルを掲載。実行結果も確認。
全24章の包括的なチュートリアル。
ポチらせる文章術
販売サイト・ネット広告・メルマガ・ブログ・ホームページ・SNS…
全WEB媒体で効果バツグン!
カリスマコピーライターが教える「見てもらう」「買ってもらう」「共感してもらう」すべてに効くネット文章術
小型で便利な Type-C アダプター USB C オス - USB3.1 オスアダプター
Type-C端子のマイコンボードをこのアダプタを介して直接Raspberry Piに挿すことができます。ケーブルなしで便利なツールです。
Divoom Ditoo Pro ワイヤレススピーカー
15W高音質重低音/青軸キーボード/Bluetooth5.3/ピクセルアート 専用アプリ/USB接続/microSDカード
電源供給USBケーブル スリム 【5本セット】
USB電源ケーブル 5V DC電源供給ケーブル スリム 【5本セット】 電源供給 バッテリー 修理 自作 DIY 電子工作 (100cm)
|