shigobuのアイコン画像
shigobu 2021年02月28日作成 (2022年02月02日更新) © CC BY 4+
製作品 製作品 閲覧数 6164
shigobu 2021年02月28日作成 (2022年02月02日更新) © CC BY 4+ 製作品 製作品 閲覧数 6164

ArduinoにYMZ294を3台つないで同時発音数9のMIDI(っぽい)音源を作る

ArduinoにYMZ294を3台つないで同時発音数9のMIDI(っぽい)音源を作る

YMZ294

秋月電子で音源ICのページを見ると、はじめから曲が内蔵されているメロディーICが連なる中、異彩を放つ(ように見えた)ヤマハ音源ICの文字。データシートを見ると、Arduinoで簡単に制御できそうだったので即購入。これを使って、ベートーヴェンの月光を演奏させることにしました。

用意したもの

部品 数量 購入先
YMZ294 3個 秋月電子 I-12141
クリスタルオシレータ 8MHz (2分周して4MHzで使用) 1個 秋月電子 P-01566
2回路Dフリップフロップ TC74HC74AP(クロックの分周用) 1個 秋月電子 I-10879
3.5mmステレオミニジャックDIP化キット 1個 秋月電子 K-05363
Arduino UNO 1個
抵抗10Ω 1個 秋月電子 R-25100
抵抗1kΩ 1個 秋月電子 R-25102
ジャンプワイヤ(付け根が細いもの) 適量 千石電商

クロック周波数

YMZ294を駆動させるマスタークロックは、4MHzか6MHzを選択できます。
データシートを見ると、はじめの方にある特徴の項目には、「4MHzまたは、8MHzから選択」と書いてありますが、これは間違いです。
実際に8MHzをつないで音を出すと、少し高い音が出ました。
データシートには、8MHzと6MHzの記述が混在していますが、6MHzが正解です。

今回は、8MHzのオシレーターを購入してしまったので、Dフリップフロップで2分周して4MHzで駆動させました。

YMZ294のライブラリ

ライブラリを作成して公開している方がいますが、作りたかったので自作しました。

YMZ294.h

#ifndef YMZ294_H #define YMZ294_H enum AddressData{ Address = 0, Data = 1 }; enum Addresses{ ChA1, ChA2, ChB1, ChB2, ChC1, ChC2, Noise, Mixer, VolumeA, VolumeB, VolumeC, EnvelopeFreqLSB, EnvelopeFreqMSB, EnvelopeShape }; enum Channel{ ChA, ChB, ChC, Noise1, Noise2, Noise3, }; enum MixerOnOff{ On, Off, }; enum EnvelopeShapes{ sh1 = 0b1000, sh2, sh3, sh4, sh5, sh6, sh7, sh8, }; class YMZ294{ private: YMZ294(); public: YMZ294(int wr, int cs, int ad, int d0, int d1, int d2, int d3, int d4, int d5, int d6, int d7); YMZ294(int wr, int ad, int d0, int d1, int d2, int d3, int d4, int d5, int d6, int d7); void begin(); void noteOn(Channel ch, uint8_t noteNum); void noteOff(Channel ch); void selectAddressData(AddressData ad); void setDataBus(uint8_t data); void setData(uint8_t data); void setAddress(Addresses data); void setFrequency(Channel ch, uint16_t TP); void setVolume(Channel ch, uint8_t volume); void setMixer(Channel ch, MixerOnOff mixer); void setEnvelopeFrequency(uint16_t freq) ; void setEnvelopeShape(EnvelopeShapes shape); private: int WR; int CS; int AD; int D0; int D1; int D2; int D3; int D4; int D5; int D6; int D7; bool isCsAvailable; uint8_t currentMixerVal = 0xFF; const int16_t TP[128]; }; #endif

YMZ294.cpp

