IT_Programming/Java

[펌] MIDI SOUND 생성하기 ▷ J2SE

JJun ™ 2008. 7. 23. 11:57

자바 데스크탑 애플리케이션에 오디오 큐(audio cues)를 추가하면 세련된 느낌을 줄 뿐만 아니라 사용성을 극적으로 증가시킬 수가 있다. Musical Instrument Digital Interface (MIDI)는 장치간에 뮤지컬 이벤트를 주고 받을 때 사용되는 커뮤니케이션 프로토콜이다. MIDI파일은 실제 오디오가 아닌 오디오 명령어를 포함한다. MIDI 가 사운드를 재현하기 위해 사운드 엔진에 명령어를 기술하는 반면, 오디오는 사운드의 디지탈 표현(digital representation)이다.

 

TechTip에서는 J2SE 1.3이후의 자바 플랫폼에서 지원하는 javax.sound.midi 패키지를 사용하여 MIDI사운드를 생성하는 3가지 방법을 소개한다.

 

첫번째 테크닉은, MidiChannel객체를 직접 호출해서 사운드를 생성하는 것이다. 이 객체는 MIDI전송을 위한 하나의 MIDI 채널을 나타낸다. 채널을 통해서 연주를 시작하기 위해서는 MidiChannel객체의 noteOn()메소드를 사용하고, 멈추기 위해서는 noteOff()메소드를 사용한다.

 

noteOn()메소드는 2개의 int값을 필요로 한다. 첫번째 int는 연주될 음을 지정하는데, 다음 SingleNoteChannel프로그램에서 noteOn()메소드에 넘겨진 int 60은 가운데 도(middle C)를 위한 표준 MIDI 음의 번호이다. 위아래의 정수는 반음에 해당하고 12 반음은 한 옥타브를 이룬다.

 

noteOn()메소드의 두번째 매개변수는 키가 눌려질 속도를 지정한다. 사실 이 매개변수는 흔히 속도를 나타내지만, 그것을 제어볼륨(volume control)으로 봐도 무방하다. SingleNoteChannel에서 noteOn()메소드의 두번째 매개변수는 70이다. 속도를 나타내는 70을 다른 숫자로 바꿔서 실험을 해보자.

 

noteOff()메소드는 두개의 signature를 갖는데, 하나는 noteOn()에서와 동일한 두개의 매개변수이고, 다른 하나는 연주될 음을 나타내는 번호이다.

음을 연주하기 전에, SingleNoteChannel생성자에 필요한 셋업을 해야한다.

 

사운드를 생성하기 위해서는 Synthesizer객체가 필요하다.

이 객체는 MIDI 1.0 명세(specification)에서 규정하는 각각의 16개 채널을 말하는 MidiChannel들의 컬랙션을 보여준다. SingleNoteChannel생성자는 MidiSystem클래스의 팩토리 메소드를 이용해서 Synthesizer에 대한 핸들을 받고, Synthesizeropen()메소드를 호출한 후, getChannels()메소드를 호출함으로써 가능한 MidiChannels array를 얻어낸다. 그리고는 index 0자리에 첫번째 MidiChannel을 넣는다.

 

   import javax.sound.midi.MidiChannel;

   import javax.sound.midi.Synthesizer;

   import javax.sound.midi.MidiSystem;

   import javax.sound.midi.MidiUnavailableException;

 

   public class SingleNoteChannel {

 

      private MidiChannel channel;

 

      public SingleNoteChannel() {

        try {

          Synthesizer synth =

                          MidiSystem.getSynthesizer();

          synth.open();

          channel = synth.getChannels()[0];

        } catch (MidiUnavailableException e) {

          e.printStackTrace();

        }

      }

 

      public void playNote(int note) {

        channel.noteOn(note, 70);

        try {

          Thread.sleep(1000);

        } catch (InterruptedException e) {

          e.printStackTrace();

        }

        channel.noteOff(note, 70);

      }

 

      public static void main(String[] args) {

        new SingleNoteChannel().playNote(60);

      }

   }

 

SingleNoteChannel프로그램의 playNote()메소드는 MidiChannel noteOn()메소드를 호출해서 음을 연주하기 시작한다. 연주는 쓰레드가 휴면하는 1초동안 지속되다가 MidiChannel noteOff()메소드가 호출됨으로 해서 끝나게 된다. 또한 버튼이나 키 혹은 on-screen event를 누르거나 떼서 특정 음들을 결합시킬 수도 있다.

