按比例索引转换音符

时间:2015-02-21 19:15:38

标签: java algorithm audio

我正在尝试创建一种算法,根据给定的比例和转置因子向上或向下转换音符。

当前算法:

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);
}

每个尺度(为此目的表示为主要尺度)有7个音符。例如,C-Major Scale有以下注释:

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

如果您要使用上述算法的这个音阶来转换音符,例如,在C-Major音阶的5个八度的八度音符上的“B”音符,该算法将按预期工作并返回音符'C'(音阶的索引0)在一个八度音阶上。

然而,假设我们使用由笔记组成的D-Major Scale

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

对于这个音阶,最后一个音符'C♯'应该被认为比音阶中的其他音符高八度。例如,如果我们使用上面的算法将音符'B'(索引6)转换为5的八度音程,那么算法实际上会在5的八度音程上给出音符'C♯',这是完全错误的:它应该在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)); 

有没有办法修复上面的算法,以便它支持上述情况?

我在NotePitch以及Scale类使用的Java类可以在这里找到:Jamie Craane Melody Generation - GoogleCode

4 个答案:

答案 0 :(得分:5)

您目前正在测试pitch(即scale中的索引)以查看是否需要更改八度音阶。

但是单个音阶可以包含多个八度音程索引,这取决于音符本身相对于C的索引。我没有在这里使用该库的术语,因为我对它不太熟悉。因此,您需要知道相对于C的音符索引是否已经包裹(向下或向上)以便更改八度音程(向上或向下)。

首先假设您的transposeFactor值不超过6。接下来在编写任何代码之前定义一些测试(见下文)。然后我认为您只需要进行一些小改动即可使用Pitch.getNoteNumber()返回相对于C的音符索引。我还假设您的getNoteByScale方法是Note上的方法,getScaleIndex 1}}是您在Scale上创建的方法,如下所示:

// 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);
}

现在我们有了这个,我们可以轻松地将它扩展到一次超过八度。没有测试,这一切都不会那么容易:

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);
}

需要比这更多的测试,但这是一个很好的入门者,所有这些都通过了:

@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 ...

答案 1 :(得分:2)

我猜你在通过比例结束时不希望+1倍频程(本例中为ScaleIndex 7)。当你通过C时,你想要+1八度音阶。

编辑:

我还没有在java中编程很长时间,我没有为它安装IDE,所以有些我会写一些伪代码(没有类型)。此外,这仅适用于正转置因子,它应该可以轻松调整负数,但我不想再模糊它。 我们的想法是首先要知道我们上升了多少满刻度,这意味着我们必须至少添加那么多八度音阶。 然后添加其余的(必须小于一个满刻度),看看我们在添加那个时是否通过了C.我们通过查看notenumber来做到这一点,如果新的notenumber小于旧的notenumber,我们必须通过C。

public Note getNoteByScale(Scale scale, int transposeFactor) {
    numberOfNotesInScale = scale.getNotesInScale().size();
    numberOfScalesToTraverseAtLeast = transposeFactor / number_of_notes_in_scale;  // this should be integer division
    newOctave = this.octave + numberOfScalesToTraverseAtLeast;
    restAfterDivide = transposeFactor % numberOfNotesInScale;
    newScaleIndex = (scale.getScaleIndex(this.pitch) + restAfterDivide) % numberOfNotesInScale;  // we are not gonna do +1 when we pass the end of the scale, so just do a modulo on the number of notes of the scale
    newPitch = scale.getNotesInScale().get(newScaleIndex)
    if (newPitch.getNoteNumber < this.pitch.getNoteNumber)  // then we have passed C, so another octave
        newOctave++;

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

}

对于某些测试驱动的开发来说,这将是一个理想的候选函数,我至少会围绕它编写一堆单元测试;)

答案 2 :(得分:0)

对比例进行排序,然后使用它。像这样,所有比例音高都在相同的八度音程中。这是我眼中最简单的解决方案。