#include <arduino.h> #include "YMZ294.h" YMZ294::YMZ294() : TP{ 15289,14431,13621,12856,12135,11454,10811,10204,9631,9091,8581,8099,7645,7215,6810,6428,6067,5727, 5405,5102,4816,4545,4290,4050,3822,3608,3405,3214,3034,2863,2703,2551,2408,2273,2145,2025,1911,1804, 1703,1607,1517,1432,1351,1276,1204,1136,1073,1012,956,902,851,804,758,716,676,638,602,568,536,506,478, 451,426,402,379,358,338,319,301,284,268,253,239,225,213,201,190,179,169,159,150,142,134,127,119,113,106, 100,95,89,84,80,75,71,67,63,60,56,53,50,47,45,42,40,38,36,34,32,30,28,27,25,24,22,21,20,19,18,17,16,15,14,13,13,12,11,11,10 }{} YMZ294::YMZ294(int wr, int cs, int ad, int d0, int d1, int d2, int d3, int d4, int d5, int d6, int d7) : YMZ294() { WR = wr; CS = cs; AD = ad; D0 = d0; D1 = d1; D2 = d2; D3 = d3; D4 = d4; D5 = d5; D6 = d6; D7 = d7; isCsAvailable = true; } YMZ294::YMZ294(int wr, int ad, int d0, int d1, int d2, int d3, int d4, int d5, int d6, int d7) : YMZ294() { WR = wr; AD = ad; D0 = d0; D1 = d1; D2 = d2; D3 = d3; D4 = d4; D5 = d5; D6 = d6; D7 = d7; isCsAvailable = false; } void YMZ294::begin(){ pinMode(D0, OUTPUT); pinMode(D1, OUTPUT); pinMode(D2, OUTPUT); pinMode(D3, OUTPUT); pinMode(D4, OUTPUT); pinMode(D5, OUTPUT); pinMode(D6, OUTPUT); pinMode(D7, OUTPUT); pinMode(AD, OUTPUT); pinMode(WR, OUTPUT); if (isCsAvailable) { pinMode(CS, OUTPUT); digitalWrite(CS, HIGH); } setVolume(Channel::ChA, 0x0f); setVolume(Channel::ChB, 0x0f); setVolume(Channel::ChC, 0x0f); setMixer(Channel::Noise1, MixerOnOff::Off); setMixer(Channel::Noise2, MixerOnOff::Off); setMixer(Channel::Noise3, MixerOnOff::Off); } void YMZ294::noteOn(Channel ch, uint8_t noteNum){ setFrequency(ch, TP[noteNum]); setMixer(ch, MixerOnOff::On); } void YMZ294::noteOff(Channel ch){ setMixer(ch, MixerOnOff::Off); } void YMZ294::selectAddressData(AddressData ad){ digitalWrite(AD, ad); } void YMZ294::setDataBus(uint8_t data){ if (isCsAvailable) { digitalWrite(CS, LOW); } digitalWrite(WR, LOW); digitalWrite(D7, data & 0b10000000); digitalWrite(D6, data & 0b01000000); digitalWrite(D5, data & 0b00100000); digitalWrite(D4, data & 0b00010000); digitalWrite(D3, data & 0b00001000); digitalWrite(D2, data & 0b00000100); digitalWrite(D1, data & 0b00000010); digitalWrite(D0, data & 0b00000001); digitalWrite(WR, HIGH); if (isCsAvailable) { digitalWrite(CS, HIGH); } } void YMZ294::setData(uint8_t data){ selectAddressData(AddressData::Data); setDataBus(data); } void YMZ294::setAddress(Addresses data){ selectAddressData(AddressData::Address); setDataBus(data); } void YMZ294::setFrequency(Channel ch, uint16_t TP){ Addresses MSBaddr; Addresses LSBaddr; switch (ch) { case Channel::ChA: LSBaddr = Addresses::ChA1; MSBaddr = Addresses::ChA2; break; case Channel::ChB: LSBaddr = Addresses::ChB1; MSBaddr = Addresses::ChB2; break; case Channel::ChC: LSBaddr = Addresses::ChC1; MSBaddr = Addresses::ChC2; break; default: return; } setAddress(LSBaddr); setData(TP & 0x00ff); setAddress(MSBaddr); setData((TP & 0xff00) >> 8); } void YMZ294::setVolume(Channel ch, uint8_t volume){ Addresses addr; switch (ch) { case Channel::ChA: addr = Addresses::VolumeA; break; case Channel::ChB: addr = Addresses::VolumeB; break; case Channel::ChC: addr = Addresses::VolumeC; break; default: return; } setAddress(addr); setData(volume & 0x0f); } void YMZ294::setMixer(Channel ch, MixerOnOff mixer){ uint8_t mixerValue = 0; setAddress(Addresses::Mixer); switch (ch) { case Channel::ChA: mixerValue = 0b1; break; case Channel::ChB: mixerValue = 0b10; break; case Channel::ChC: mixerValue = 0b100; break; case Channel::Noise1: mixerValue = 0b1000; break; case Channel::Noise2: mixerValue = 0b10000; case Channel::Noise3: mixerValue = 0b100000; break; default: return; } if (mixer == MixerOnOff::On){ currentMixerVal &= ~mixerValue; } else{ currentMixerVal |= mixerValue; } setData(currentMixerVal); } void YMZ294::setEnvelopeFrequency(uint16_t freq) { setAddress(Addresses::EnvelopeFreqLSB); setData(freq & 0x00ff); setAddress(Addresses::EnvelopeFreqMSB); setData(freq >> 8); } void YMZ294::setEnvelopeShape(EnvelopeShapes shape){ setAddress(Addresses::EnvelopeShape); setData(shape); setAddress(Addresses::VolumeA); setData(0xff); }

GitHubにソースの全体を上げました。
https://github.com/shigobu/Arduino_YMZ294Library

回路図

回路図

配線