MIDI사운드를 생성하기 위한 두번째 테크닉은, Synthesizer로 결합된 Receiver객체를 사용하는 것이다. 이 때에 특정 MidiChannel 대신 Receiver에 대한 핸들을 받아야 한다. 사용자는 ShortMessage타입의 MIDI메세지를 만들고 사용자설정을 한 후에 Receiver객체의 send()메소드를 호출해서 연주를 시작할 수 있다.

ShortMessage객체는 많아야 2데이터 바이트를 갖는 MIDI 메세지를 포함한다. MIDI 메세지의 매개변수를 설정하거나, 채널 메세지를 위한 메세지 매개변수를 설정하기 위해서 setMessage() 메소드를 사용할 수 있다. SingleNoteSynthesizer프로그램의 setMessage()메소드는 채널 메세지를 위한 메세지 매개변수를 설정하기 위해서 4개의 int값을 포함하는 signature를 갖는다. 첫번째 intNOTE_ON NOTE_OFF중 보내질 코맨드를, 두번째 int는 타겟 채널을 지정한다. (앞의 예제와 일관되게, index 0을 조건으로 지정했다.) 마지막 두개의 int값은 음 식별자와 속도이다. SingleNoteSynthesizer에서 noteOn()noteOff()에 넘겨지는 int값은 SingleNoteChannel예제에서와 동일하다.

   import javax.sound.midi.ShortMessage;

   import javax.sound.midi.InvalidMidiDataException;

   import javax.sound.midi.Receiver;

   import javax.sound.midi.Synthesizer;

   import javax.sound.midi.MidiSystem;

   import javax.sound.midi.MidiUnavailableException;

 

   public class SingleNoteSynthesizer {

 

      private ShortMessage message =

                                    new ShortMessage();

      private Receiver receiver;

 

      private SingleNoteSynthesizer() {

        try {

          Synthesizer synth =

                           MidiSystem.getSynthesizer();

          synth.open();

          receiver = synth.getReceiver();

        } catch (MidiUnavailableException e) {

          e.printStackTrace();

        }

      }

 

      public void playNote(int note) {

        setShortMessage(note, ShortMessage.NOTE_ON);

        receiver.send(message, -1);

        try {

          Thread.sleep(1000);

        } catch (InterruptedException e) {

       e.printStackTrace();

        }

        setShortMessage(note, ShortMessage.NOTE_OFF);

        receiver.send(message, -1);

      }

 

      private void setShortMessage(

                               int note, int onOrOff) {

        try {

          message.setMessage(onOrOff, 0, note, 70);

        } catch (InvalidMidiDataException e) {

          e.printStackTrace();

        }

      }

 

      public static void main(String[] args) {

        new SingleNoteSynthesizer().playNote(60);

      }

   }

지금까지는 디폴트 악기에서 하나의 음을 연주하는 예만을 살펴보았다. 그렇다면 다른 악기에서의 연주는 어떨까? 세번째 테크닉에서 이 물음의 답을 보게 될 것이다. 실험을 통해 시작해보자. 연주 가능한 악기의 리스트를 생성하는 메소드가 있다.

   public void listAvailableInstruments(){

      Instrument[] instrument =

                     synth.getAvailableInstruments();

      for (int i=0; i<instrument.length; i++){

        System.out.println(i + "   "

                          + instrument[i].getName());

      }

   }

위의 메소드를 호출하면 다음과 같은 아이템 리스트를 보게 된다.

0   Piano

1   Bright Piano

2   Electric Grand

3   Honky Tonk Piano

4   Electric Piano 1

5   Electric Piano 2

6   Harpsichord

7   Clavinet

8   Celesta

9   Glockenspiel

10   Music Box

11   Vibraphone

12   Marimba

13   Xylophone

14   Tubular Bell

15   Dulcimer

16   Hammond Organ

17   Perc Organ

18   Rock Organ

19   Church Organ

20   Reed Organ

21   Accordion

22   Harmonica

23   Tango Accordion

24   Nylon Str Guitar

25   Steel String Guitar

26   Jazz Electric Gtr

27   Clean Guitar

28   Muted Guitar

29   Overdrive Guitar

30   Distortion Guitar

 

