Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Transposing music notes by scale indices

I'm trying to create an algorithm that transposes musical notes up or down according to their given scale and transpose factor.

Current Algorithm:

public Note getNoteByScale(Scale scale, int transposeFactor) {
    int newPitch = scale.getScaleIndex(this.pitch) + transposeFactor;
    int newOctave = this.octave;

    if (newPitch > 6) {
        newPitch -= 7;
        newOctave++;
    }
    if (newPitch < 0) {
        newPitch += 7;
    }

    return Note.createNote(scale.getNotesInScale().get(newPitch),
            newOctave, this.duration);
}

Every scale (denoted as a major scale for this purpose) has 7 notes. The C-Major Scale for example has the notes:

C, D, E, F, G, A, and B

If you are to use this scale with the algorithm above to transpose notes such as, for example, a 'B' note on an octave of 5 one up in the C-Major Scale, the algorithm would work as intended and return the note 'C' (index 0 of the scale) on an octave of 6.

However, say that we used the D-Major Scale which consists of the notes

D, E, F♯, G, A, B, and C♯

For this scale, the last note 'C♯' is supposed to be considered an octave higher than the rest of the notes in the scale. If we were to use the algorithm above to transpose the note 'B' (index 6) on an octave of 5 one up, for example, the algorithm would actually give the note 'C♯' on an octave of 5 which is completely wrong: it should be on the octave of 6.

Note original = Note.createNote(Pitch.B, 5, Duration.WHOLE);
// Prints: [Note: [pitch: C#],[octave: 5]]
System.out.println(original.getNoteByScale(Scale.D_MAJOR, 1)); 

Is there any way to fix the algorithm above so that it would support such cases as the above?

The Java classes which I am using for the Note and Pitch and Scale class can be found here: Jamie Craane Melody Generation - GoogleCode

like image 767
Dranithix Avatar asked Feb 10 '23 21:02

Dranithix


1 Answers

You currently test the pitch (i.e. the index in the scale) to see if you need to change octave.

But a single scale can contain more than one octave index, and that depends on the index of the note itself relative to C. I'm not using the terminology of that library here as I'm not too familiar with it. So, you need to know if the note's index relative to C has wrapped (down or up) in order to change the octave (up or down).

First assume that you never move more than a transposeFactor value of 6. Next define some tests (see below) before writing any code. And then I think you just need a small change to use Pitch.getNoteNumber() which returns the note index relative to C. I also assume that your getNoteByScale method is a method on Note, and getScaleIndex is a method you created on Scale as per the below:

// this is a method on Scale
public int getScaleIndex(Pitch pitch) {
    return notesInScale.indexOf(pitch);
}
...
public Note getNoteByScale(Scale scale, int transposeFactor) {
    // rename to "newPitchIndex" to distinguish from the actual new Pitch object
    int newPitchIndex = scale.getScaleIndex(this.pitch) + transposeFactor;
    // only use pitch indexes for a scale in the range 0-6
    if (newPitchIndex > 6) newPitchIndex -=7;
    else if (newPitchIndex < 0) newPitchIndex += 7;
    // create the new Pitch object
    Pitch newPitch = scale.getNotesInScale().get(newPitchIndex);

    //  Get the note numbers (relative to C)
    int noteNumber = this.pitch.getNoteNumber();
    int newNoteNumber = newPitch.getNoteNumber();
    int newOctave = this.octave;
    if      (transposeFactor > 0 && newNoteNumber < noteNumber) newOctave++;
    else if (transposeFactor < 0 && newNoteNumber > noteNumber) newOctave--;

    return Note.createNote(newPitch, newOctave, this.duration);
}

Now that we have this we can easily extend it to move more than octave at a time. None of this would really have been that easy without the tests:

public Note getNoteByScale(Scale scale, int transposeFactor) {
    // move not by more than 7
    int pitchIndex = scale.getScaleIndex(this.pitch);
    int newPitchIndex = (pitchIndex + transposeFactor) % 7;
    // and adjust negative values down from the maximum index
    if (newPitchIndex < 0) newPitchIndex += 7;
    // create the new Pitch object
    Pitch newPitch = scale.getNotesInScale().get(newPitchIndex);

    //  Get the note numbers (relative to C)
    int noteNumber = this.pitch.getNoteNumber();
    int newNoteNumber = newPitch.getNoteNumber();
    //  Get the number of whole octave changes
    int octaveChanges = transposeFactor / 7;
    int newOctave = this.octave + octaveChanges;
    //  Adjust the octave based on a larger/smaller note index relative to C
    if      (transposeFactor > 0 && newNoteNumber < noteNumber) newOctave++;
    else if (transposeFactor < 0 && newNoteNumber > noteNumber) newOctave--;

    return Note.createNote(newPitch, newOctave, this.duration);
}