配線はブレッドボードで行いました。
はじめは、秋月電子で購入した激安のジャンパーワイヤを使用しました。しかしこのジャンパーワイヤは、ピンの付け根が太く一列に連続でブレッドボードに刺すと、隣と干渉して抜き差しがしづらくなりました。
なので、ちょっと高いですが千石電商でサンハヤト製のものを購入しました。ピンの付け根が細く使いやすいです。

全体像

MIDIデータの受信と再生

MIDIデータの受信には、「Arduino MIDI Library」を使用しました。
Arduino MIDI Library(GitHub)

MIDI再生コード

#include <MIDI.h> #include <YMZ294.h> #define D0 12 #define D1 11 #define D2 10 #define D3 9 #define D4 8 #define D5 7 #define D6 6 #define D7 5 #define AD 13 #define WR 4 #define CS1 3 #define CS2 2 #define CS3 A5 YMZ294 ymz1(WR, CS1, AD, D0, D1, D2, D3, D4, D5, D6, D7); YMZ294 ymz2(WR, CS2, AD, D0, D1, D2, D3, D4, D5, D6, D7); YMZ294 ymz3(WR, CS3, AD, D0, D1, D2, D3, D4, D5, D6, D7); MIDI_CREATE_DEFAULT_INSTANCE(); void setup() { ymz1.begin(); ymz2.begin(); ymz3.begin(); delay(10); MIDI.begin(MIDI_CHANNEL_OMNI); } void loop() { if(MIDI.read()) { midi::Channel ch = MIDI.getChannel(); switch(MIDI.getType()) // Get the type of the message we caught { case midi::NoteOn: switch (ch) { case 1: case 2: case 3: if (MIDI.getData2() == 0) { ymz1.noteOff(static_cast<Channel>(ch - 1)); } else { ymz1.noteOn(static_cast<Channel>(ch - 1), MIDI.getData1()); } break; case 4: case 5: case 6: if (MIDI.getData2() == 0) { ymz2.noteOff(static_cast<Channel>(ch - 4)); } else { ymz2.noteOn(static_cast<Channel>(ch - 4), MIDI.getData1()); } break; case 7: case 8: case 9: if (MIDI.getData2() == 0) { ymz3.noteOff(static_cast<Channel>(ch - 7)); } else { ymz3.noteOn(static_cast<Channel>(ch - 7), MIDI.getData1()); } break; default: break; } break; case midi::NoteOff: switch (ch) { case 1: case 2: case 3: ymz1.noteOff(static_cast<Channel>(ch - 1)); case 4: case 5: case 6: ymz2.noteOff(static_cast<Channel>(ch - 4)); break; case 7: case 8: case 9: ymz3.noteOff(static_cast<Channel>(ch - 7)); break; default: break; } case midi::ControlChange: midi::DataByte data1 = MIDI.getData1(); switch (data1) { case midi::ExpressionController: //エクスプレッション int vol = map(MIDI.getData2(), 0, 0x7f, 0, 0x0f); switch (ch) { case 1: case 2: case 3: ymz1.setVolume(static_cast<Channel>(ch - 1), vol); case 4: case 5: case 6: ymz2.setVolume(static_cast<Channel>(ch - 4), vol); break; case 7: case 8: case 9: ymz3.setVolume(static_cast<Channel>(ch - 7), vol); break; default: break; } break; default: break; } default: break; } } }

PCとの接続

実は、これはMIDI規格に準じた音源では有りません。AruduinoをPCにつなぐとMIDIデバイスとしてではなく、普通にCOMポートとして認識されます。COMポートを使いArduinoとはUARTで通信します。
タイトルに「MIDI(っぽい)」と書いたのは、これが理由です。

MIDI信号の流れ

月光の再生

早速楽譜を購入し、打ち込みを行いました。
楽譜は、「ぷりんと楽譜」のサービスを使用し、セブンイレブンのコピー機でプリントしました。
ピアノソナタ第14番-月光<第1楽章>(楽譜)- ヤマハ「ぷりんと楽譜」

打ち込みはCubaseを使いました。
すべて同じトラックに打ち込み、ノートエクスプレッションの機能を使用しノートをMIDIチャンネルに分配しました。

エンベロープを使いたいのですが、YMZ294のエンベロープは1チャンネルしかなく、すべてのチャンネルで同じエンベロープが使用されてしまいます。
なので、エンベロープはMIDIデータにエクスプレッションとして入力するようにしました。
ノート毎にエンベロープを手作業で書くのは大変なので、自動でエンベロープを挿入するアプリを作成しました。

こうしてMIDIデータが完成しました。
MIDIデータをCubaseに読み込ませ、再生しながら録音を行います。
録音したデータをYouTubeにアップしました。

ここに動画が表示されます

まとめ

今回は、ブレッドボード上に回路を構成しましたが、次は基盤に実装してみたいと思います。

shigobuのアイコン画像
arduinoを触ってから、マイコンを使った電子工作に目覚めました。
ログインしてコメントを投稿する