Keys and Fingerboard
A notebook demonstrating a custom View
functions with a working toy-like piano and guitar fingerboard. This covers basic graphics manipulation, sound emission and working with dynamic graphics groups.
Helper function to work with notes and scales
Define semintone converters
Download original notebooksemitone["C"] = 0; semitone["C#"] = 1; semitone["D"] = 2; semitone["D#"] = 3; semitone["Eb"] = 3; semitone["E"] = 4; semitone["F"] = 5; semitone["F#"] = 6; semitone["Gb"] = 6; semitone["G"] = 7; semitone["Ab"] = 8; semitone["G#"] = 8; semitone["A"] = 9; semitone["Bb"] = 10; semitone["B"] = 11; convertToSemitones[s_String] := With[{c = StringCases[s, {a__~~b:DigitCharacter :> {a,ToExpression[b]}, a__ :> {a,4}}]//First}, semitone[c[[1]]] + (c[[2]] - 4) 12 ] convertToSemitones[s_] := s; convertToSemitones[l_List] := convertToSemitones /@ l
Piano view
A view-function to visualize and play SoundNote
on a keyboard.
Helper functions to generate keys
keyLevels = {0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 0}; generateKeys[keyCount_] := Module[ {index = 1, position = 1, keyWidth, keyOffset}, Table[ With[ { cycleIndex = Mod[index - 1, 12] + 1, level = keyLevels[[Mod[index - 1, 12] + 1]] }, keyWidth = 0.5 - 0.2 level; keyOffset = 0.5 level; position += 1 - level; index++; Rectangle[ {position - keyWidth + keyOffset, keyOffset}, {position + keyWidth + keyOffset, 1} ] ], {keyCount} ] ]; annotateKeys[keys_] := MapIndexed[ Annotation[#, keyLevels[[Mod[#2[[1]] - 1, 12] + 1]], #2[[1]]] &, keys ]; highlightNotes[skeys_, indexes_] := Map[Function[key, If[MemberQ[indexes, key[[3]]], {(*VB[*)(RGBColor[1., 0.6862745098039216, 0.47843137254901963])(*,*)(*"1:eJxTTMoPSmNkYGAoZgESHvk5KRCeGJAIcndyzs/JLwouTyxJzghJzS3ISSxJTWMGyXMgyRcxgMEH+6JvX0HgqX3R/HkgcM8eAIGmHxI="*)(*]VB*), key} , key ] ], skeys] separateKeys[keys_] := Module[ {annotatedKeys = annotateKeys[keys]}, { Cases[annotatedKeys, Annotation[_, 0, _]], Cases[annotatedKeys, Annotation[_, 1, _]] } ];
A main function to construct a piano view
PianoView[] := PianoView[SoundNote[{}]] PianoView[SoundNote[rawNotes_List], OptionsPattern[]] := Module[{ keys, separated, scene, highlighted, names = {"C", "C#", "D", "Eb", "E", "F", "F#", "G", "Ab", "A", "Bb", "B", "C"}, annotations, notes = convertToSemitones @ rawNotes }, keys = generateKeys[OptionValue["Size"]]; separated = separateKeys[keys]; scene = FrontInstanceReference[]; highlighted = Mod[#, Length[keys]] + 1 &/@ notes; annotations = Text[names[[Mod[#, 12]+1]], Mean @ ( List @@ keys[[Mod[#, Length[keys]] + 1]] ) + {0.,-0.1}, {0,0}] &/@ notes; notes = Switch[OptionValue["Mode"], "Play", notes, "Show", {}, _, notes ]; Graphics[{scene, {White, EdgeForm[Gray], highlightNotes[separated[[1]], highlighted], Black//Lighter, highlightNotes[separated[[2]],highlighted]}, {Yellow, Opacity[0.3]}, {Black, annotations}, EventHandler[Graphics`Canvas[], {"click" -> clicker[scene, separated, keys, notes, names]}] }, ImageSize->OptionValue[ImageSize], "Controls"->False] ] Options[PianoView] = {ImageSize->{500,200}, "Size"->25, "Mode"->"Play"};
Define a handler for emitting sound and highlighting keys
ClearAll[clicker] clicker[scene_, separated_, keys_, additional_:{}, notes_][xy_] := With[{ whiteMatch = SelectFirst[separated[[1]], RegionMember[#[[1]], xy] &], blackMatch = SelectFirst[separated[[2]], RegionMember[#[[1]], xy] &], group = FrontInstanceGroup[] }, With[{ key = If[!MissingQ[blackMatch], blackMatch[[3]], whiteMatch[[3]]] }, FrontSubmit[SoundNote[Join[additional, {key-1}]//DeleteDuplicates] // Sound]; FrontSubmit[{ Red, Text[notes[[Mod[# - 1, 12] + 1]], Mean[List @@ (keys[[#]])] - {0,0.2}, {0,0}] &/@ Join[(Mod[#, Length[keys]-1] &/@ additional)+1, {key}], Opacity[0.3], keys[[Join[((Mod[#, Length[keys]-1]) &/@ additional)+1, {key}]]] } // group, scene]; SetTimeout[Delete[group], 200]; ] ]
Testing
Time to play some chords
SoundNote[{"D4", "E5", 7}] // PianoView
(*VB[*)(FrontEndRef["159960d2-454d-45c9-a227-6b2958ecc9ce"])(*,*)(*"1:eJxTTMoPSmNkYGAoZgESHvk5KRCeEJBwK8rPK3HNS3GtSE0uLUlMykkNVgEKG5paWpoZpBjpmpiapACJZEvdRCMjc12zJCNLU4vU5GTL5FQActkVVQ=="*)(*]VB*)
Guitar Fingerboard
A view function to do the same, but on the fingerboard of a guitar with an arbitary tuning
GuitarView[SoundNote[rawNotes_List], OptionsPattern[]] := Module[{ tuning = OptionValue["Tuning"], scene = FrontInstanceReference[], names = {"C", "C#", "D", "Eb", "E", "F", "F#", "G", "Ab", "A", "Bb", "B", "C"}, annotations, notes = convertToSemitones @ rawNotes, board, highlighted, semitoneTuning = Reverse[convertToSemitones @ OptionValue["Tuning"]] }, notes = Switch[OptionValue["Mode"], "Play", notes, "Show", {}, _, notes ]; notes = With[{c = Mod[#, convertToSemitones @ (semitoneTuning[[-1]] + 12)]}, If[c < semitoneTuning[[1]], c + 12 , c ] ] &/@ notes; board = GroupBy[Flatten[ Table[With[{note = semitoneTuning[[y]] + x}, Annotation[Disk[{x,y}, {0.15, 0.15 2.8 }], note, names[[Mod[note, 12]+1]]] ], {x, 0, 12}, {y, Length[tuning]}] ], #[[2]] &]; highlighted[notes_] := { ColorDataFunction[(*VB[*) 24, "Indexed", {1, 10, 1}, {RGBColor[0.9215686274509803, 0.49411764705882355, 0.43137254901960786], RGBColor[1., 0.7215686274509804, 0.2196078431372549], RGBColor[0.9490196078431372, 0.8627450980392157, 0.43529411764705883], RGBColor[0.6705882352941176, 0.8784313725490196, 0.9372549019607843], RGBColor[0.3176470588235294, 0.6549019607843137, 0.7529411764705882], RGBColor[0.12941176470588237, 0.5176470588235295, 0.6313725490196078], RGBColor[0.09019607843137255, 0.33725490196078434, 0.49411764705882355], RGBColor[0.7058823529411765, 0.49411764705882355, 0.5450980392156862], RGBColor[0.5333333333333333, 0.23529411764705882, 0.3058823529411765], RGBColor[0.8941176470588236, 0.7098039215686275, 0.7490196078431373]}[[Mod[Floor[#1], 10, 1]]] & (*,*)(*"1:eJylk82TmjAYxrdf04/pTI+d6al74OiMBti6vRFERdRVglg9NTUBWRKCAXb1b+s/16Bra6edqdO9PLz83jd5HiBcfhN+9Ozi4qL4oASKbVdI/tWSEmcxJajiHMudwtGTeuadkq4UWelkJExkWWF2aDxX0heM7IuxyGj09IiHSVH+7e79id+Dj1tSHib0Hr1UPTcjdPv542H4jRKHJKWQdf/gWQ/dRBETmBzAayUzf9ihK0EoqtcB45ezXzGKXtUFxeQmY7s9DWRF/52nXmYLJmTxyECt5vmBzn9dtc8EM1qW9BH5viumgbYGLAg1E6qrBq73oGbhb+x4CHwa7TEgxicSAaI3AKVGwzBAs4GBQRqGjnVwTdq03TbVmGZ2jpuqnewTD1WAlqZbdLANgpGYID5OFz1LLGMH9e9S0/Nt6gzgvdeWE0/vj5HeK5HTrIYzNtql6TiMHS83k0V+NV3m/q3sbeacVwnmMF5e2QvMcSD4Os5663KZsySds2RhxXYv2YagDnaaxOyEPx/1/8LO2ciFRj+14mqz2g7IanMXu4llumNv1fKG1QyiTcDzHZoP0MyqPLMjWb4OMnfDby1STFmXx180AOmyZUI8ZbY77UC2Lr0/o8KHqOccrOKtKlCOM/X9+JBGZfGi/qMxK+gPONoNHg=="*)(*]VB*)] /@ Range[notes // Length], Map[Function[place, If[place[[1,1,1]] > 0.5, {place, White, Text[place[[3]] , place[[1,1]]- {0,0.16}, {0,0}]} , place ] ], board[#]] &/@ notes }//Transpose; Graphics[{ scene, EventHandler[Graphics`Canvas[], {"click"->plucker[scene, board, highlighted, notes]}], Table[Line[{{0, string}, {12, string}}], {string, Length[tuning]}], Table[Line[{{i - 0.5, 1 - 0.2 }, {i - 0.5, Length[tuning] + 0.2}}], {i, 12}], Table[Text[tuning[[Length[tuning] + 1 - string]], {-0.5, string - 0.2}, {-1,0}], {string, Length[tuning]}], { AbsoluteThickness[4], Line[{{0.5, 1}, {0.5, Length[tuning]}}], Gray, Table[Disk[{0.5 + 2.5 + k, 0.3}, {0.15, 0.15 2.8 }/2], {k, 0, 6, 2}], Disk[{0.5 + 2.5 + 8.9, 0.3}, {0.15, 0.15 2.8 }/2], Disk[{0.5 + 2.5 + 9.1, 0.3}, {0.15, 0.15 2.8 }/2] }, highlighted[notes] }, ImageSize->(1.2 {500,100}), "Controls"->False, ImagePadding->None, PlotRange->{{0 - 0.5, 12 + 0.5}, {0, Length[tuning] + .5}}] ] Options[GuitarView] = {"Tuning"->({"E4", "A4", "D5", "G5", "B5", "E6"}//Reverse), "Mode"->"Play"};
A handler function to emit sound
plucker[scene_, board_, highlighted_, additional_][xy_] := With[{ group = FrontInstanceGroup[], match = SelectFirst[board//Values//Flatten, With[{reg = Disk[#[[1,1]], #[[1,2]] * 2] }, RegionMember[reg, xy] ]&] }, If[MissingQ[match], Return[]]; With[{ notes = Join[additional, {match[[2]]}]//DeleteDuplicates }, FrontSubmit[SoundNote[notes] // Sound]; FrontSubmit[{ Opacity[0.7], highlighted[notes] } // group, scene]; SetTimeout[Delete[group], 200]; ] ]
Testing
Let's play some guitar chords
SoundNote[{"E4", "G5", "B4", "E5", "B5"}] // GuitarView
(*VB[*)(FrontEndRef["7692caba-70e0-4406-833f-3697781f9650"])(*,*)(*"1:eJxTTMoPSmNkYGAoZgESHvk5KRCeEJBwK8rPK3HNS3GtSE0uLUlMykkNVgEKm5tZGiUnJiXqmhukGuiamBiY6VoYG6fpGptZmptbGKZZmpkaAAB6yhTD"*)(*]VB*)
Explore chords
Let's build chords in E Major. Firstly we need to define scales
scale["Major"] = {2,2,1,2,2,2,1}; scale["Dorian"] = {2,1,2,2,2,1,2}; scale["Lydian"] = {2,2,2,1,2,2,1}; makeScale[root_Integer, scale_List] := With[{base = Accumulate[Join[{root}, scale]]}, (base[[Mod[#-1, Length[base]-1] + 1]] + 12 Floor[# / Length[base]]) & ] makeScale[root_String, scale_List] := makeScale[convertToSemitones[root], scale]
Structures for building chords
makeChord[root_, scalename_String, ext_Integer:3] := With[{s = makeScale[root, scale[scalename]]}, chordObject[s, ext] ] NoteTranspose[SoundNote[l_List], i_Integer] := With[{c = convertToSemitones[l]}, SoundNote[Map[(# + i)&, c]] ] NoteMerge[s:(SoundNote[_List]..)] := SoundNote[DeleteDuplicates[convertToSemitones[Join @@ ({s}[[All,1]])]]] makeChord[root_, scalename_String, ext_String]; makeChord[root_, scalename_String, ext_Integer, mod_Rule]; makeChord[root_, scalename_String, ext_Integer, mod:{__Rule}]; chordObject[s_, ext_][offset_String] := chordObject[s, ext][FromRomanNumeral[offset]-1] chordObject[s_, ext_][offset_Integer] := SoundNote @ Table[ s[i + offset], {i, 1, 2 ext, 2}]
Use GuitarView to show all chords
With[{m = makeChord["E4", "Major", 3]}, Table[{ GuitarView[NoteMerge[m[r], NoteTranspose[m[r], 12]]], Spacer[40], Style[r, FontSize->20] }, {r, {"I", "II", "III", "IV", "V", "VI"}}] ] // TableForm
(*GB[*){{(*VB[*)(FrontEndRef["8b30a46a-009e-4ed9-b4a3-f87e9574e93d"])(*,*)(*"1:eJxTTMoPSmNkYGAoZgESHvk5KRCeEJBwK8rPK3HNS3GtSE0uLUlMykkNVgEKWyQZGySamCXqGhhYpuqapKZY6iaZJBrrplmYp1qampukWhqnAACEGhWy"*)(*]VB*)(*|*),(*|*)(*VB[*)(Spacer[40])(*,*)(*"1:eJxTTMoPSmNkYGAoZgESHvk5KRAeJ5AILkhMTi1yyq/I1ADyALJsCKU="*)(*]VB*)(*|*),(*|*)(*BB[*)("I")(*,*)(*"1:eJxTTMoPSmNiYGAo5gcSAUX5ZZkpqSn+BSWZ+XnFaYwgCRYg4ZGfkwLhcQKJ4JKizLx0p/wKiE6QgqDSnNRiDiDDLT+vJDizKjVTBMhBlQ9mg+sFi4UUlaYCAHYQHT8="*)(*]BB*)}(*||*),(*||*){(*VB[*)(FrontEndRef["270ca4fa-9d5d-4130-8042-10b932c42081"])(*,*)(*"1:eJxTTMoPSmNkYGAoZgESHvk5KRCeEJBwK8rPK3HNS3GtSE0uLUlMykkNVgEKG5kbJCeapCXqWqaYpuiaGBob6FoYmBjpGhokWRobJZsYGVgYAgB9cBTK"*)(*]VB*)(*|*),(*|*)(*VB[*)(Spacer[40])(*,*)(*"1:eJxTTMoPSmNkYGAoZgESHvk5KRAeJ5AILkhMTi1yyq/I1ADyALJsCKU="*)(*]VB*)(*|*),(*|*)(*BB[*)("II")(*,*)(*"1:eJxTTMoPSmNiYGAo5gcSAUX5ZZkpqSn+BSWZ+XnFaYwgCRYg4ZGfkwLhcQKJ4JKizLx0p/wKiE6QgqDSnNRiDiDDLT+vJDizKjVTBMhBlQ9mg+sFi4UUlaYCAHYQHT8="*)(*]BB*)}(*||*),(*||*){(*VB[*)(FrontEndRef["fdf77bdb-a9df-40c8-90f1-ef7bb0b2345a"])(*,*)(*"1:eJxTTMoPSmNkYGAoZgESHvk5KRCeEJBwK8rPK3HNS3GtSE0uLUlMykkNVgEKp6WkmZsnpSTpJlqmpOmaGCRb6FoapBnqpqaZJyUZJBkZm5gmAgCeiBaT"*)(*]VB*)(*|*),(*|*)(*VB[*)(Spacer[40])(*,*)(*"1:eJxTTMoPSmNkYGAoZgESHvk5KRAeJ5AILkhMTi1yyq/I1ADyALJsCKU="*)(*]VB*)(*|*),(*|*)(*BB[*)("III")(*,*)(*"1:eJxTTMoPSmNiYGAo5gcSAUX5ZZkpqSn+BSWZ+XnFaYwgCRYg4ZGfkwLhcQKJ4JKizLx0p/wKiE6QgqDSnNRiDiDDLT+vJDizKjVTBMhBlQ9mg+sFi4UUlaYCAHYQHT8="*)(*]BB*)}(*||*),(*||*){(*VB[*)(FrontEndRef["b7a43dde-dda7-4056-ba48-87d3914619b1"])(*,*)(*"1:eJxTTMoPSmNkYGAoZgESHvk5KRCeEJBwK8rPK3HNS3GtSE0uLUlMykkNVgEKJ5knmhinpKTqpqQkmuuaGJia6SYlmljoWpinGFsampgZWiYZAgCQPhWm"*)(*]VB*)(*|*),(*|*)(*VB[*)(Spacer[40])(*,*)(*"1:eJxTTMoPSmNkYGAoZgESHvk5KRAeJ5AILkhMTi1yyq/I1ADyALJsCKU="*)(*]VB*)(*|*),(*|*)(*BB[*)("IV")(*,*)(*"1:eJxTTMoPSmNiYGAo5gcSAUX5ZZkpqSn+BSWZ+XnFaYwgCRYg4ZGfkwLhcQKJ4JKizLx0p/wKiE6QgqDSnNRiDiDDLT+vJDizKjVTBMhBlQ9mg+sFi4UUlaYCAHYQHT8="*)(*]BB*)}(*||*),(*||*){(*VB[*)(FrontEndRef["64314d4e-1126-49a2-b5e9-83ef05ab00bb"])(*,*)(*"1:eJxTTMoPSmNkYGAoZgESHvk5KRCeEJBwK8rPK3HNS3GtSE0uLUlMykkNVgEKm5kYG5qkmKTqGhoamemaWCYa6SaZplrqWhinphmYJiYZGCQlAQB17xVj"*)(*]VB*)(*|*),(*|*)(*VB[*)(Spacer[40])(*,*)(*"1:eJxTTMoPSmNkYGAoZgESHvk5KRAeJ5AILkhMTi1yyq/I1ADyALJsCKU="*)(*]VB*)(*|*),(*|*)(*BB[*)("V")(*,*)(*"1:eJxTTMoPSmNiYGAo5gcSAUX5ZZkpqSn+BSWZ+XnFaYwgCRYg4ZGfkwLhcQKJ4JKizLx0p/wKiE6QgqDSnNRiDiDDLT+vJDizKjVTBMhBlQ9mg+sFi4UUlaYCAHYQHT8="*)(*]BB*)}(*||*),(*||*){(*VB[*)(FrontEndRef["f864ce3b-cbd3-4558-b425-6f2e55e75361"])(*,*)(*"1:eJxTTMoPSmNkYGAoZgESHvk5KRCeEJBwK8rPK3HNS3GtSE0uLUlMykkNVgEKp1mYmSSnGifpJielGOuamJpa6CaZGJnqmqUZpZqappqbGpsZAgCK4BV/"*)(*]VB*)(*|*),(*|*)(*VB[*)(Spacer[40])(*,*)(*"1:eJxTTMoPSmNkYGAoZgESHvk5KRAeJ5AILkhMTi1yyq/I1ADyALJsCKU="*)(*]VB*)(*|*),(*|*)(*BB[*)("VI")(*,*)(*"1:eJxTTMoPSmNiYGAo5gcSAUX5ZZkpqSn+BSWZ+XnFaYwgCRYg4ZGfkwLhcQKJ4JKizLx0p/wKiE6QgqDSnNRiDiDDLT+vJDizKjVTBMhBlQ9mg+sFi4UUlaYCAHYQHT8="*)(*]BB*)}}(*]GB*)
A common pop progression could be like repeat
With[{m = makeChord["E4", "Major", 3]}, Table[{ NoteMerge[m[r], NoteTranspose[m[r], 12]], NoteMerge[m[r], NoteTranspose[m[r], 12]], NoteMerge[m[r], NoteTranspose[m[r], 12]], NoteMerge[m[r], NoteTranspose[m[r], 12]] }, {r, {"I", "V", "VI", "IV", "I", "V", "VI", "IV", "I"}}] ] // Flatten // Sound
(*VB[*)(FrontEndRef["467ab9f3-38b4-4357-940e-78b7d925e3b6"])(*,*)(*"1:eJxTTMoPSmNkYGAoZgESHvk5KRCeEJBwK8rPK3HNS3GtSE0uLUlMykkNVgEKm5iZJyZZphnrGlskmeiaGJua61qaGKTqmlskmadYGpmmGieZAQB6mxUr"*)(*]VB*)
Click to play