More tests are needed than this, but this is a good starter, and all these do pass:

@Test
public void transposeUpGivesCorrectPitch() {
    //                 ┌~1▼
    // |D  E  F♯ G  A  B ║C♯|D  E  F♯ G  A  B ║C♯ |
    Note original = Note.createNote(Pitch.B, 5, Duration.WHOLE);
    Note actual = original.getNoteByScale(Scale.D_MAJOR, 1);
    Note expected = Note.createNote(Pitch.C_SHARP, 6, Duration.WHOLE);
    assertEquals(expected.getPitch(), actual.getPitch());
}

@Test
public void transposeDownGivesCorrectPitch() {
    //                 ▼1~┐                     
    // |D  E  F♯ G  A  B ║C♯|D  E  F♯ G  A  B ║C♯ |
    Note original = Note.createNote(Pitch.C_SHARP, 6, Duration.WHOLE);
    Note actual = original.getNoteByScale(Scale.D_MAJOR, -1);
    Note expected = Note.createNote(Pitch.B, 5, Duration.WHOLE);
    assertEquals(expected.getPitch(), actual.getPitch());
}

@Test
public void transposeUpOutsideScaleGivesCorrectPitch() {
    //                 ┌‒1‒~2‒‒3‒‒4▼         
    // ║C  D  E  F  G  A  B ║C  D  E  F  G  A  B |
    Note original = Note.createNote(Pitch.A, 5, Duration.WHOLE);
    Note actual = original.getNoteByScale(Scale.C_MAJOR, 4);
    Note expected = Note.createNote(Pitch.E, 6, Duration.WHOLE);
    assertEquals(expected.getPitch(), actual.getPitch());
}

@Test
public void transposeDownOutsideScaleGivesCorrectPitch() {
    //           ▼4‒‒3‒‒2‒‒1~┐                        
    // ║C  D  E  F  G  A  B ║C  D  E  F  G  A  B |
    Note original = Note.createNote(Pitch.C, 6, Duration.WHOLE);
    Note actual = original.getNoteByScale(Scale.C_MAJOR, -4);
    Note expected = Note.createNote(Pitch.F, 5, Duration.WHOLE);
    assertEquals(expected.getPitch(), actual.getPitch());
}

@Test
public void transposeUpGivesCorrectOctave() {
    //                 ┌~1▼
    // |D  E  F♯ G  A  B ║C♯|D  E  F♯ G  A  B ║C♯ |
    Note original = Note.createNote(Pitch.B, 5, Duration.WHOLE);
    Note actual = original.getNoteByScale(Scale.D_MAJOR, 1);
    Note expected = Note.createNote(Pitch.C_SHARP, 6, Duration.WHOLE);
    assertEquals(expected.getOctave(), actual.getOctave());
}

@Test
public void transposeUp2GivesCorrectOctave() {
    //                 ┌~1‒‒2‒‒3‒‒4‒‒5‒‒6‒‒7‒~1▼                     
    // |D  E  F♯ G  A  B ║C♯|D  E  F♯ G  A  B ║C♯ |
    Note original = Note.createNote(Pitch.B, 5, Duration.WHOLE);
    Note actual = original.getNoteByScale(Scale.D_MAJOR, 1 + 7);
    Note expected = Note.createNote(Pitch.C_SHARP, 7, Duration.WHOLE);
    assertEquals(expected.getOctave(), actual.getOctave());
}

@Test
public void transposeDownGivesCorrectOctave() {
    //                 ▼1~┐                     
    // |D  E  F♯ G  A  B ║C♯|D  E  F♯ G  A  B ║C♯ |
    Note original = Note.createNote(Pitch.C_SHARP, 6, Duration.WHOLE);
    Note actual = original.getNoteByScale(Scale.D_MAJOR, -1);
    Note expected = Note.createNote(Pitch.B, 5, Duration.WHOLE);
    assertEquals(expected.getOctave(), actual.getOctave());
}

@Test
public void transposeDown2GivesCorrectOctave() {
    //                 ▼1~‒7‒‒6‒‒5‒‒4‒‒3‒‒2‒‒1~┐                     
    // |D  E  F♯ G  A  B ║C♯|D  E  F♯ G  A  B ║C♯ |
    Note original = Note.createNote(Pitch.C_SHARP, 6, Duration.WHOLE);
    Note actual = original.getNoteByScale(Scale.D_MAJOR, -1 - 7);
    Note expected = Note.createNote(Pitch.B, 4, Duration.WHOLE);
    assertEquals(expected.getOctave(), actual.getOctave());
}

// ... and more tests are needed ...
like image 55
Andy Brown Avatar answered Feb 13 '23 21:02

Andy Brown