public Note getNoteByScale(Scale scale, int transposeFactor) {
       List<Pitch> tempScale=new ArrayList<Pitch>(scale.getNotesInScale());

       Collections.sort(tempScale,new Comparator<Pitch>(){
                             @Override
                             public int compare(Pitch a,Pitch b){
                                 if(a.getNoteNumber() == b.getNoteNumber()){
                                     return 0;
                                 }else if(a.getNoteNumber() > b.getNoteNumber()){
                                     return 1;
                                 }else{
                                     return -1;   
                                 }
                             }

       });

       int index = (tempScale.indexOf(this.pitch) + transposeFactor );

       int newPitch  = ((index % 7)+7)%7;
       int newOctave;
       if(index>0){
           newOctave = this.octave + index/7;
       }else{
           newOctave = this.octave + (index-6)/7;
       }


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

编辑:现在它通过了所有测试

在@AndyBrown的单元测试中添加了两个案例:

@Test
public void transposeDown6GivesCorrectOctave() {
    Note original = Note.createNote(Pitch.C_SHARP, 6, Duration.WHOLE);
    Note actual = original.getNoteByScale(Scale.D_MAJOR, -1-7-7-7-7-7);
    Note expected = Note.createNote(Pitch.B, 0, Duration.WHOLE);
    assertEquals(expected.getOctave(), actual.getOctave());
}

@Test
public void transposeUp5GivesCorrectOctave() {
    Note original = Note.createNote(Pitch.B, 5, Duration.WHOLE);
    Note actual = original.getNoteByScale(Scale.D_MAJOR, 1+7+7+7+7);
    Note expected = Note.createNote(Pitch.C_SHARP, 10, Duration.WHOLE);
    assertEquals(expected.getOctave(), actual.getOctave());
}

答案 3 :(得分:0)

是的,我知道这是一个老问题,但我需要类似的东西。基本上问题可以解析为以下内容:给出一个音符(“D”)和一些正或负半步(1),返回一个新音符,即原始音符转置(D#)

这里的答案太复杂了。

试试这个:

public class Transposer {

    enum TARGET { FLAT, SHARP };
    static String[] SHARPS = { "A", "A#", "B", "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#" };
    static String[] FLATS  = { "A", "Bb", "B", "C", "Db", "D", "Eb", "E", "F", "Gb", "G", "Ab" };

    static String transpose(String note, int halfSteps, TARGET target) {
        if (target == null) {
            // By default we like flats
            target = TARGET.FLAT;
        }

        String[] scale = FLATS;
        if (target == TARGET.SHARP) {
            scale = SHARPS;
        }

        int index = findIndex(scale, note);
        if (index < 0) return note; // Not found, just return note


        String everythingelse = "";
        if (note.length() > scale[index].length()) {
           everythingelse = note.substring(scale[index].length());
        }


        index = index + halfSteps;
        while (index < 0) {
            index += scale.length;  // Make the index positive
        }
        index = index % scale.length;
        return scale[index]+everythingelse;
    }

    public static int findIndex(String[] scale, String note) {
        int r = -1;
        String root = note.substring(0,1);
        if (note.charAt(1) == '#' || note.charAt(1) == 'b') {
            root = note.substring(0,2);
        }
        for (int i=0; i<scale.length; ++i) {
            if (scale[i].equalsIgnoreCase(root)) {
                // Match.
                return i;
            }
        }
        return r;
    }

    public static void main(String args[]) {
        String note = args[0];
        int halfsteps = Integer.parseInt(args[1]);
        System.out.println(note+" transposed "+halfsteps+" halfsteps is "+transpose(note, halfsteps, TARGET.FLAT));
    }
}

main()是测试:

java -cp . Transposer A 1
A transposed 1 halfsteps is Bb

java -cp . Transposer Eb -3
Eb transposed -3 halfsteps is C

java -cp Transposer C7 2
C7 transposed 2 halfsteps is D7

采用比例是微不足道的:

    String[] scale = { "C", "D", "E", "F", "G", "A", "B" };
    for (String snote: scale) {
      System.out.print(Transposer.transpose(snote,1,null));
    }
    System.out.println();

=> Db Eb F Gb Ab Bb C