But before I had managed that (see the most recent incomplete version here) I became interested in overhauling the component design and re-architecting it as another learning exercise, this time to help me work through the concepts of producing loosely-coupled component designs using MVC/MVVM patterns and looking at concerns like unit testing & testability together with an attempt at expressing the data structures involved in XML.
I long since gave up trying to make the UI pretty – whilst I think I can distinguish good UI/UX from bad I am no designer, but I have managed to produce something that fits some of the MVVM principles and allowed me to understand how MVVM works and makes for testable components and clean data-binding. And before I get distracted again and feel the urge to overhaul the code again and douse everything in MEF, it is time for those long-delayed posts.
Representing note sequences as data
Without straying too much into music theory, chords and musical scales are basically sequences of notes – the 8 notes in a scale (e.g. the scale of C Major: C-D-E-F-G-A-B-C) or the 3, 4 or more notes in a chord (e.g. the chord of C Major: C-E-G) can be stored and positions or offsets in a sequence. However the progressive sequence is measured in pitch-steps of semitones from the starting note i.e. including the black notes (looking at the piano keyboard, the semitone sequence starting from the note C is: C, C#, D, Eb, E, F, F#, G, Ab, A, B, C, C#, D, Eb… and so on up the keyboard). So to store the sequence for the scale of C Major we need to store offsets for the 1st, 3rd, 5th, 6th, 8th, 10th, 11th and 12th semitones, and for the chord of C Major we need to store the offsets for the 1st, 5th and 7th. With this information for a scale or a chord we have the means to select or highlight on the UI representation of our piano keyboard the notes that make it up, starting at the base or root note – C in the previous examples.
We can now store these sequences of offsets for all the standard scales and chords that musicians have come up with and use them to produce representations of any variation from a particular root note. An XML schema for these sequences is simple to produce:
- <?xml version="1.0" encoding="utf-8"?>
- <xs:schema targetNamespace="http://tempuri.org/XMLSchema.xsd"
- elementFormDefault="qualified"
- xmlns="http://tempuri.org/XMLSchema.xsd"
- xmlns:mstns="http://tempuri.org/XMLSchema.xsd"
- xmlns:xs="http://www.w3.org/2001/XMLSchema">
- <xs:element name="ChordFactoryData">
- <xs:complexType>
- <xs:sequence>
- <xs:element name="Chords" type="Chords" />
- <xs:element name="Scales" type="Scales" />
- </xs:sequence>
- </xs:complexType>
- </xs:element>
- <xs:complexType name="Chords">
- <xs:sequence>
- <xs:element name="Chord" minOccurs="1" maxOccurs="unbounded" type="NoteSequence" />
- </xs:sequence>
- </xs:complexType>
- <xs:complexType name="Scales">
- <xs:sequence>
- <xs:element name="Scale" minOccurs="1" maxOccurs="unbounded" type="NoteSequence" />
- </xs:sequence>
- </xs:complexType>
- <xs:complexType name="NoteSequence">
- <xs:sequence>
- <xs:element name="SequenceType" type="SequenceType" maxOccurs="1" minOccurs="1" />
- <xs:element name="Description" type="xs:string" maxOccurs="1" minOccurs="1" />
- <xs:element name="NoteList" type="NoteList" maxOccurs="1" minOccurs="1" />
- </xs:sequence>
- </xs:complexType>
- <xs:complexType name="NoteList">
- <xs:sequence>
- <xs:element name="NoteIndex" type="xs:int" minOccurs="1" maxOccurs="unbounded" />
- </xs:sequence>
- </xs:complexType>
- <xs:simpleType name="SequenceType">
- <xs:restriction base="xs:string">
- <xs:enumeration value="Chord" />
- <xs:enumeration value="Scale" />
- </xs:restriction>
- </xs:simpleType>
- </xs:schema>
The XML for a Major Chord would then look like this:
- <chords>
- <chord>
- <sequencetype>Chord</sequencetype>
- <description>Major</description>
- <notelist>
- <noteindex>0</noteindex>
- <noteindex>4</noteindex>
- <noteindex>7</noteindex>
- </notelist>
- </chord>
- ...more chords...
- </chords>
and a Major Scale would look like this:
- <scales>
- <scale>
- <sequencetype>Scale</sequencetype>
- <description>Major</description>
- <notelist>
- <noteindex>0</noteindex>
- <noteindex>2</noteindex>
- <noteindex>4</noteindex>
- <noteindex>5</noteindex>
- <noteindex>7</noteindex>
- <noteindex>9</noteindex>
- <noteindex>11</noteindex>
- <noteindex>12</noteindex>
- </notelist>
- </scale>
- ...more scales...
- </scales>
Data Classes
In my MVVM implementation this Chord and Scale data is loaded into models that represent the Chord or the Scale and I can then produce collections of instances of all the Chords and Scales. Here are the simple models in C#:
- public abstract class NoteSequence
- {
- public List<int> Notes { get; set; }
- public string Description { get; set; }
- }
- public class Chord : NoteSequence
- {
- public static Chord CreateNewChord()
- {
- return new Chord();
- }
- public static Chord CreateChord(string description, List<int> notes)
- {
- return new Chord
- {
- Notes = notes,
- Description = description
- };
- }
- protected Chord() { }
- }
- public class Scale : NoteSequence
- {
- public static Scale CreateNewScale()
- {
- return new Scale();
- }
- public static Scale CreateScale(string description, List<int> notes)
- {
- return new Scale
- {
- Notes = notes,
- Description = description
- };
- }
- protected Scale() { }
- }
In implementation, the Chord and Scale classes are no different and it could be argued that the respective data could be loaded into two collections of the more general NoteSequence class (which would have to be changed from its current abstract incarnation of course); but I prefer to keep them as separate classes as they are logically different and might diverge in the future.
So I now represent my chord and scale data as XML and I have a straightforward model for instantiating it in data objects. The next stage is to load that data into a ViewModel using data access code that will deserialize the XML into the objects. That will be in part 2.