Если нельзя, но очень хочется, то нужно обязательно и ничего в мире не стоит того, чтобы делать из этого проблему!


Интересна Java? Кликай по ссылке и изучай!
Если тебе полезно что-то из того, чем я делюсь в своем блоге - можешь поделиться своими деньгами со мной.
с пожеланием
столько времени читатели провели на блоге - 
сейчас онлайн - 

среда, 17 июля 2013 г.

Играем на фортепиано на Java: Шаг 3 - Играем аккорды

В прошлый раз мы играли гаммы в разных тональностях. Попробуем теперь поиграть аккордами.

Для удобства ввел понятие нота, она инкапсулирует в себе то число, которое я назвал тональностью - оно является кодом ноты на midi устройстве.
public class Нота {
    private int тональность;

    public Нота(int тональность) {
        this.тональность = тональность;
    }

    public Нота(Нота нота) {
        тональность = нота.тональность();
    }

    public int тональность() {
        return тональность;
    }
}
Я избавился от нот в Тональностях, потому что нелогично - ее можно сыграть от какой-то ноты, но держать для каждой из нот копию Можор, Минор и так далее - не ок...
public enum Тональность {

    Мажор(Тон, Тон, Полутон, Тон, Тон, Тон, Полутон),
    Минор(Тон, Полутон, Тон, Тон, Полутон, Тон, Тон),
    МинорГармонический(Тон, Полутон, Тон, Тон, Полутон, МалаяТерция, Полутон),
    МинорМелодический(Тон, Полутон, Тон, Тон, Тон, Тон, Полутон);

    private Интервал[] интервалы;

    private Тональность(Интервал... интервалы) {
        this.интервалы = интервалы;
    }

    public Нота get(Нота from, int order) {
        int sum = 0;
        for (int index = 0; index < order - 1; index++) {
            sum += 2*интервалы[index].интервал();
        }
        return new Нота(from.тональность() + sum);
    }
}
По аналогии я создал понятие Трезвучие
public enum Трезвучие {

    Мажорное(БольшаяТерция, МалаяТерция),
    Минорное(МалаяТерция, БольшаяТерция),
    Уменьшенное(МалаяТерция, МалаяТерция),
    Увеличенное(БольшаяТерция, БольшаяТерция);

    private Интервал[] интервалы;

    private Трезвучие(Интервал... интервалы) {
        this.интервалы = интервалы;
    }

    public List<Нота> get(Нота from) {
        List<Нота> result = new LinkedList<Нота>();

        int sum = 0;
        result.add(new Нота(from));

        for (Интервал i : интервалы) {
            sum += 2*i.интервал();

            result.add(new Нота(from.тональность() + sum));
        }

        return result;
    }
}
Оно очень похоже, только его основной метод расчета дает сразу три ноты, которые потом синтезатор отыграет. Добавил понятие октав
public abstract class Октава {

    private List<Нота> ноты = new LinkedList<Нота>();
    private Октава следующая;
    private Октава предыдущая;

    public void init(Октава предыдущая, Октава следующая, int... ноты) {
        this.следующая = следующая;
        this.предыдущая = предыдущая;
        for (int нота : ноты) {
            this.ноты.add(new Нота(нота));
        }
    }

    public abstract Нота база();

    public Нота get(char нота) {
        switch (нота) {
            case 'C' : return get(1);
            case 'D' : return get(2);
            case 'E' : return get(3);
            case 'F' : return get(4);
            case 'G' : return get(5);
            case 'A' : return get(6);
            case 'H' : return get(7);
            default: throw new IllegalArgumentException("Нет такой ноты");
        }
    }

    public Нота get(int номерНоты) {
        return ноты.get(номерНоты - 1);
    }

    public Октава следующая() {
        return следующая;
    }

    public Октава предыдущая() {
        return предыдущая;
    }
}
И для каждой октавы реализовал наследника
public class ПерваяОктава extends Октава {

    private static ПерваяОктава instance;
    public static ПерваяОктава get() {
        return (instance != null)?instance:new ПерваяОктава();
    }

    public static Нота C4 = ПерваяОктава.нота(1);
    public static Нота D4 = ПерваяОктава.нота(2);
    public static Нота E4 = ПерваяОктава.нота(3);
    public static Нота F4 = ПерваяОктава.нота(4);
    public static Нота G4 = ПерваяОктава.нота(5);
    public static Нота A4 = ПерваяОктава.нота(6);
    public static Нота H4 = ПерваяОктава.нота(7);

    private static Нота нота(int номерНоты) {
        return get().get(номерНоты);
    }

    public ПерваяОктава() {
        instance = this;
        init(МалаяОктава.get(), ВтораяОктава.get(), 48, 50, 52, 53, 55, 57, 59);
    }

    @Override
    public Нота база() {
        return get(1);
    }
}
Избыточно немного, но пусть будет. Не пригодится потом удалю... А так я изменил синтезтор
public class Синтезатор {

    private MidiChannel midi;

    public Синтезатор(MidiChannelFactory midiFactory) {
        this.midi = midiFactory.get();
    }

    public void звучать(Нота нота, int длительность, int сила) {
        midi.noteOn(нота.тональность(), сила);
        пауза(длительность);
        midi.noteOff(нота.тональность());
        пауза(длительность);
    }

