arduino midi - · pdf filearduino midi pieter p, 08-03-2017 this is a guide that covers the...
TRANSCRIPT
ArduinoMIDIPieterP, 08-03-2017
ThisisaguidethatcoversthebasicsoftheMusicalInstrumentDigitalInterface(MIDI)protocolanditsimplementationontheArduinoplatform.Theformatoftheprotocolisexplainedinthefirstchapter.Chaptertwogoesoverthehardware.Inchapterthree,examplecodeforsendingMIDIispresented.ChapterfourcontainseverythingneededtobuildaworkingMIDIcontroller.MIDIinputiscoveredinchapterfive,andchaptersixextendsthisbyaddingsupportforSystemExclusive(SysEx)messages.
TheMIDIprotocolTheMIDIspecificationcanbefoundhere:https://www.midi.org/specifications/item/the-midi-1-0-specification
TheMIDIprotocoldescribesasetofMIDIevents.Forexample,anoteisplayed,oranoteisturnedoff,acontrollerismovedandsettoanewvalue,anewinstrumentisselected,etc.TheseeventscorrespondtoMIDImessagesthatcanbesentovertheMIDIhardwareconnection.
Therearetwomaintypesofmessages:channelmessagesandsystemmessages.Mostperformanceinformationwillbesentaschannelmessages,whilesystemmessagesareusedforthingslikeproprietaryhandshakes,manufacturer-specificsettings,sendinglongpacketsofdata,real-timemessagesforsynchronizationandtuning,andotherthingsthatarenotreallyofinteresttosomeonewhojustwantstomakeanArduinoMIDIinstrumentorcontroller.That'swhythisguidewillmainlyfocusonchannelmessages.
ChannelmessagesThereare16MIDIchannels.EachMIDIinstrumentcanplaynotesononeofthesechannels,andtheycanapplydifferentvoicesorpatchestodifferentchannels,aswellassettingsomecontrollerslikevolume,pan,balance,sustainpedal,pitchbend,etc.MIDImessagesthattargetaspecificchannelarecalledchannelmessages.
AMIDIchannelmessageconsistofaheaderbyte,referredtoasthestatusbyte,followedbyoneortwodatabytes:
Status ─ Data
Status ─ Data1─ Data2
Eachbyteconsistsof8binarydigits.Todistinguishbetweenstatusanddatabytes,andtopreventframingerrors,statusbyteshavethemostsignificantbit(msb)settoone(1),anddatabyteshavethemsbsettozero(0).
Statusbyte Databyte1 Databyte2(optional)Bit 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0Value 1 x x x x x x x 0 x x x x x x x 0 x x x x x x x
Statusbytes
Thestatusbyteofchannelmessagesisdividedintotwo4-bitnibbles.Thehighnibble(bits4-7)specifiesthemessagetype,andthelownibble(bits0-3)specifiestheMIDIchannel.Becausethemostsignificantbithastobeone,thereare8differentmessagetypes(0b1000-0b1111or0x8-0xF),and16differentchannels(0x0-0xF).Messagetype0xFisusedforsystemmessages,soitwon'tbecoveredinthissectiononchannelmessages.
StatusbyteBit 7 6 5 4 3 2 1 0Value m m m m n n n nWheremmmmisthemessagetype(0x8-0xE)andnnnnisthechannelnibble.Notethatthechannelsstartfromnnnn=0forMIDIchannel1.(nnnn=channel-1)
Databytes
Eachdatabytecontainsa7-bitvalue,anumberbetween0and127(0b01111111or0x7F).Themeaningofthisvaluedependsonthemessagetype.Forexample,itcantellthereceiverwhatnoteisplayed,howhardthekeywasstruck,whatinstrumenttoselect,whatvalueacontrollerissetto,etc.
ChannelMessages:messagetypes
Thefollowingsectionwillgooverthedifferentchannelmessagesandtheirstatusanddatabytes.Keepinmindthatnnnn=channel-1.
NoteOff(0x8)
Anoteoffeventisusedtostopaplayingnote.Forexample,whenakeyisreleased.
Data1(0b0kkkkkkk): Notenumber(key).SeeMIDInotenames.Data2(0b0vvvvvvv): Velocity(howfastthekeyisreleased).
Avelocityof0isnotdefined,andsomesoftwareordevicesmaynotregisterthenoteoffeventifthevelocityiszero.Mostsoftwareordeviceswillignorethenoteoffvelocity.Insteadofanoteoffevent,anoteoneventwithavelocityofzeromaybeused.Thisisespeciallyusefulwhenusingarunningstatus.
Statusbyte Notenumber VelocityBit 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0Value 1 0 0 0 n n n n 0 k k k k k k k 0 v v v v v v v
NoteOn(0x9)
Anoteoneventisusedtoplayanote.Forexample,whenakeyispressed.
Data1(0b0kkkkkkk): Notenumber(key).SeeMIDInotenames.Data2(0b0vvvvvvv): Velocity(howfast/hardthekeyispressed).
Ifthevelocityiszero,thenoteoneventisinterpretedasanoteoffevent.Thisisespeciallyusefulwhenusingarunningstatus.
Statusbyte Notenumber VelocityBit 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0Value 1 0 0 1 n n n n 0 k k k k k k k 0 v v v v v v v
Polyphonickeypressure(0xA)
Apolyphonickeypressureeventisusedwhenthepressureonakeyorapressuresensitivepadchangesafterthenoteonevent.
Data1(0b0kkkkkkk): Notenumber(key).SeeMIDInotenames.Data2(0b0vvvvvvv): Pressureonthekey.
MostnormalMIDIkeyboardsdonotimplementthisevent.Keypressureissometimesreferredtoasafter-touchorafter-pressure.
Statusbyte Notenumber PressureBit 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0Value 1 0 1 0 n n n n 0 k k k k k k k 0 v v v v v v v
Controlchange(0xB)
Acontrolchangeeventisusedwhenthevalueofacontrollerchanges.
Data1(0b0ccccccc): Controllernumber.SeeControllernumbers.Data2(0b0vvvvvvv): Thevalueofthecontroller.
Controllernumbers120-127arereservedas"ChannelModeMessages".
Statusbyte Controllernumber ValueBit 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0Value 1 0 1 1 n n n n 0 c c c c c c c 0 v v v v v v v
Programchange(0xC)
Aprogramchangeeventisusedtochangetheprogram(i.e.sound,voice,tone,presetorpatch)ofagivenchannelischanged.
Data1(0b0ppppppp): Programnumber.SeeProgramnumbers.
Controllernumbers120-127arereservedas"ChannelModeMessages".
Statusbyte ProgramnumberBit 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0Value 1 1 0 0 n n n n 0 c c c c c c c
Channelpressure(0xD)
Achannelpressureeventisusedwhenthepressureonakeyorapressuresensitivepadchangesafterthenoteonevent.Unlikepolyphonickeypressure,channelpressureaffectsallnotesplayingonthechannel.
Data1(0b0vvvvvvv): Pressurevalue.
MostnormalMIDIkeyboardsdonotimplementthisevent.Channelpressureissometimesreferredtoasafter-touchorafter-pressure.
Statusbyte PressureBit 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0Value 1 1 0 1 n n n n 0 v v v v v v v
Pitchbendchange(0xE)
Apitchbendchangeeventisusedtoalterthepitchofthenotesplayedonagivenchannel.
Data1(0b0lllllll): Leastsignificantbyte(bits0-7)ofthepitchbendvalue.Data2(0b0mmmmmmm): Mostsignificantbyte(bits8-13)ofthepitchbendvalue.
Thecenterposition(nopitchchange)isrepresentedbyLSB=0x0,MSB=0x40
Statusbyte LSB MSBBit 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0Value 1 1 1 0 n n n n 0 l l l l l l l 0 m m m m m m m
Runningstatus
Therearealotofcircumstanceswhereyouhavetosendmanymessagesofthesametype.Forexample,ifyouhaveadigitalkeyboard,prettymuchallmessageswillbenoteonandnoteoffevents,orwhenyouturnaknobonaMIDIcontroller,alotofcontrolchangemessageswillbesenttoupdatethecontrollervalue.Tosavebandwidthinthesekindsofsituations,youonlyhavetosendthestatusbyteonce,followedbyonlydatabytes.Thistechniqueiscalled"runningstatus".Becausenoteoneventsaremostlikelytobefollowedbynoteoffevents(ormorenoteonevents),theMIDIstandardallowsyoutouseanoteoneventwithavelocityofzeroinsteadofanoteoffevent.Thismeansthatyouonlyneedonestatusbyteforallnoteevents,drasticallyreducingthedatathroughput,thusminimizingthedelaybetweenevents.
SystemMessages
SystemmessagesareMIDImessagesthatdonotcarrydataforaspecificMIDIchannel.Therearethreetypesofsystemmessages:
SystemCommonMessages
SystemCommonmessagesareintendedforallreceiversinthesystem.Thesemessagesarebeyondthescopeofthisguide.Ifyouwantmoreinformation,refertopage27oftheMIDI1.0DetailedSpecification4.2.
MIDITimeCodeQuarterFrame(0xF1)SongPositionPointer(0xF2)SongSelect(0xF3)TuneRequest(0xF6)EOX(EndofExclusive)(0xF7)
SystemRealTimeMessages
SystemRealTimemessagesareusedforsynchronizationbetweenclock-basedMIDIcomponents.Thesemessagesarebeyondthescopeofthisguide.Ifyouwantmoreinformation,refertopage30oftheMIDI1.0DetailedSpecification4.2.
TimingClock(0xF8)Start(0xFA)Continue(0xFB)Stop(0xFC)ActiveSensing(0xFE)SystemReset(0xFF)
SystemExclusiveMessages
SystemExclusive(SysEx)messagesareusedforthingslikesettingsynthesizerorpatchsettings,sendingsamplerdata,memorydumps,etc.MostSysExmessagesaremanufacturer-specific,soitisbesttoconsulttheMIDIimplementationinthemanual.Ifyouwantmoreinformationonthetopic,youcanfinditonpage34oftheMIDI1.0DetailedSpecification4.2.
Asystemexclusivemessagestartswithastatusbyte0xF0,followedbyanarbitrarynumberofdatabytes,andendswithanotherstatusbyte0xF7.
SysExstart Data … SysExendBit 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 … 7 6 5 4 3 2 1 0Value 1 1 1 1 0 0 0 0 0 d d d d d d d … 1 1 1 1 0 1 1 1
AppendicesAllnumbersareinhexadecimalrepresentation,unlessotherwisespecified.
MIDInotenames
MiddleCorC4isdefinedasMIDInote0x3C.Thelowestnoteonastandard88-keypianoisA0(0x15)andthehighestnoteisC8(0x6C).
NoteOctave C C# D D# E F F# G G# A A# B
-1 00 01 02 03 04 05 06 07 08 09 0A 0B0 0C 0D 0E 0F 10 11 12 13 14 15 16 171 18 19 1A 1B 1C 1D 1E 1F 20 21 22 232 24 25 26 27 28 29 2A 2B 2C 2D 2E 2F3 30 31 32 33 34 35 36 37 38 39 3A 3B4 3C 3D 3E 3F 40 41 42 43 44 45 46 475 48 49 4A 4B 4C 4D 4E 4F 50 51 52 536 54 55 56 57 58 59 5A 5B 5C 5D 5E 5F7 60 61 62 63 64 65 66 67 68 69 6A 6B8 6C 6D 6E 6F 70 71 72 73 74 75 76 779 78 79 7A 7B 7C 7D 7E 7F
Controllernumbers
ThisisanoverviewoftheMIDIcontrollernumbersthatcanbeusedasthefirstdatabyteofacontrolchangeevent.Theseconddatabyteisthevalueforthecontroller.Thisvalueis7bitswide,sohasarangeof[0,127].Controllernumbers0x00-0x1Fcanbecombinedwithnumbers0x20-0x3Ffor14-bitresolution.Inthiscase,numbers0x00-0x1FsettheMSB,andnumbers0x20-0x3FtheLSB.Controllernumbers120-127arereservedforChannelModeMessages,whichratherthancontrollingsoundparameters,affectthechannel'soperatingmode.Controller Function Value Used
Dec Hex as0 00 BankSelect 00-7F MSB1 01 ModulationWheelorLever 00-7F MSB2 02 BreathController 00-7F MSB3 03 Undefined 00-7F MSB4 04 FootController 00-7F MSB5 05 PortamentoTime 00-7F MSB6 06 DataEntryMSB 00-7F MSB7 07 ChannelVolume(formerlyMainVolume) 00-7F MSB8 08 Balance 00-7F MSB9 09 Undefined 00-7F MSB10 0A Pan 00-7F MSB11 0B ExpressionController 00-7F MSB12 0C EffectControl1 00-7F MSB13 0D EffectControl2 00-7F MSB14 0E Undefined 00-7F MSB15 0F Undefined 00-7F MSB16 10 GeneralPurposeController1 00-7F MSB17 11 GeneralPurposeController2 00-7F MSB18 12 GeneralPurposeController3 00-7F MSB19 13 GeneralPurposeController4 00-7F MSB20 14 Undefined 00-7F MSB21 15 Undefined 00-7F MSB22 16 Undefined 00-7F MSB23 17 Undefined 00-7F MSB24 18 Undefined 00-7F MSB25 19 Undefined 00-7F MSB26 1A Undefined 00-7F MSB27 1B Undefined 00-7F MSB28 1C Undefined 00-7F MSB29 1D Undefined 00-7F MSB30 1E Undefined 00-7F MSB31 1F Undefined 00-7F MSB32 20 LSBforControl0(BankSelect) 00-7F LSB33 21 LSBforControl1(ModulationWheelorLever) 00-7F LSB34 22 LSBforControl2(BreathController) 00-7F LSB35 23 LSBforControl3(Undefined) 00-7F LSB36 24 LSBforControl4(FootController) 00-7F LSB37 25 LSBforControl5(PortamentoTime) 00-7F LSB38 26 LSBforControl6(DataEntry) 00-7F LSB39 27 LSBforControl7(ChannelVolume,formerlyMainVolume) 00-7F LSB40 28 LSBforControl8(Balance) 00-7F LSB41 29 LSBforControl9(Undefined) 00-7F LSB42 2A LSBforControl10(Pan) 00-7F LSB43 2B LSBforControl11(ExpressionController) 00-7F LSB44 2C LSBforControl12(Effectcontrol1) 00-7F LSB45 2D LSBforControl13(Effectcontrol2) 00-7F LSB46 2E LSBforControl14(Undefined) 00-7F LSB47 2F LSBforControl15(Undefined) 00-7F LSB48 30 LSBforControl16(GeneralPurposeController1) 00-7F LSB49 31 LSBforControl17(GeneralPurposeController2) 00-7F LSB50 32 LSBforControl18(GeneralPurposeController3) 00-7F LSB
51 33 LSBforControl19(GeneralPurposeController4) 00-7F LSB52 34 LSBforControl20(Undefined) 00-7F LSB53 35 LSBforControl21(Undefined) 00-7F LSB54 36 LSBforControl22(Undefined) 00-7F LSB55 37 LSBforControl23(Undefined) 00-7F LSB56 38 LSBforControl24(Undefined) 00-7F LSB57 39 LSBforControl25(Undefined) 00-7F LSB58 3A LSBforControl26(Undefined) 00-7F LSB59 3B LSBforControl27(Undefined) 00-7F LSB60 3C LSBforControl28(Undefined) 00-7F LSB61 3D LSBforControl29(Undefined) 00-7F LSB62 3E LSBforControl30(Undefined) 00-7F LSB63 3F LSBforControl31(Undefined) 00-7F LSB64 40 DamperPedalon/off(Sustain) ≤3Foff,≥40on ---65 41 PortamentoOn/Off ≤3Foff,≥40on ---66 42 SostenutoOn/Off ≤3Foff,≥40on ---
67 43 SoftPedalOn/Off ≤3Foff,≥40on ---
68 44 LegatoFootswitch ≤3FNormal,≥40Legato ---
69 45 Hold2 ≤3Foff,≥40on ---70 46 SoundController1(default:SoundVariation) 00-7F LSB71 47 SoundController2(default:Timbre/HarmonicIntens.) 00-7F LSB72 48 SoundController3(default:ReleaseTime) 00-7F LSB73 49 SoundController4(default:AttackTime) 00-7F LSB74 4A SoundController5(default:Brightness) 00-7F LSB75 4B SoundController6(default:DecayTime-seeMMARP-021) 00-7F LSB76 4C SoundController7(default:VibratoRate-seeMMARP-021) 00-7F LSB77 4D SoundController8(default:VibratoDepth-seeMMARP-021) 00-7F LSB78 4E SoundController9(default:VibratoDelay-seeMMARP-021) 00-7F LSB79 4F SoundController10(defaultundefined-seeMMARP-021) 00-7F LSB80 50 GeneralPurposeController5 00-7F LSB81 51 GeneralPurposeController6 00-7F LSB82 52 GeneralPurposeController7 00-7F LSB83 53 GeneralPurposeController8 00-7F LSB84 54 PortamentoControl 00-7F LSB85 55 Undefined --- ---86 56 Undefined --- ---87 57 Undefined --- ---88 58 HighResolutionVelocityPrefix 00-7F LSB89 59 Undefined --- ---90 5A Undefined --- ---
91 5B Effects1Depth(default:ReverbSendLevel-seeMMARP-023)(formerlyExternalEffectsDepth) 00-7F ---
92 5C Effects2Depth(formerlyTremoloDepth) 00-7F ---
93 5D Effects3Depth(default:ChorusSendLevel-seeMMARP-023)(formerlyChorusDepth) 00-7F ---
94 5E Effects4Depth(formerlyCeleste[Detune]Depth) 00-7F ---95 5F Effects5Depth(formerlyPhaserDepth) 00-7F ---96 60 DataIncrement(DataEntry+1)(seeMMARP-018) N/A ---97 61 DataDecrement(DataEntry-1)(seeMMARP-018) N/A ---98 62 Non-RegisteredParameterNumber(NRPN)-LSB 00-7F LSB99 63 Non-RegisteredParameterNumber(NRPN)-MSB 00-7F MSB100 64 RegisteredParameterNumber(RPN)-LSB* 00-7F LSB
101 65 RegisteredParameterNumber(RPN)-MSB* 00-7F MSB102 66 Undefined --- ---103 67 Undefined --- ---104 68 Undefined --- ---105 69 Undefined --- ---106 6A Undefined --- ---107 6B Undefined --- ---108 6C Undefined --- ---109 6D Undefined --- ---110 6E Undefined --- ---111 6F Undefined --- ---112 70 Undefined --- ---113 71 Undefined --- ---114 72 Undefined --- ---115 73 Undefined --- ---116 74 Undefined --- ---117 75 Undefined --- ---118 76 Undefined --- ---119 77 Undefined --- ---
Channelmode Function ValueDec Hex120 78 AllSoundOff 00121 79 ResetAllControllers 00122 7A LocalControlOn/Off 00off,7Fon123 7B AllNotesOff 00
124 7C OmniModeOff(+allnotesoff) 00
125 7D OmniModeOn(+allnotesoff) 00
126 7E MonoModeOn(+polyoff,+allnotesoff)
Note:Thisequalsthenumberofchannels,orzeroifthenumberofchannelsequalsthenumberofvoicesinthereceiver.
127 7F PolyModeOn(+monooff,+allnotesoff) 0
Source
Programnumbers
TheMIDIspecificationdoesn'tspecifyinstrumentsorvoicesforprogramnumbers.TheGeneralMIDI1soundsetdoesdefinealistofsoundsandfamiliesofsounds.Program FamilyName1-8 Piano9-16 ChromaticPercussion17-24 Organ25-32 Guitar33-40 Bass41-48 Strings49-56 Ensemble57-64 Brass65-72 Reed73-80 Pipe81-88 SynthLead89-96 SynthPad
97-104 SynthEffects
105-112 Ethnic113-120 Percussive121-128 SoundEffects
Program InstrumentName1 AcousticGrandPiano2 BrightAcousticPiano3 ElectricGrandPiano4 Honky-tonkPiano5 ElectricPiano16 ElectricPiano27 Harpsichord8 Clavi9 Celesta10 Glockenspiel11 MusicBox12 Vibraphone13 Marimba14 Xylophone15 TubularBells16 Dulcimer17 DrawbarOrgan18 PercussiveOrgan19 RockOrgan20 ChurchOrgan21 ReedOrgan22 Accordion23 Harmonica24 TangoAccordion25 AcousticGuitar(nylon)26 AcousticGuitar(steel)27 ElectricGuitar(jazz)28 ElectricGuitar(clean)29 ElectricGuitar(muted)30 OverdrivenGuitar31 DistortionGuitar32 Guitarharmonics33 AcousticBass34 ElectricBass(finger)35 ElectricBass(pick)36 FretlessBass37 SlapBass138 SlapBass239 SynthBass140 SynthBass241 Violin42 Viola43 Cello44 Contrabass45 TremoloStrings46 PizzicatoStrings47 OrchestralHarp48 Timpani
49 StringEnsemble150 StringEnsemble251 SynthStrings152 SynthStrings253 ChoirAahs54 VoiceOohs55 SynthVoice56 OrchestraHit57 Trumpet58 Trombone59 Tuba60 MutedTrumpet61 FrenchHorn62 BrassSection63 SynthBrass164 SynthBrass265 SopranoSax66 AltoSax67 TenorSax68 BaritoneSax69 Oboe70 EnglishHorn71 Bassoon72 Clarinet73 Piccolo74 Flute75 Recorder76 PanFlute77 BlownBottle78 Shakuhachi79 Whistle
80 Ocarina81 Lead1(square)82 Lead2(sawtooth)83 Lead3(calliope)84 Lead4(chiff)85 Lead5(charang)86 Lead6(voice)87 Lead7(fifths)88 Lead8(bass+lead)89 Pad1(newage)90 Pad2(warm)91 Pad3(polysynth)92 Pad4(choir)93 Pad5(bowed)94 Pad6(metallic)95 Pad7(halo)96 Pad8(sweep)97 FX1(rain)98 FX2(soundtrack)99 FX3(crystal)100 FX4(atmosphere)
101 FX5(brightness)102 FX6(goblins)103 FX7(echoes)104 FX8(sci-fi)105 Sitar106 Banjo107 Shamisen108 Koto109 Kalimba110 Bagpipe111 Fiddle112 Shanai113 TinkleBell114 Agogo115 SteelDrums116 Woodblock117 TaikoDrum118 MelodicTom119 SynthDrum120 ReverseCymbal
121 GuitarFretNoise122 BreathNoise123 Seashore124 BirdTweet125 TelephoneRing126 Helicopter127 Applause128 Gunshot
Source
MIDIhardwareTheMIDIhardwarelinkisjusta5mAcurrentloopthatasynchronouslysendsandreceives8-bitbytesatabaudrateof31250symbolspersecond.ThismeansthattheArduino'shardwareUARTcanbeusedfortransmittingandreceivingMIDI.DIN5pin(180degree)femalereceptaclesareusedforMIDIin,outandthroughconnectors.
Thisistheoriginalschematicthatcanbefoundinthe1996MIDI1.0DetailedSpecification4.2:
Thecurrentloopconsistsofaanopencollectoroutputonthetransmittingend(MIDIoutandMIDIthrough),andanopto-isolatoratthereceivingend(MIDIin).Whena'zero'issent,theopencollectoroutputsinkscurrent,turningontheLEDoftheopto-isolator.Thiswillinturnbringlowtheopencollectoroutputoftheopto-isolator,resultinginalowsignal.Thereasonforusingacurrentloopinsteadofavoltage,isthatthesenderandthereceivercan
beatdifferentpotentials,becauseeverythingisgalvanicallyisolated.Thisalsopreventsgroundloops,whichcanresultinnoise.Notethatthegroundandshielding(pin2onthe5-pinDINconnector)isconnectedtothegroundoftheMIDIoutandthroughcircuits,butnottothegroundofthereceiverintheMIDIincircuit.
Thestandardwasupdatedin2014toincludespecificationsfor3.3VMIDIdevices.(MIDI1.0ElectricalSpecificationUpdate(CA-033)(2014).MMATechnicalStandardsBoard/AMEIMIDICommittee.)
Pin2mustbetiedtogroundontheMIDItransmitteronly.
ThebufferbetweentheUARTtransmitterandRC isoptionalandsystem-dependent.
TheUARTisconfiguredwith8databits,noparity,and1stopbit,or8-N-1.
Theresistorvaluesdependonthetransmissionsignalingvoltage,VTX,asdetailedbelow.
Theoptionalferritebeadsare1k-ohmat100MHzsuchasMMZ1608Y102BTorsimilar.
VTX +5V±10% +3.3V±5%
RA 220Ω5%0.25W 33Ω5%0.5W
RC 220Ω5%0.25W 10Ω5%0.25W
31,250bits/secUARTReceiver
VTX
RE
RF
N/CN/C
THRU
RB220
RD
VRX OptionalMIDIThruCircuit
ChooseRE andRF basedonVTX inthesamewayasdescribedforMIDIOutRA
andRC
Opto-Isolatorsuchas
PC900Vor6N138
IN
1
2
34
5
1
2
34
5
DonotconnectanypinsoftheMIDIINjackdirectlytoground ValueofRD
dependsonopto-isolatorandVRX.
Recommendedvalueis280ΩforPC900V
withVRX=5V.
D11N914
FB31K@100MHz
FB41K@100MHz
N/CN/C
OptionalferritebeadstoimproveEMI/EMCperformance
FB51K@100MHz
FB61K@100MHz
OptionalferritebeadstoimproveEMI/EMCperformance
Reversevoltageprotectionforopto-isolator
Jackshield– N/CoroptionalgroundtoimproveEMI/EMCperformance
Jackshield– N/Coroptionalsmallcapacitor(0.1 µFtypical)toimprove
EMI/EMCperformance
Pin2– N/Coroptionalsmallcapacitor(0.1 µFtypical)toimprove RFgrounding
SendingMIDIoverSerialTheeasiestwaytosendoutMIDIpacketsistousetheSerial.write(uint8_tdata);function.Thisfunctionwritesoutone8-bitbyteovertheSerialconnection(eitherhardwareUART0orthevirtualCOMportoverUSB).TosendoutaMIDIpacket,wejusthavetowriteoutthethreebytesthatmakeupthepacket:firstthestatusbyte,thenthetwodatabytes.
voidsendMIDI(uint8_tstatusByte,uint8_tdataByte1,uint8_tdataByte2){Serial.write(statusByte);Serial.write(dataByte1);Serial.write(dataByte2);}
InordertosupportMIDIpacketswithonlyonedatabyteaswell,wecanjustoverloadthesendMIDIfunction.Thismeansthatwecreatetwofunctionswiththesamename,butwithdifferentparameters.
voidsendMIDI(uint8_tstatusByte,uint8_tdataByte){Serial.write(statusByte);Serial.write(dataByte);}
Initscurrentform,thesendMIDIfunctionisquitesilly.AlthoughitsendsoutMIDIpackets,itdoesn'tautomaticallycreatethesepacketsforus,westillhavetoputtogetherthestatusanddatabytesourselves,andwehavetomakesurethatitisavalidMIDIpacketbeforecallingsendMIDI.Let'screateamoreusefulfunctionthattakesamessagetype,channelnumberanddataasinputs,createsaMIDIpacket,andsendsitovertheSerialport.
voidsendMIDI(uint8_tmessageType,uint8_tchannel,uint8_tdata1,uint8_tdata2){channel--;//Decrementthechannel,becauseMIDIchannel1correspondstobinarychannel0uint8_tstatusByte=messageType|channel;//CombinethemessageType(highnibble)//withthechannel(lownibble)//Boththemessagetypeandthechannelshouldbe4bitswideSerial.write(statusByte);Serial.write(data1);Serial.write(data2);}
WenowhaveaworkingfunctionthatsendsMIDIpackets,andtakesasomewhatsensibleinput,notjustthebytesofthepacket.Butthere'sstillnoguaranteethatitisavalidMIDImessage.Rememberthatthestatusbyteshouldhaveamostsignificantbitequalto1,andthedatabytesamostsignificantbitequalto0.We'llusesomebitwisemathtomakesurethatthisisalwaysthecase,nomatterwhatdatatheuserenters.
voidsendMIDI(uint8_tmessageType,uint8_tchannel,uint8_tdata1,uint8_tdata2){channel--;//Decrementthechannel,becauseMIDIchannel1//correspondstobinarychannel0uint8_tstatusByte=messageType|channel;//CombinethemessageType(highnibble)//withthechannel(lownibble)//Boththemessagetypeandthechannel//shouldbe4bitswidestatusByte|=0b10000000;//Setthemostsignificantbitofthestatusbytedata1&=0b01111111;//Clearthemostsignificantbitofthedatabytesdata2&=0b01111111;Serial.write(statusByte);//SendoverSerialSerial.write(data1);Serial.write(data2);}
voidsendMIDI(uint8_tmessageType,uint8_tchannel,uint8_tdata){channel--;//Decrementthechannel,becauseMIDIchannel1//correspondstobinarychannel0uint8_tstatusByte=messageType|channel;//CombinethemessageType(highnibble)//withthechannel(lownibble)//Boththemessagetypeandthechannel//shouldbe4bitswide
statusByte|=0b10000000;//Setthemostsignificantbitofthestatusbytedata&=0b01111111;//ClearthemostsignificantbitofthedatabyteSerial.write(statusByte);//SendoverSerialSerial.write(data);}
Beforesendingthepacket,wesetthemostsignificantbitofthestatusbytebyperformingabitwiseORoperation:
0bxsssssss0b10000000-----------|0b1sssssss
Where0bsssssssisthestatus,andxiseither1or0.Asyoucansee,nomatterthevalueofx,theresultwillalwaysbe0b1sssssss.WealsoclearthemostsignificantbitsofthedatabytesbyperformingabitwiseANDoperation:
0bxddddddd0b01111111-----------&0b0ddddddd
Where0bdddddddisthedata,andxiseither1or0.Nomatterwhatthevalueofxis,theresultwillalwaysbe0b0ddddddd
Youcouldgoevenfurtherbymakingsurethatthemessagetypeandthechanneldon'tinterferewitheachother.However,thatmightbeoverlydefensive.
voidsendMIDI(uint8_tmessageType,uint8_tchannel,uint8_tdata1,uint8_tdata2){channel--;//Decrementthechannel,becauseMIDIchannel1//correspondstobinarychannel0messageType&=0b11110000;//Makesurethatonlythehighnibble//ofthemessagetypeissetchannel&=0b00001111;//Makesurethatonlythelownibble//ofthechannelissetuint8_tstatusByte=messageType|channel;//CombinethemessageType(highnibble)//withthechannel(lownibble)//Boththemessagetypeandthechannel//shouldbe4bitswidestatusByte|=0b10000000;//Setthemostsignificantbitofthestatusbytedata1&=0b01111111;//Clearthemostsignificantbitofthedatabytesdata2&=0b01111111;Serial.write(statusByte);//SendoverSerialSerial.write(data1);Serial.write(data2);}
ImprovingreadabilityTosendaControlChange(0xB0)messageonchannel3forcontroller80withavalueof64,youwouldcallsendMIDI(0xB0,3,80,64);Tomakeitalittlemoreobviouswhat'sgoingon,wecoulddeclaresomeconstantsforthedifferentmessagetypes:
constuint8_tNOTE_OFF=0x80;constuint8_tNOTE_ON=0x90;constuint8_tKEY_PRESSURE=0xA0;constuint8_tCC=0xB0;constuint8_tPROGRAM_CHANGE=0xC0;constuint8_tCHANNEL_PRESSURE=0xD0;constuint8_tPITCH_BEND=0xE0;
YoucannowusesendMIDI(CC,3,80,64);whichwillmakethecodemucheasiertoread.Whenwritingcode,it'salwaysagoodideatokeepso-calledmagicnumberstoaminimum.Theseareseeminglyarbitrarynumericliteralsinyourcodethatdon'thaveaclearmeaning.Forexample,thiscodesnippetplaysachromaticglissando(allkeys,oneaftertheother)onanhonky-tonkpiano:
sendMIDI(0xC0,1,4);for(uint8_ti=21;i<=108;i++){sendMIDI(0x90,1,i,64);delay(100);
sendMIDI(0x80,1,i,64);}
Tosomeonewhohasneverseenthecode,orsomeonewhodoesn'tknowallMIDImessagetypecodesbyheart,it'snotclearwhatallthesenumbersmean.Amuchbettersketchwouldbe:
constuint8_thonkyTonkPiano=4;//GMdefinestheHonky-tonkPianoasinstrument#4
constuint8_tnote_A1=21;//lowestnoteonan88-keypianoconstuint8_tnote_C9=108;//highestnoteonan88-keypiano
uint8_tchannel=1;//MIDIchannel1uint8_tvelocity=64;//64=mezzoforte
sendMIDI(PROGRAM_CHANGE,channel,honkyTonkPiano);for(uint8_tnote=note_A1;note<=note_C9;note++){//chromaticglissandooverall88pianokeyssendMIDI(NOTE_ON,channel,note,velocity);delay(100);sendMIDI(NOTE_OFF,channel,note,velocity);}
Thissnippetdoesexactlythesamethingasthepreviousexample,butit'smucheasiertoreadandunderstand.
Usingstructs
AnotherapproachwouldbetocomposetheMIDImessageinabuffer,andthenjustwriteoutthatbuffer.WecandefineastructwiththedifferentfieldsofaMIDIevent.Takealookatthisstruct:
typedefstructMIDI_message_3B{unsignedintchannel:4;//secondnibble:MIDIchannel(0-15)unsignedintstatus:3;//firstnibble:statusmessageunsignedint_msb0:1;//mostsignificantbitofstatusbyte:shouldbe1accordingtoMIDIspecificationunsignedintdata1:7;//secondbyte:firstvalueunsignedint_msb1:1;//mostsignificantbitoffirstdatabyte:shouldbe0accordingtoMIDIspecificationunsignedintdata2:7;//thirdbyte:secondvalueunsignedint_msb2:1;//mostsignificantbitofseconddatabyte:shouldbe0accordingtoMIDIspecificationMIDI_message_3B():_msb0(1),_msb1(0),_msb2(0){}//setthecorrectmsb'sforMIDI};
Youmighthavenoticedthatthebitfieldsareinthewrongorder:forexample,thenormalorderofthestatusbytewouldbe1.mmm.ccccwithmmmthemessagetypeandccccthechannel.However,theorderinourstructiscccc.mmm.1.Tounderstandwhat'sgoingon,youhavetoknowthatArduinosareLittleEndian.Thismeansthatthefirstbitfieldtakesuptheleastsignificantbitsineachbyte.Inotherwords,thebitfieldswithineachbyteareinreversedorder,comparedtotheconventionalBigEndiannotation(thatisusedintheMIDIspecification).
Youcannowfillupallfieldsofthestruct,tocreateavalidMIDIpacket.Youdon'thavetoworryaboutthemostsignificantbitsofeachbyte,bitmaskingisdoneautomatically,becauseofthebitfields.Thesebitsaresettothecorrectvaluewhenamessageiscreated,intheinitializerlistoftheconstructor.Theonlythingyouneedtokeepinmindisthatthechannelsarezero-based.Alsonotethatthemessagetypesarenolonger0x80,0x90etc.,but0x8,0x9...
constuint8_tNOTE_ON=0x9;MIDI_message_3Bmsg;//Createavariablecalled'msg'ofthe'MIDI_message_3B'typewejustdefinedmsg.status=NOTE_ON;msg.channel=channel-1;//MIDIchannelsstartfrom0,sosubtract1msg.data1=note_A1;msg.data2=velocity;
Finally,youcanjustwriteoutthemessageovertheSerialport.We'llcreateanotheroverloadofthesendMIDIfunction:
voidsendMIDI(MIDI_message_3Bmsg){Serial.write((uint8_t*)&msg,3);}
We'reusingthewrite(uint8_t*buffer,size_tnumberOfBytes)function.Thefirstargumentisapointertoabuffer(orarray)ofdatabytestowriteout.Thepointerpointstothefirstelementofthisarray.Thesecondargumentisthenumberofbytestosend,startingfromthatfirstelement.There'soneminorproblem:msgisnotanarray,it'sanobjectoftypeMIDI_message_3B.Thewritefunctionexpectsapointertoanarrayofbytes(uint8_t).Togetaroundthis,wecanjusttaketheaddressofmsg,usingtheaddress-ofoperator(&)andcastittoapointertoanarrayofuint8_t'susing(uint8_t*).WeneedtowriteouttheentireMIDIpacket,whichis3byteslong,sothesecondargumentisjust3.Tousethefunction,justuse:
sendMIDI(msg);
Infact,wecoulddoevenbetter.NoweverytimethesendMIDIfunctioniscalled,themsgobjectiscopied.Thistakestimeandmemory.Topreventitfrombeingcopied,wecanpassonlyareferencetomsgtothefunction.Here'swhatthatlookslike:
voidsendMIDI(MIDI_message_3B&msg){Serial.write((uint8_t*)&msg,3);}
sendMIDI(msg);
Youcandothesamethingfortwo-byteMIDIpackets:
typedefstructMIDI_message_2B{unsignedintchannel:4;//secondnibble:MIDIchannel(0-15)unsignedintstatus:3;//firstnibble:messagetypeunsignedint_msb0:1;//mostsignificantbitofstatusbyte:shouldbe1accordingtoMIDIspecificationunsignedintdata:7;//secondbyte:firstvalueunsignedint_msb1:1;//mostsignificantbitoffirstdatabyte:shouldbe0accordingtoMIDIspecificationMIDI_message_2B():_msb0(1),_msb1(0){}//setthecorrectmsb'sforMIDI};
voidsendMIDI(MIDI_message_2B&msg){Serial.write((uint8_t*)&msg,2);}
Runningstatus
Asdiscussedinchapter1,youcanuserunningstatusestosavebandwidth.Theimplementationisrelativelyeasy:rememberthelaststatusbyte(header)thatwassent,andthencompareeveryfollowingstatusbytetothisheader.Ifit'sthesamestatus,sendthedatabytesonly,otherwise,sendthenewstatusbyte,andrememberthisheader.Torememberthepreviousheader,astaticvariableisused.Staticvariablesarenotdestroyedwhentheygooutofscope,sothevalueisretainedthenexttimethesendMIDIHeaderfunctionisexecuted.
voidsendMIDIHeader(uint8_theader){staticuint8_trunningHeader=0;if(header!=runningHeader){//IfthenewheaderisdifferentfromthepreviousSerial.write(header);//SendthestatusbyteoverSerialrunningHeader=header;//Rememberthenewheader}}
voidsendMIDI(uint8_tmessageType,uint8_tchannel,uint8_tdata1,uint8_tdata2){channel--;//Decrementthechannel,becauseMIDIchannel1//correspondstobinarychannel0uint8_tstatusByte=messageType|channel;//CombinethemessageType(highnibble)//withthechannel(lownibble)
//Boththemessagetypeandthechannel//shouldbe4bitswidestatusByte|=0b10000000;//Setthemostsignificantbitofthestatusbytedata1&=0b01111111;//Clearthemostsignificantbitofthedatabytesdata2&=0b01111111;
sendMIDIHeader(statusByte);//SendtheheaderoverSerial,usingrunningstatusSerial.write(data1);//SendthedatabytesoverSerialSerial.write(data2);}
voidsendMIDI(uint8_tmessageType,uint8_tchannel,uint8_tdata){channel--;//Decrementthechannel,becauseMIDIchannel1//correspondstobinarychannel0uint8_tstatusByte=messageType|channel;//CombinethemessageType(highnibble)//withthechannel(lownibble)//Boththemessagetypeandthechannel//shouldbe4bitswidestatusByte|=0b10000000;//Setthemostsignificantbitofthestatusbytedata&=0b01111111;//Clearthemostsignificantbitofthedatabyte
sendMIDIHeader(statusByte);//SendtheheaderoverSerial,usingrunningstatusSerial.write(data);//SendthedatabyteoverSerial}
Goingevenfurther,wecanreplacenoteoffeventsbynoteoneventswithavelocityofzero:
voidsendMIDI(uint8_tmessageType,uint8_tchannel,uint8_tdata1,uint8_tdata2){if(messageType==NOTE_OFF){//ReplacenoteoffmessagesmessageType=NOTE_ON;//withanoteonmessagedata2=0;//withavelocityofzero.}channel--;//Decrementthechannel,becauseMIDIchannel1//correspondstobinarychannel0uint8_tstatusByte=messageType|channel;//CombinethemessageType(highnibble)//withthechannel(lownibble)//Boththemessagetypeandthechannel//shouldbe4bitswidestatusByte|=0b10000000;//Setthemostsignificantbitofthestatusbytedata1&=0b01111111;//Clearthemostsignificantbitofthedatabytesdata2&=0b01111111;
sendMIDIHeader(statusByte);//SendtheheaderoverSerial,usingrunningstatusSerial.write(data1);//SendthedatabytesoverSerialSerial.write(data2);}
Toensurethatthereceiverwillknowwhattodowiththedata,evenifitmissedthefirstheaderbyte,itisagoodideatosendaheaderbyteregularly.Thiscanbedonebyrememberingthetimethelastheaderwassent:
constunsignedlongheaderResendTime=1000;//sendanewheadereverysecond
voidsendMIDIHeader(uint8_theader){staticunsignedlonglastHeaderTime=millis();staticuint8_trunningHeader=0;if(header!=runningHeader//Ifthenewheaderisdifferentfromtheprevious||(millis()-lastHeaderTime)>headerResendTime){//Orifthelastheaderwassentmorethan1sagoSerial.write(header);//SendthestatusbyteoverSerialrunningHeader=header;//RememberthenewheaderlastHeaderTime=millis();}}
Finishedcode
Youcannowputthesefunctionsinaseparatefile,sothatyoucanuseitinallofyoursketches.SaveitassendMIDI.h,you'llneeditinthefollowingchapters.
#ifndefsendMIDI_h_#definesendMIDI_h_
constuint8_tNOTE_OFF=0x80;constuint8_tNOTE_ON=0x90;constuint8_tKEY_PRESSURE=0xA0;constuint8_tCC=0xB0;constuint8_tPROGRAM_CHANGE=0xC0;constuint8_tCHANNEL_PRESSURE=0xD0;constuint8_tPITCH_BEND=0xE0;
constunsignedlongheaderResendTime=1000;//sendanewheadereverysecond
voidsendMIDIHeader(uint8_theader){staticunsignedlonglastHeaderTime=millis();staticuint8_trunningHeader=0;
if(header!=runningHeader//Ifthenewheaderisdifferentfromtheprevious||(millis()-lastHeaderTime)>headerResendTime){//Orifthelastheaderwassentmorethan1sagoSerial.write(header);//SendthestatusbyteoverSerialrunningHeader=header;//RememberthenewheaderlastHeaderTime=millis();}}
voidsendMIDI(uint8_tmessageType,uint8_tchannel,uint8_tdata1,uint8_tdata2){if(messageType==NOTE_OFF){//ReplacenoteoffmessagesmessageType=NOTE_ON;//withanoteonmessagedata2=0;//withavelocityofzero.}channel--;//Decrementthechannel,becauseMIDIchannel1//correspondstobinarychannel0uint8_tstatusByte=messageType|channel;//CombinethemessageType(highnibble)//withthechannel(lownibble)//Boththemessagetypeandthechannel//shouldbe4bitswidestatusByte|=0b10000000;//Setthemostsignificantbitofthestatusbytedata1&=0b01111111;//Clearthemostsignificantbitofthedatabytesdata2&=0b01111111;
sendMIDIHeader(statusByte);//SendtheheaderoverSerial,usingrunningstatusSerial.write(data1);//SendthedatabytesoverSerialSerial.write(data2);}voidsendMIDI(uint8_tmessageType,uint8_tchannel,uint8_tdata){channel--;//Decrementthechannel,becauseMIDIchannel1//correspondstobinarychannel0uint8_tstatusByte=messageType|channel;//CombinethemessageType(highnibble)//withthechannel(lownibble)//Boththemessagetypeandthechannel//shouldbe4bitswidestatusByte|=0b10000000;//Setthemostsignificantbitofthestatusbytedata&=0b01111111;//Clearthemostsignificantbitofthedatabyte
sendMIDIHeader(statusByte);//SendtheheaderoverSerial,usingrunningstatusSerial.write(data);//SendthedatabyteoverSerial}
#endif
MIDIControllersTheMIDIprotocolisoftenusedforMIDIcontrollers,deviceswithphysicalknobsandbuttonstocontrolsettingsinaDigitalAudioWorkstation(DAW),ortoenternotesinaudioormusicnotationsoftware.Thisisoftenmuchfasterandmoreintuitivethanusingthemouseandkeyboardforeverything.MIDIcontrollersarealsousedduringliveperformances,tocontroleffectmodules,samplers,synthesizers,DJsoftware,etc.ThischapterwillcoverhowtowritethecodeforaworkingMIDIcontrollerusingArduino.
ButtonsForsendingthestateofabutton,noteeventsareused.Whenthebuttonispressed,anoteoneventissent,whenit'sreleased,anoteoffeventissent.
Hardware
ConnectingabuttontotheArduinoisprettystraightforward:Connectoneleadofthebuttontoadigitalinputpin,andconnecttheotherleadtoground.
+5V
S1
Internalpull-upresistorR1
Arduinodigitalinput
Theinternalpull-upresistor*oftheinputpinwillbeused,soifthebuttonisreleased(ifitdoesn'tconduct),theinputwillbe"pulledup"to5V,anditwillreadadigital1.Whenthebuttonispressed,itconnectstheinputpindirectlytoground,soitwillreadadigital0.
(*)Themicrocontrollerhasbuilt-inpull-upresistors,tomakeworkingwithbuttonsandopen-collectoroutputsawholeloteasier.Thisresistorcanbeenabledinsoftware,usingpinMode(pushButtonPin,INPUT_PULLUP).Thismeansthatyoudon'thavetoaddaresistorexternally.
Software
TheMIDIcontrolleronlyhastosendeventswhenthestateofthebuttonchanges.Todothis,theinputwillconstantlybepolledintheloop,andthenthepreviousstateiskeptinastaticvariable.Whenthenewinputstatedoesnotequalthepreviousstate,thestateofthebuttonhaschanged,andaMIDIeventwillbesent.Ifthenewstateislow,thebuttonhasbeenpressedandanoteoneventissent.Ifit'shigh,ithasbeenreleased,andanoteoffeventissent.
constuint8_tpushButtonPin=2;
constuint8_tchannel=1;//MIDIchannel1constuint8_tnote=0x3C;//MiddleC(C4)constuint8_tvelocity=0x7F;//Maximumvelocity
voidsetup(){pinMode(pushButtonPin,INPUT_PULLUP);//Enabletheinternalpull-upresistorSerial.begin(31250);}
voidloop(){staticboolpreviousState=HIGH;//Declareastaticvariabletosavethepreviousstate
//andinitializeittoHIGH(notpressed).boolcurrentState=digitalRead(pushButtonPin);//Readthecurrentstateoftheinputpinif(currentState!=previousState){//Ifthecurrentstateisdifferentfromthepreviousstateif(currentState==LOW){//IfthebuttonispressedsendMIDI(NOTE_ON,channel,note,velocity);//Sendanoteonevent}else{//IfthebuttonisreleasedsendMIDI(NOTE_OFF,channel,note,velocity);//Sendanoteoffevent}previousState=currentState;//Rememberthecurrentstateofthebutton}}
Keepinmindthatthedeclarationandinitializationofastaticlocalvariablehappenonlyonce,thevalueisretainedthenexttimethefunctionisexecuted.
Inprinciple,thisapproachshouldwork,however,inpractice,therewillbecontactbounce.Whenyoupressorreleaseabutton,itactuallychangesstatemanytimesreallyquickly,beforesettlingtothecorrectstate.Thisiscalledbounce,andcanbearealproblemifyouwanttoreliablydetectbuttonpresses.Byincludingatimerinthecode,youcanmakesurethatthebuttonisstableforatleastacoupleoftensofmillisecondsbeforeregisteringthestatechange.Here'swhatthatlookslike:
constunsignedlongdebounceTime=25;//Ignoreallstatechangesthathappen25milliseconds//afterthebuttonispressedorreleased.voidloop(){staticboolpreviousState=HIGH;//Declareastaticvariabletosavethepreviousstateoftheinput//andinitializeittoHIGH(notpressed).staticboolbuttonState=HIGH;//Declareastaticvariabletosavethestateofthebutton//andinitializeittoHIGH(notpressed).staticunsignedlongpreviousBounceTime=0;//Declareastaticvariabletosavethetimethebuttonlast//changedstate(bounced).boolcurrentState=digitalRead(pushButtonPin);//Readthecurrentstateoftheinputpinif(currentState!=buttonState){//Ifthecurrentstateisdifferentfromthebuttonstateif(millis()-previousBounceTime>debounceTime){//Iftheinputhasbeenstableforatleast25msbuttonState=currentState;//Rememberthestatethatthe(debounced)buttonisinif(buttonState==LOW){//IfthebuttonispressedsendMIDI(NOTE_ON,channel,note,velocity);//Sendanoteonevent}else{//IfthebuttonisreleasedsendMIDI(NOTE_OFF,channel,note,velocity);//Sendanoteoffevent}}}if(currentState!=previousState){//Ifthestateoftheinputchanged(ifthebuttonbounces)previousBounceTime=millis();//RememberthecurrenttimepreviousState=currentState;//Rememberthecurrentstateoftheinput}}
buttonStatekeepsthestateoftheideal,debouncedbutton,whilepreviousStatekeepsthepreviousstateoftheactualinput.
PotentiometersandfadersMIDIcontrollersoftenfeaturepotentiometersandfadersforcontinuouscontrollerslikevolume,pan,modulation,etc.
Hardware
Thevariableresistors(potentiometersorfaders)arejustusedinavoltagedividerconfiguration,withthetwoouterpinsconnectedtogroundand5V,andthecenterpinconnectedtoananaloginputpinontheArduino.Keepinmindthatyouneedapotentiometerwithalineartaper(nota
logarithmicoraudiotaper).
R1
+5V
Arduinoanaloginput
ControlChange
Formostcontinuouscontrollers,controlchangeeventsareused.Mostsoftwareonlysupports7-bitcontrollers.Thisallowsforatotalof1920controllers(120oneachofthe16MIDIchannels).
Acontinuouscontrollercanbeimplementedasfollows:sampletheanaloginputintheloop,convertfromthe10-bitanalogvaluetoa7-bitControlChangevalue,ifit'sadifferentvaluethanlasttime,sendacontrolchangemessagewiththenewvalue.
constuint8_tanalogPin=A0;
constuint8_tchannel=1;//MIDIchannel1constuint8_tcontroller=0x10;//GeneralPurposeController1
voidsetup(){Serial.begin(31250);}
voidloop(){staticuint8_tpreviousValue=0b10000000;//DeclareastaticvariabletosavethepreviousCCvalue//andinitializeitto0b10000000(themostsignificantbitisset,//soitisdifferentfromanypossible7-bitCCvalue).
uint16_tanalogValue=analogRead(analogPin);//Readthevalueoftheanaloginputuint8_tCC_value=analogValue>>3;//Convertfroma10-bitnumbertoa7-bitnumberbyshifting//it3bitstotheright.if(CC_value!=previousValue){//IfthecurrentvalueisdifferentfromthepreviousvaluesendMIDI(CC,channel,controller,CC_value);//SendthenewvalueoverMIDIpreviousValue=CC_value;//Rememberthenewvalue}}
Theproblemisthattherecanbequitealotofnoiseontheanaloginputs.Soifthevaluefluctuatesalot,itwillconstantlysendnewCCmessages,eveniftheknobisnotbeingtouched.Topreventthis,arunningaveragefiltercanbeusedontheinput.
constuint8_taverageLength=8;//Averagetheanaloginputover8samples(maximum=2^16/2^10=2^6=64)
voidloop(){staticuint8_tpreviousValue=0b10000000;//DeclareastaticvariabletosavethepreviousCCvalue//andinitializeitto0b10000000(themostsignificantbitisset,//soitisdifferentfromanypossible7-bitCCvalue).
uint16_tanalogValue=analogRead(analogPin);//ReadthevalueoftheanaloginputanalogValue=runningAverage(analogValue);//Averagethevalueuint8_tCC_value=analogValue>>3;//Convertfroma10-bitnumbertoa7-bitnumberbyshifting//it3bitstotheright.if(CC_value!=previousValue){//IfthecurrentvalueisdifferentfromthepreviousvaluesendMIDI(CC,channel,controller,CC_value);//SendthenewvalueoverMIDIpreviousValue=CC_value;//Rememberthenewvalue}}
uint16_trunningAverage(uint16_tvalue){//https://playground.arduino.cc/Main/RunningAverage
staticuint16_tpreviousValues[averageLength];staticuint8_tindex=0;staticuint16_tsum=0;staticuint8_tfilled=0;
sum-=previousValues[index];previousValues[index]=value;sum+=value;index++;index=index%averageLength;if(filled<averageLength)filled++;returnsum/filled;}
PitchBend
Ifahigherresolutionisrequired,forexampleforvolumefaders,pitchbendeventsareused.Thismeansthattheyhavea14-bitaccuracy,however,mostdevicesonlyusethe10mostsignificantbits.Therecanbeonlyonepitchbendcontrolleroneachofthe16MIDIchannels.
Thecodeisprettysimilartothepreviousexample.Justshiftthevalue4bitstotheleftinsteadof3bitstotheright,andsendapitchbendmessageinsteadofacontrolchangemessage.Alsonotethatsomeofthevariablesarenowoflargerdatatypes,toaccommodatethe14-bitpitchbendvalues.Tosendthe14-bitpitchbendvalue,ithastobesplitupintotwo7-bitdatabytes.Thiscanbeacchievedbyshiftingit7bitstotheright,togetthe7mostsignificantbits.ThesendMIDIfunctiontakescareofthebitmaskingofthe7leastsignificantbits.
constuint8_tanalogPin=A0;
constuint8_tchannel=1;//MIDIchannel1
voidsetup(){Serial.begin(31250);}
constuint8_taverageLength=16;//Averagetheanaloginputover16samples(maximum=2^16/2^10=2^6=64)
voidloop(){staticuint16_tpreviousValue=0x8000;//Declareastaticvariabletosavethepreviousvalue//andinitializeitto0x8000(themostsignificantbitisset,//soitisdifferentfromanypossible14-bitpitchbendvalue).
uint16_tanalogValue=analogRead(analogPin);//ReadthevalueoftheanaloginputanalogValue=runningAverage(analogValue);//Averagethevalueuint16_tvalue=analogValue<<4;//Convertfroma10-bitnumbertoa14-bitnumberbyshifting//it4bitstotheleft(adds4paddingzerostotheright).if(value!=previousValue){//IfthecurrentvalueisdifferentfromthepreviousvaluesendMIDI(PITCH_BEND,channel,value,value>>7);//SendthenewvalueoverMIDI(splitupintotwo7-bitbytes)previousValue=value;//Rememberthenewvalue}}
uint16_trunningAverage(uint16_tvalue){//https://playground.arduino.cc/Main/RunningAveragestaticuint16_tpreviousValues[averageLength];staticuint8_tindex=0;staticuint16_tsum=0;staticuint8_tfilled=0;
sum-=previousValues[index];previousValues[index]=value;sum+=value;index++;index=index%averageLength;if(filled<averageLength)filled++;returnsum/filled;}
Rotaryencoders
Thedisadvantageofpotentiometersisthatthecomputercan'tchangetheirposition.Forexample,ifyouhaveapotentiometermappedtoapluginparameter,andyouselectadifferentplugin,thepotentiometerdoesn'tautomaticallymovetothepositionofthenewplugginparameter'svalue.Evenworse,ifyouaccidentallytouchthepotentiometer,itwilloverwritetheparameterwiththepositionofpotentiometer,regardlessofthevalueithadbefore.
Onesolutionistouserotaryencoders.Thisisarelativeorincrementaltypeofrotaryknob,whichmeansthatitdoesn'thaveanabsoluteposition,itonlysendsincrementalpositionchangeswhenmoved.Whentheencoderisturnedtwotickstotheright,itsendsavalueof+2,whenit'sturned5tickstotheleft,itsendsavalueof-5.
Hardware
Connectthecommonpinoftherotaryencodertoground,andconnecttheAandBpinstodigitalinputpins(preferablyinterruptcapablepins)oftheArduino.Asahardwaredebouncingmeasure,youcouldaddanRClow-passfilter.
Software
Theeasiestwaytoreadarotaryencoderistousealibrary.Thisensurescompatibilityonprettymuchallboards,andmanyoftheselibrariesaremuchmoreefficientthanwritingtheISRcodeyourself.MypersonalfavoriteisthePJRCEncoderlibrary.
#include<Encoder.h>//IncludethePJRCEncoderlibrary
constuint8_tchannel=1;//MIDIchannel1constuint8_tcontroller=0x10;//GeneralPurposeController1
Encoderencoder(2,3);//Arotaryencoderconnectedtopins2and3
voidsetup(){Serial.begin(31250);}
voidloop(){staticint32_tpreviousPosition=0;//Astaticvariableforsavingthepreviousencoderpositionint32_tposition=encoder.read();//Readthecurrentencoderpositionint32_tdifference=position-previousPosition;//Calculatetherelativemovementif(difference!=0){//IftheencoderwasmovedsendMIDI(CC,channel,controller,difference);//SendtherelativepositionchangeoverMIDIpreviousPosition=position;//Rememberthecurrentpositionasthepreviousposition}}
Mostrotaryencoderssend4pulsesforeveryphysical'tick'(indent).Itmakessensetodividethenumberofpulsesby4beforesendingitoverMIDI.Keepinmindthatisafloordivision,sowecan'tjustreplacepreviousPositionwithposition,becausewe'dlosepulses.Forexample,ifthecurrentpositionis6,andthepreviouspositionis0,differencewillbe6pulses.6/4=1completetick.Thenthepreviouspositionwillbesetto6.However,only1tick,i.e.4pulses,hasbeensent,and6%4=2pulseshavejustbeenlost.Thesolutionisverysimple:
voidloop(){staticint32_tpreviousPosition=0;//Astaticvariableforsavingthepreviousencoderpositionint32_tposition=encoder.read();//Readthecurrentencoderpositionint32_tdifference=position-previousPosition;//Calculatetherelativemovementdifference/=4;//Onetickforevery4pulsesif(difference!=0){//IftheencoderwasmovedsendMIDI(CC,channel,controller,difference);//SendtherelativepositionchangeoverMIDIpreviousPosition+=difference*4;//AddthepulsessentoverMIDItothepreviousposition}}
Therearethreewaystoencodenegativepositionchangesintoa7-bitMIDIdatabyte:
1. Two'scomplement
2. Signedmagnitude3. Offsetbinary
OntheArduino,allsignednumbersarerepresentedastwo'scomplement.Sosendingatwo'scomplementnumberoverMIDIisassimpleasjustsending(the7leastsignificantbitsof)thesignedvariable.Insignedmagnituderepresentation,bit6isusedasasignbit(0=positive,1=negative),andthe6leastsignificantbitsareusedtostoretheabsolutevalueofthesignednumber.Whenusingbinaryoffsetrepresentation,64isaddedtothesignednumbertomakeeverythingpositive.
Someprogramsdon'tsupportrelativechangesofmorethan15inoneMIDImessage,soweconstrainthedifferenceto15.
Thissketchallowsyoutochoosewhatrepresentationtouse,toguaranteecompatibilitywithmostsoftware,andalsolimitstherelativepositionchangeperMIDImessageto15.
#include<Encoder.h>//IncludethePJRCEncoderlibrary
enumrelativeCCmode{TWOS_COMPLEMENT,BINARY_OFFSET,SIGN_MAGNITUDE};
constuint8_tchannel=1;//MIDIchannel1constuint8_tcontroller=0x10;//GeneralPurposeController1
constEncoderencoder(2,3);//Arotaryencoderconnectedtopins2and3
constrelativeCCmodenegativeRepresentation=SIGN_MAGNITUDE;//Selectthewaynegativenumbersarerepresented
voidsetup(){Serial.begin(31250);}
voidloop(){staticint32_tpreviousPosition=0;//Astaticvariableforsavingthepreviousencoderpositionint32_tposition=encoder.read();//Readthecurrentencoderpositionint32_tdifference=position-previousPosition;//Calculatetherelativemovementdifference/=4;//Onetickforevery4pulsesdifference=constrain(difference,-15,15);//Makesurethatonly15ticksaresentatonceif(difference!=0){//Iftheencoderwasmoveduint8_tCC_value=mapRelativeCC(difference);//ChangetherepresentationofnegativenumberssendMIDI(CC,channel,controller,CC_value);//SendtherelativepositionchangeoverMIDIpreviousPosition+=difference*4;//AddthepulsessentoverMIDItothepreviousposition}}
uint8_ttwosComplementTo7bitSignedMagnitude(int8_tvalue){//Convertan8-bittwo'scomplementintegerto7-bitsign-magnitudeformatuint8_tmask=value>>7;uint8_tabs=(value+mask)^mask;uint8_tsign=mask&0b01000000;return(abs&0b00111111)|sign;}
uint8_tmapRelativeCC(int8_tvalue){//Convertan8-bittwo'scomplementintegertoa7-bitvaluetosendoverMIDIswitch(negativeRepresentation){caseTWOS_COMPLEMENT:returnvalue;//RememberthatthesendMIDIfunctiondoesthebitmasking,soyoudon'thavetoworryaboutbit7being1.caseBINARY_OFFSET:returnvalue+64;caseSIGN_MAGNITUDE:returntwosComplementTo7bitSignedMagnitude(value);}}
Object-OrientedapproachTheexamplesaboveonlyworkforasinglebutton,potentiometerorencoder.Justcopyingand
pastingthecodeforeachnewcomponentwouldleadtomanyrepetitionsandverymessycode.That'swhyit'sagoodideatoimplementthecodeindifferentclasses:aclassforbuttons,anotherclassforpotentiometers,etc.YoucanthenjustinstantiatemanyobjectsoftheseclassesforthemanybuttonsandknobsonyourMIDIcontroller.
IwroteanArduinoMIDIcontrollerlibrarythatmakesthisreallyeasy.Forexample,thisisallthecodeyouneedforaMIDIcontrollerwith4potentiometers,4buttonsand2rotaryencoders:
#include<MIDI_Controller.h>//Includethelibrary
/*Createfournewinstancesoftheclass'Analog'onpinsA0,A1,A2andA3,withcontrollernumber0x07(channelvolume),onMIDIchannels1through4.*/Analogpotentiometers[]={{A0,0x7,1},{A1,0x7,2},{A2,0x7,3},{A3,0x7,4},};/*Createfournewinstancesoftheclass'Digital'onpins4,5,6and7,withnotenumbers0x10through0x13(mute),onMIDIchannel1.*/Digitalbuttons[]={{4,0x10,1},{5,0x11,1},{6,0x12,1},{7,0x13,1},};/*Createtwonewinstancesoftheclass'RotaryEncoder'called'encoders',onpins0&1,and2&3,controllernumbers0x2Fand0x30,onMIDIchannel1,atnormalspeed,usingnormalencoders(4pulsesperclick/step),usingtwo'scomplementsignrepresentation.*/RotaryEncoderencoders[]={{0,1,0x2F,1,1,NORMAL_ENCODER,TWOS_COMPLEMENT},{2,3,0x30,1,1,NORMAL_ENCODER,TWOS_COMPLEMENT}};
voidsetup(){}
voidloop(){//RefreshallinputsMIDI_Controller.refresh();}
Asyoucansee,there'sonlythedefinitionsofallcontrols,thenanemptysetup,andfinallyjustaloopthatrefreshesallcontrolsindefinitely.TheMIDIControllerlibraryhandleseverythingdiscussedabove,andevenmore!Itallowsyoutoarrangecontrolsintodifferentbanks,switchbetweenbanks,choosebetweenmanydifferentMIDIinterfaces(USB,Serial,SoftwareSerial),hassupportformultiplexers,buttonmatrices,etc.
Youcandownloadthelibraryhere.
MIDIInputReadingMIDIcanbedoneusingtheArduino'sUART.TheMIDIspecificationproposesanalgorithmforreceivingMIDImessages:
?
?
StoreinRunningStatus
Buffer
ClearThirdByteFlag
?
StoreitinFIFO
IncrementPointer+1
(donotincrementpointerhere)
?
?
?
?
IgnoreDataByte
ClearThirdByteFlag
StoreThirdByteintoFIFO
IncrementPointer+3
?
ClearRunningStatusBuffer
?
?SetThirdByteFlag
StoreStatusintoFIFO
StoreDataByteintoFIFO
(donotincrementpointerhere)IgnoreStatus
IncrementPointer+2
StoreDataByteintoFIFO
StoreStatusintoFIFO
ReadSerialInputBit7=0Bit7=1
ThirdByteFlag=1Yes IsitaReal-TimeMessage?
No
No
Yes
IsthisaTuneRequest?
=F6H
Flag=0RunningStatusBuffer=0
BufferGreaterthan0 Less
thanC0H
LessthanE0H
BufferLessthanF0H
BufferGreaterthanF0H
Buffer=F2H
Buffer=F3HorF1H
Buffer>=F0H
ClearRunningStatusBuffer
ClearRunningStatusBuffer
Inthischapter,wewon'tbeconcernedwithSystemorReal-Timemessages.Theimplementationofthealgorithmaboveisprettystraightforward.Wewon'tuseaFIFO,buthandlethemessagesimmediately.
voidsetup(){Serial.begin(31250);}
voidhandleMIDI(uint8_tstatusByte,uint8_tdata1,uint8_tdata2=0){;}
voidloop(){staticuint8_trunningStatus=0;staticuint8_tdata1=0;staticboolthirdByte=false;
if(Serial.available()){uint8_tnewByte=Serial.read();if(newByte&0b10000000){//HeaderbytereceivedrunningStatus=newByte;thirdByte=false;}else{if(thirdByte){//Seconddatabytereceiveduint8_tdata2=newByte;handleMIDI(runningStatus,data1,data2);thirdByte=false;return;}else{//Firstdatabytereceivedif(!runningStatus)//nostatusbytereturn;//invaliddatabyteif(runningStatus<0xC0){//FirstdatabyteofNoteOff/On,KeyPressureorControlChangedata1=newByte;thirdByte=true;return;}if(runningStatus<0xE0){//FirstdatabyteofProgramChangeorChannelPressuredata1=newByte;handleMIDI(runningStatus,data1);return;}if(runningStatus<0xF0){//FirstdatabyteofPitchBenddata1=newByte;thirdByte=true;return;}else{;//Systemmessage(notimplemented)}}}}}
Thereareafewoptimizationswecando.Wecanjustcheckiftherunningstatusbytecontainsamessagetypeforatwo-orthree-bytemessage,insteadofthecomparisonswehaverightnow.Apartfromthat,wedon'treallyneedanextravariableforthethirdbyteflag,wecanjustusebit7ofthedata1variable.
constuint8_tNOTE_OFF=0x80;constuint8_tNOTE_ON=0x90;constuint8_tKEY_PRESSURE=0xA0;constuint8_tCC=0xB0;constuint8_tPROGRAM_CHANGE=0xC0;constuint8_tCHANNEL_PRESSURE=0xD0;constuint8_tPITCH_BEND=0xE0;
voidloop(){staticuint8_trunningStatus=0;staticuint8_tdata1=0b10000000;
if(Serial.available()){uint8_tnewByte=Serial.read();if(newByte&0b10000000){//StatusbytereceivedrunningStatus=newByte;data1=0b10000000;}else{if(data1!=0b10000000){//SeconddatabytereceivedhandleMIDI(runningStatus,data1,newByte);data1=0b10000000;return;}else{//Firstdatabytereceivedif(!runningStatus)//nostatusbytereturn;//invaliddatabyteif(runningStatus==PROGRAM_CHANGE||runningStatus==CHANNEL_PRESSURE){//FirstdatabyteofProgramChangeorChannelPressurehandleMIDI(runningStatus,newByte);return;}elseif(runningStatus<0xF0){//FirstdatabyteofNoteOff/On,Key