I want to manage music notes in a C# program, and I wrote a couple classes for that. However, I think I’m having trouble respecting the DRY principle. Either this or I’m overthinking things. Sorry if the question has already been treated somewhere else, I couldn’t think of more explicit keywords and yet found nothing about my problem.
I couldn’t find a simpler example which would fit the explanation of my problem, and I didn’t know what to include and what to leave out of my question, so I wrote a quick version and a detailed version of the question.
Quick version
Let’s say there exist seven labels to describe all musical notes (letters from A to G). Of course, there are more than seven notes to any composition, so only seven labels weren’t enough. So we started putting them on shelves (octaves). A note could share the same label as another, but could be on a different shelf (a higher one or a lower one). This is why we find C notes that are higher than other C notes.
Because of all of this, labeling notes is non-trivial in computer representation: there are two separate informations (label and shelf), labels wrap around when a note shifts from a shelf to another, and that has to be managed.
I have the following class which encapsulates both informations about label and octave:
public class OctaveNote { // Note is an enum storing my seven labels. private Note n; private byte o; public OctaveNote(Note note, byte octave) { n = note; o = octave; } }
I also have these two member functions which are used to pitch a note up or down:
public OctaveNote PitchUp(byte tones) { byte octaveShift = (byte)(tones / 7); byte newOct = (byte)(o + octaveShift); char noteShift = (char)(tones % 7); Note newNote = (Note)((((int)(n + noteShift) % 7) + 7) % 7); if (newNote < n) { newOct++; } return new OctaveNote(newNote, newOct); } public OctaveNote PitchDown(byte tones) { byte octaveShift = (byte)(tones / 7); byte newOct = (byte)(o - octaveShift); byte noteShift = (byte)(tones % 7); Note newNote = (Note)((((int)(n - noteShift) % 7) + 7) % 7); if (newNote > n) { newOct--; } return new OctaveNote(newNote, newOct); }
The code is a bit complicated so let’s say the logic isn’t relevant for now. The real problem is here: the two methods are identical, except for three statements:
n + noteShift vs. n - noteShift newNote < n vs. newNote > n newOct++ vs. newOct--
I would like to merge the two methods to avoid code duplication. tones
would not be a byte
but a char
, and could take negative values to express pitching downwards, like so:
n.Pitch(3) => n.PitchUp(3) n.Pitch(-4) => n.PitchDown(4)
The naive answer would be to throw in a few if
statements here and there to test whether tones
is negative, and perform operations accordingly, but isn’t there a more elegant way?
Detailed version
First of all, I have the Note
enumeration which looks like this:
public enum Note { C = 0, D = 1, E = 2, F = 3, G = 4, A = 5, B = 6 }
For non-musicians out there:
- Notes are represented by letters from A to G and wrap around on both ends:
A
B
C
D
E
F
G
A
B
C
… - Thus, there can be different notes with the same label, for example a C note higher than another C note.
- In order to distinguish these, we have octaves. For example,
C5
is one octave aboveC4
. - Octaves shift upon reaching the C note. This is (partly) because the most well-known scale starts on C, and this is why my
enum
starts on C as well. The following sequence is notes in their natural order: …G3
A3
B3
C4
D4
E4
F4
G4
A4
B4
C5
D5
…
For musicians that I triggered by omitting sharps and flats: let’s keep things simple for the sake of explaining. π
So I wrote the OctaveNote
class which encapsulates both a note (as a Note
) and an octave (as a byte
):
public class OctaveNote { private Note n; private byte o; public OctaveNote(Note note, byte octave) { n = note; o = octave; } }
I added a few methods here and there, and it works like a charm. Trouble comes in when I want to add these methods:
public OctaveNote PitchUp(byte tones); public OctaveNote PitchDown(byte tones);
Here is the expected behavior illustrated in a code sample:
OctaveNote n = new OctaveNote(Note.A, 5); //n is A5 n.PitchUp(1); //n is now B5 n.PitchUp(1); //n is now C6 n.PitchUp(1); //n is now D6 n.PitchDown(3); //n is now back to A5
Implementing these methods looks like this:
public OctaveNote PitchUp(byte tones) { byte octaveShift = (byte)(tones / 7); byte newOct = (byte)(o + octaveShift); char noteShift = (char)(tones % 7); Note newNote = (Note)((((int)(n + noteShift) % 7) + 7) % 7); if (newNote < n) { newOct++; } return new OctaveNote(newNote, newOct); } public OctaveNote PitchDown(byte tones) { byte octaveShift = (byte)(tones / 7); byte newOct = (byte)(o - octaveShift); byte noteShift = (byte)(tones % 7); Note newNote = (Note)((((int)(n - noteShift) % 7) + 7) % 7); if (newNote > n) { newOct--; } return new OctaveNote(newNote, newOct); }
The number 7 is being used a lot because (in this case) an octave is comprised of 7 tones. It is used to find the octave shift (integer division) and the note shift (mod).
The only tricky part is the assignment of newNote
, because n - noteShift
can be negative and the mod may produce an invalid value to cast back into my enum, so I worked around it by adding 7 before mod-ing again.
The question stays the same: is there a way to merge the two functions without having to throw in additional if statements? That is, is it possible to write a single fragment code that behaves in a different way when tones < 0
?