    public void звучать(Нота нота, int сила) {
        midi.noteOn(нота.тональность(), сила);
    }

    public void звучать(Нота отНоты, Тональность тональность, int длительность, int сила) {
        for (int index = 1; index <= 8; index++) {
            звучать(тональность.get(отНоты, index), длительность, сила);
        }
    }

    public void звучать(Нота отНоты, Трезвучие трезвучие, int сила) {
        List<Нота> аккорд = трезвучие.get(отНоты);
        for (Нота нота : аккорд) {
            звучать(нота, сила);
        }
    }

    private void пауза(int длительность)  {
        try {
            Thread.sleep(длительность);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}
Появился интерфейс
public interface MidiChannelFactory {
    MidiChannel get();
}
А метод создания конкретного звучащего устройства ушло в новый класс
import javax.sound.midi.*;

public class RealMidiChannelFactory implements MidiChannelFactory {

    @Override
    public MidiChannel get() {
        try {
            // init sequencer
            Sequencer sequencer = null;
            sequencer = MidiSystem.getSequencer();
            sequencer.open();

            // init synthesizer
            Synthesizer synth = MidiSystem.getSynthesizer();
            synth.open();

            // get channel for synthesizing: the highest numbered channel.  sets it up
            MidiChannel[] channels = synth.getChannels();
            MidiChannel midi = channels[channels.length - 1];
            midi.programChange(0);

            midi.noteOn(0, 10);
            sleep();
            midi.noteOff(0);
            return midi;
        } catch (MidiUnavailableException e) {
            throw new RuntimeException(e);
        }
    }

    private void sleep() {
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
Зато теперь синтезатор можно протестировать
import com.apofig.октавы.ПерваяОктава;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;

import javax.sound.midi.MidiChannel;

import java.util.Arrays;
import java.util.LinkedList;

import static junit.framework.Assert.assertEquals;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Mockito.*;

public class СинтезаторTest {

    private MidiChannel midi;
    private Синтезатор синтезатор;

    @Before
    public void setup() {
        RealMidiChannelFactory factory = mock(RealMidiChannelFactory.class);
        midi = mock(MidiChannel.class);
        when(factory.get()).thenReturn(midi);

        синтезатор = new Синтезатор(factory);
    }

    @Test
    public void shouldPlayДоМажорСПервойОктавы() {
        синтезатор.звучать(ПерваяОктава.get().get('C'), Тональность.Мажор, 1, 120);

        assertPlay(48, 50, 52, 53, 55, 57, 59, 60);
    }

    private void assertPlay(Integer... expected) {
        ArgumentCaptor captor = ArgumentCaptor.forClass(Integer.class);
        verify(midi, times(expected.length)).noteOn(captor.capture(), anyInt());

        LinkedList expected1 = new LinkedList();
        expected1.addAll(Arrays.asList(expected));
        assertEquals(expected1.toString(), captor.getAllValues().toString());
    }

    @Test
    public void shouldPlayЛяМажорСПервойОктавы() {
        синтезатор.звучать(ПерваяОктава.get().get('A'), Тональность.Мажор, 1, 120);

        assertPlay(57, 59, 61, 62, 64, 66, 68, 69);
    }

    @Test
         public void shouldPlayЛяМинорСПервойОктавы() {
        синтезатор.звучать(ПерваяОктава.get().get('A'), Тональность.Минор, 1, 120);

        assertPlay(57, 59, 60, 62, 64, 65, 67, 69);
    }

    @Test
    public void shouldPlayМажорныйАккордСПервойОктавы() {
        синтезатор.звучать(ПерваяОктава.get().get('C'), Трезвучие.Мажорное, 120);

        assertPlay(48, 55, 52);
    }

    @Test
    public void shouldPlayМинорныйАккордCПервойОктавы() {
        синтезатор.звучать(ПерваяОктава.get().get('C'), Трезвучие.Минорное, 120);

        assertPlay(48, 51, 55);
    }
}
Вот как бы и все пока. Теперь можно изучать, как звучат разные аккорды, что мне было и нужно....
import com.apofig.октавы.ПерваяОктава;
import static com.apofig.Трезвучие.Мажорное;

public class Main {

    public static void main(String[] args) {
        Синтезатор синтезатор = new Синтезатор(new RealMidiChannelFactory());

//        синтезатор.звучать(ПерваяОктава.get().get('C'), Мажорное, 120);
        синтезатор.звучать(ПерваяОктава.get().get('C'), Мажорное, 120);
    }
}
Продолжение следует... А пока вот исходники к этим трем частям (с git репозиторием).

2 комментария:

  1. Ух ты!))) Надо, надо применить свои 22 года обучения музыке! Саша, спасибо за пример, будем копать в эту сторону, очень интересно!))
    А вообще, почитав твои посты про игру на ф-но, вдруг тоже захотелось поиграть, поподбирать. Только вот пианинку я отдала недавно... Пойду хоть на гитаре побрынчу).

    ОтветитьУдалить
    Ответы
    1. Если "Ух ты!" если захотелось что-то сделать - значит у меня получилось.
      Приятного аппетита!!

      Удалить