SingleNoteSynthesizer2는 다른 악기들을 사용하여 하나 이상의 음을 연주하기 위해 SingleNoteSynthesizer를 보강한 프로그램이다. 하지만 이를 실행해보면, 시간을 맞추기 위해 Thread가 여전히 휴면하는 것을 볼 수 있다. 이는 음악을 만들고 연주하기 위한 좋은 방법이 아닐 뿐더러, AWT Event Dispatch thread에서 이를 실행했다면 GUI가 이벤트를 처리하거나 보수하는 것을 막게 될 것이다. 이 부분은 마지막 테크닉을 설명할 때 다루기로 하겠다.

 

SingleNoteSynthesizer2를 보면, 사용자가 만든 악기 리스트에서 사용하고 싶은 악기의 int값을 찾아 MidiChannelprogramChange()메소드에 넣고, 이를 호출해서 연주될 악기를 바꿀 수가 있다. 예에서 교회 오르간을 나타내는 int 19이다.

 

startNote()stopNote()메소드가 각각 NOTE_ON명령어와 NOTE_OFF명령어를 이용해서 메세지를 생성할 수 있도록 코드를 추가한 것을 주목하자. playNote()메소드는 startNote()를 호출하고, duration에 지정된 밀리초(milliseconds)동안 휴면한 후, stopNote()를 호출한다.

 

   import javax.sound.midi.ShortMessage;

   import javax.sound.midi.Synthesizer;

   import javax.sound.midi.Receiver;

   import javax.sound.midi.MidiSystem;

   import javax.sound.midi.MidiUnavailableException;

   import javax.sound.midi.InvalidMidiDataException;

 

   public class SingleNoteSynthesizer2 {

      private ShortMessage message = new ShortMessage();

      private Synthesizer synth;

      private Receiver receiver;

 

      public SingleNoteSynthesizer2() {

        try {

          synth = MidiSystem.getSynthesizer();

          synth.open();

          receiver = synth.getReceiver();

        } catch (MidiUnavailableException e) {

          e.printStackTrace();

        }

      }

 

      public void startNote(int note) {

        setShortMessage(ShortMessage.NOTE_ON, note);

        receiver.send(message, -1);

      }

 

      public void stopNote(int note) {

        setShortMessage(ShortMessage.NOTE_OFF, note);

        receiver.send(message, -1);

      }

 

      public void playNote(int note, int duration){

        startNote(note);

        try {

          Thread.sleep(duration);

        } catch (InterruptedException e) {

          e.printStackTrace();

        }

        stopNote(note);

      }

 

      public void setInstrument(int instrument){

        synth.getChannels()[0].programChange(

                                           instrument);

      }

 

      private void setShortMessage(

                               int onOrOff, int note) {

        try {

          message.setMessage(onOrOff, 0, note, 70);

        } catch (InvalidMidiDataException e) {

          e.printStackTrace();

        }

      }

 

      public void playMajorChord(int baseNote){

        playNote(baseNote,1000);

        playNote(baseNote+4,1000);

        playNote(baseNote+7, 1000);

        startNote(baseNote);

        startNote(baseNote+4);

        playNote(baseNote+7,2000);

        stopNote(baseNote+4);

        stopNote(baseNote);

      }

 

      public static void main(String[] args) {

        SingleNoteSynthesizer2 synth

                        = new SingleNoteSynthesizer2();

        synth.setInstrument(19);

        synth.playMajorChord(60);

      }

   }

 

주요 화음은 한개의 기본 음, 기본음으로부터 4개 반음 올린 음, 3개 반음 선간(spaces)을 올린 음으로 구성된다. SingleNoteSynthesizer2에서, playMajorChord()메소드는 먼저 하나씩 3개의 음를 연주한 뒤, 이들을 동시에 연주한다. 타이밍은 Threads를 이용해서 맞추는데, 타이밍이 중요할 경우에는 Sequencer를 이용하는 것이 좋다.

Sequencer객체는 MIDI 이벤트의 타이밍을 좀 더 효과적으로 컨트롤 할 수 있는 메소드를 포함한다. 전에는 MidiSystem의 팩토리 메소드를 사용했던 반면, 여기서는 Sequencer를 이용한다. Sequencer를 사용한 예는 다음 예제인 SequencerSound에서 볼 수 있다. 앞선 예와 비교해서 여기서는 코드에 몇몇 주요한 변경이 있음을 유의해야 한다. 먼저 startNote()stopNote()메소드는 생성되는 메세지가 일어나는 시점을 나타내는 int값을 갖는다. createTrack()메소드는 8분음표마다 4번의 똑딱 소리를 내서 하나의 Sequence를 생성한다. 예를 들면, 4/4박자는 4개의 8분음표 혹은 16번의 똑딱 소리로 나뉘어 질 수 있는 것처럼, 박자를 나눌 때 이 메소드를 사용할 수 있다.

setShortMessage()메소드는 setMessage()를 호출한후, ShortMessage에 기반하고, 똑딱 소리로 결합된 MidiEvent객체를 생성한다. 그리고는 마지막으로 트랙에 MidiEvent를 추가한다. 트랙을 재생할 때, startSequencer()메소드는 새롭게 생성된 Sequence를 기존 Sequence에 할당하고, start()메소드를 호출해서 재생시킬수가 있다. 또한 setTempoInBPM()메소드를 이용해서 템포를 분당 16 비트(BPM)으로 설정할 수가 있다.

 

   import javax.sound.midi.Track;

   import javax.sound.midi.Sequencer;

   import javax.sound.midi.Sequence;

   import javax.sound.midi.MidiSystem;

   import javax.sound.midi.MidiUnavailableException;

   import javax.sound.midi.InvalidMidiDataException;

   import javax.sound.midi.ShortMessage;

   import javax.sound.midi.MidiEvent;

 

   public class SequencerSound {

      private Track track;

      private Sequencer sequencer;

      private Sequence sequence;

 

      public SequencerSound() {

        try {

          sequencer = MidiSystem.getSequencer();

          sequencer.open();

        } catch (MidiUnavailableException e) {

          e.printStackTrace();

        }

        createTrack();

        makeScale(20);

        startSequencer();

      }

 

      private void startSequencer() {

        try {

          sequencer.setSequence(sequence);

        } catch (InvalidMidiDataException e) {

          e.printStackTrace();

        }

        sequencer.start();

        sequencer.setTempoInBPM(60);

      }

 

      private void createTrack() {

        try {

          sequence = new Sequence(Sequence.PPQ, 4);

        } catch (InvalidMidiDataException e) {

          e.printStackTrace();

        }

        track = sequence.createTrack();

      }

 

      public void startNote(int note, int tick) {

        setShortMessage(

                     ShortMessage.NOTE_ON, note, tick);

      }

 

      public void stopNote(int note, int tick) {

        setShortMessage(

                    ShortMessage.NOTE_OFF, note, tick);

      }

 

      private void setShortMessage(

                    int onOrOff, int note, int tick) {

        ShortMessage message = new ShortMessage();

        try {

          message.setMessage(onOrOff, 0, note, 90);

          MidiEvent event = new MidiEvent(

                                       message, tick);

          track.add(event);

        } catch (InvalidMidiDataException e) {

          e.printStackTrace();

        }

      }

 

      public void makeScale(int baseNote) {

        for (int i = 0; i < 13; i++) {

          startNote(baseNote + i, i);

          stopNote(baseNote + i, i + 1);

          startNote(baseNote + i, 25 - i);

          stopNote(baseNote + i, 26 - i);

        }

      }

 

      public static void main(String[] args) {

        new SequencerSound();

      }

   }

이 글은 MIDI 파일을 생성하고 재생하는 방법을 설명했다. 개발자는 사운드뱅크가 J2SE SDK에 따라서 자동적으로 인스톨되기 때문에 음악을 들을 수가 있다. 사운드 뱅크는 악기 설정을 보여주고, 사운드를 종합하기 위해 필요하다. (사운드뱅크에 대해서는 후에 더 자세히 다루도록 하겠다.) 비 개발자들은 생성한 파일을 들으려면 사운드뱅크를 따로 설치해야 될 것이다. J2SE 1.4.1 혹은 이후 버전에 JRE를 인스톨할 때, 사용자는 사운드뱅크를 포함시키도록 설정해야 한다.

MIDI파일의 저장과 재생관련 정보는 javax.sound.midi의 documentation, 이 팁에 대한 자세한 내용은 Java Sound API home page를 참고한다

midi_sound_생성하기.doc
0.08MB