이번 시간에는 게임에서 빠져서는 안될 중요한 사운드를 출력해보는 시간을 갖도록 하지요.

사운드를 출력하는 부분 역시 SK-VM 자체에서 상당부분 라이브러리화 되어 있기 때문에,

우리는 단순히 라이브러리를 사용하는 방식만을 익혀주면 되겠습니다.


사운드를 재생/정지 하기 위한 SoundThread라는 클래스를 하나 작성해 보겠습니다.

* common.SoundThread.java.

package common;

import java.io.InputStream;
import com.skt.m.AudioClip;
import com.skt.m.AudioSystem;

/**
 * 소리를 낼때 사용하는 thread
 * 잠시 기다렸다가, 소리를 낼때 깨서 다시 기다리는 과정을 반복한다.
 */

public class SoundThread extends Thread
{
    private AudioClip currentClip = null;

    private byte[] sndtmp;
    private byte[][] snd;
    private int currentIndex;
    private boolean bPlaying=false;

    private String path;
    private InputStream is = null;
    private boolean bReady;
    private boolean bOn;

    /**
     * Constructor #1.
     */
    public SoundThread()
    {
        this(11);
    }

    /**
     * Constructor #2.
     * @param maxSound
     */
    public SoundThread(int maxSound)
    {
        bOn = true;

        snd = new byte[maxSound][];

        try
        {
            currentClip = AudioSystem.getAudioClip("mmf");
        } catch(Exception e) {}
    }

    /**
     * mix해 놓은 사운드 data 파일의 경로를 설정해 놓는다.
     * @param path
     */
    public void setSoundData(String path)
    {
        this.path = path;
    }

    public void cleanUp()
    {
        is = null;
        System.gc();
    }


    /**
     * mix해 놓은 사운드 data를 해당 byte 배열에 읽어 놓는다.
     * @param index
     * @param off
     * @param len
     */
    public void addSound(int index, int off, int len)
    {
        try
        {
            is = getClass().getResourceAsStream(path);
            snd[index] = new byte[len];
            is.skip(off);
            is.read( snd[index], 0, len);
            is.close();
        } catch (Exception e)
        {
        }
    }

    /**
     * 지정해 놓은 index로 사운드를 재생한다.
     * @param index
     */
    public synchronized void play(int index)
    {
        closeClip();

        waitForReady();

        currentIndex = index;
        try
        {
            currentClip.open(snd[currentIndex], 0, snd[currentIndex].length);
        } catch (Exception e) {
        }
        bPlaying = true;
        this.notify();
    }

    /**
     * 임시로 어떤 파일을 재생할때 사용한다.
     * @param path
     */
    public synchronized void play(String path)
    {

        closeClip();
        waitForReady();
        try
        {
            is = getClass().getResourceAsStream(path);
            sndtmp = new byte[is.available()];
            is.read( sndtmp, 0, is.available());
            is.close();
            currentClip.open(sndtmp,0,sndtmp.length);
        } catch(Exception e){
            is = null;
        }
        currentIndex = -1;      // 임시로 재생 하는 번호
        bPlaying = true;
        this.notify();
    }

    /**
     * 처리하고 있는 중이라면 잠시 기다린다.
     */
    private void waitForReady()
    {
        int count=0;
        try
        {
            while(bReady==false)
            {

                this.wait(100);
                Thread.yield();

                count++;
                if(count>=50) break;

                // sleep을 사용하면 안됨.
            }
        } catch(Exception e) {}
    }

    /**
     * 현재 재생중인 사운드를 정지한다.
     */
    private void closeClip()
    {
        try
        {
            currentClip.stop();
        } catch(Exception e) {}

        try
        {
            currentClip.close();
        } catch(Exception e) {}
    }

    public void run()
    {
        while (true)
        {
            synchronized (this)
            {
                bReady = true;
                try
                {
                    // 잠시 기다린다.
                    while (!bPlaying)
                    {
                        this.wait();
                    }
                } catch (Exception e) {}
                bReady = false;
            }

            try
            {
                if(bOn==true)
                {       // 사운드가 켜져 있을때만 소리를 낸다.
                    currentClip.play();
                }

                synchronized (this)
                {
                    bPlaying = false;
                }
            } catch (Exception e)
            {
                bPlaying = false;
            } finally
            {
                sndtmp = null;
                try
                {
                    currentClip.close();
                } catch (Exception e) {}
            }
        }
    }

    /**
     * 사운드 재생 : On
     */
    public void turnOn()
    {
        bOn = true;
    }

    /**
     * 사운드 재생 : Off
     */
    public void turnOff()
    {
        bOn = false;
    }



    /**
     * 볼륨을 1 증가한다.
     */
    public void volUp()
    {
        try
        {
            AudioSystem.setVolume("mmf", AudioSystem.getVolume("mmf")+1);
        } catch (Exception e)
        {}
    }

    /**
     * 볼륨을 1감소한다.
     */
    public void volDown()
    {
        try
        {
            AudioSystem.setVolume("mmf", AudioSystem.getVolume("mmf")-1);
        } catch (Exception e)
        {}
    }

    /**
     * 볼륨을 설정한다.
     * @param volume
     */
    public void setVol(int volume)
    {
        try
        {
            AudioSystem.setVolume("mmf", volume);
        } catch (Exception e)
        {}
    }

    /**
     * 현재 설정된 볼륨값을 전달한다.
     * @return 현재volume값
     */
    public int getVol()
    {
        int vol = 0;
        try
        {
            vol = AudioSystem.getVolume("mmf");
        } catch (Exception e)
        {}

        return vol;
    }
}

위의 SoundThread 클래스를 추가하는 것만으로 사운드를 출력하기 위한 모든 준비는 끝납니다.

그럼 클래스를 좀더 자세히 살펴보도록 하지요.


먼저 생성자입니다.


    /**
     * Constructor #1.
     */
    public SoundThread()
    {
        this(11);
    }

    /**
     * Constructor #2.
     * @param maxSound
     */
    public SoundThread(int maxSound)
    {
        bOn = true;

        snd = new byte[maxSound][];

        try
        {
            currentClip = AudioSystem.getAudioClip("mmf");
        } catch(Exception e) {}
    }

생성자가 두개가 있는데요. Default 생성자를 호출하면 두번째 생성자로 11이라는 값을 넘겨 줍니다.

두번째 생성자는 인자로 받은 maxSound 만큼의 사운드 배열을 생성해 주는 역할을 합니다.

또, com.skt.m.AudioSystem 클래스의 getAudioClip() 메소드를 이용해서, mmf 오디오 클립을 얻어옵니다.


그리고 다음의 setSound() 메소드는, mix된 사운드 데이터 파일의 경로를 기억하는 메소드입니다.

    /**
     * mix해 놓은 사운드 data 파일의 경로를 설정해 놓는다.
     * @param path
     */
    public void setSoundData(String path)
    {
        this.path = path;
    }

mix된 사운드 data 파일은, mmf 형식의 사운드 파일들을 LBMMIXER를 이용해서 sound.dat에 mix 해 놓았습니다.

 

LBMMIXER는 다들 기억 하고 계시리라고 생각합니다.

원래 LBM, PNG 파일을 dat 파일로 mix하는 용도의 유틸리티이지만, mmf 파일을 mix하는데도 사용될수 있습니다.

왜냐하면 이 LBMMIXER 자체가 이미지의 헤더를 별도로 수정하거나 하는 것이 아니고,

단순히 여러개의 파일들의 내용을 하나의 파일에 일렬로 기록해 주는 기능만을 하기때문에,

실제로 LBM 파일 사이즈의 합이 mix후 생성된 dat파일의 사이즈와 동일하죠.


그러므로, mmf 파일을 이 유틸리티를 이용해서 mix 한다고 해서 특별히 mmf 파일에 손상이 가거나 하는 일은 없겠죠.

그러나 LBMMIXER 자체에서 읽어들일 수 있는 확장자를 LBM, PNG로 고정시켜 놓았기 때문에

mmf 파일을 LBMMIXER에서 직접 읽어 올 수는 없습니다. 때문에, mmf 파일의 확장자를 lbm으로 변경 시킨 후,

LBMMIXER에서 읽어와서 mixing 버튼을 눌러주면 짜잔 하고 dat 파일이 생성이 되겠죠.

그렇게 해서 합쳐 놓은 파일이 바로 sound 폴더의 sound.dat 파일입니다.


    /**
     * mix해 놓은 사운드 data를 해당 byte 배열에 읽어 놓는다.
     * @param index
     * @param off
     * @param len
     */
    public void addSound(int index, int off, int len)
    {
        try
        {
            is = getClass().getResourceAsStream(path);
            snd[index] = new byte[len];
            is.skip(off);
            is.read( snd[index], 0, len);
            is.close();
        } catch (Exception e)
        {
        }
    }

다음으로 위의 addSound() 메소드는,

사운드 배열 snd[]의 index에 sound.dat 파일의 offset부터 len 만큼을 byte 배열로 저장합니다.


    /**
     * 지정해 놓은 index로 사운드를 재생한다.
     * @param index
     */
    public synchronized void play(int index)
    {
        closeClip();

        waitForReady();

        currentIndex = index;
        try
        {
            currentClip.open(snd[currentIndex], 0, snd[currentIndex].length);
        } catch (Exception e) {}
bPlaying = true; this.notify(); }

대망의 play 메소드입니다. 당연히 지정한 index의 사운드를 재생하는 역할을 하는 메소드이구요.

현재 재상할 클립(currentClip)을 오픈해서 notify()를 호출하면 사운드가 재생됩니다.


위에서 호출된 waitForReady()는 사운드를 출력할 준비가 아직 안되었다면,

사운드를 출력할 준비가 될때까지 루프를 돌면서 기다리는 역할을 하는 메소드입니다.


    /**
     * 처리하고 있는 중이라면 잠시 기다린다.
     */
    private void waitForReady()
    {
        int count=0;
try { while(bReady==false) { this.wait(100); Thread.yield(); count++; if(count>=50) break; // sleep을 사용하면 안됨. } } catch(Exception e) {} }

bReady가 false인 동안 계속해서 100 ms씩 간격을 두고 bReady가 true가 될때까지 기다린 후,

5초 이상(100ms * 50)을 기다렸는데도 bReady가 풀리지 않는 다면, 그냥 루프를 탈출합니다.


    /**
     * 현재 재생중인 사운드를 정지한다.
     */
    private void closeClip()
    {
        try
        {
            currentClip.stop();
        } catch(Exception e) {}

        try
        {
            currentClip.close();
        } catch(Exception e) {}
    }

closeClip() 메소드는 현재 재생중인 사운드를 정지시켜 줍니다.

현재 재생중인 사운드를 정지(stop())하고, 열려 있는 클립을 닫아주면(close()) 끝나죠.


이제 사운드를 출력하기 위한 준비는 모두 끝났습니다. 이제 준비된 SoundThread 클래스를 사용하기 위해

twogp1p 패키지의 Midlet 클래스에, initSound() 라는 메소드를 정의해줍니다.

/**
 * 사운드를 설정한다.
 */
private void initSound()
{
    sound.setSoundData("/sound/sound.dat");

    sound.addSound(SND_ABELL,0,199);
    sound.addSound(SND_CARD, 199,510);		
    sound.addSound(SND_CHOICE,709,203);
    sound.addSound(SND_GAMEOVER,912,2931);
    sound.addSound(SND_GAMESTARTED,3843,4016);
    sound.addSound(SND_GO,7859,1424);
    sound.addSound(SND_LONGBELL,9283,203);
    sound.addSound(SND_MATCH,9486,728);
    sound.addSound(SND_MYTURN,10214,1632);
    sound.addSound(SND_NOMATCH,11846,593);
    sound.addSound(SND_VOLUMEKEY,12439,148);
		
    sound.cleanUp();
}

addSound 메소드를 이용해서 각 인덱스 별로 오프셋과 mmf의 사이즈(byte 단위)를 지정해 줍니다.


각각의 사운드 인덱스 상수 별로 간단히 사운드의 용도를 설명드리자면,


SND_ABELL : 짧은 확인 사운드
SND_LONGBELL : 긴 확인 사운드
SND_CHOICE : 플레이어의 턴에서 고/스톱, 흔듬/폭탄, 국진등의 선택을 해야할때 출력
SND_MYTURN : 플레이어의 턴일때 출력
SND_CARD : 카드를 나눠 줄때 출력
SND_MATCH : 카드를 냈을때 바닥에 동일한 카드가 있을때 출력
SND_NOMATCH : 카드를 냈지만 바닥에 동일한 카드가 없을때 출력
SND_GO : 고를 했을때 출력
SND_RESULT : 게임이 끝나고 결과가 출력될때 출력
SND_VOLUMEKEY : 볼륨키를 눌렀을때 출력

이제 사운드를 출력하고 싶다면 다음과 같이 play() 메소드에 인덱스 상수를 넘겨주면 됩니다.

    sound.play( 사운드 인덱스 상수 );


단순히 위의 한 문장을 삽입한 것이므로 따로 설명을 할 필요는 없을 것 같네요.

위의 상황이 발생하는 곳에다가 play() 메소드를 이용해서 사운드를 출력해 주시면 되리라고 봅니다.


이제 다음으로는 진동을 출력하는 방법을 배워보죠.

진동은 사용자나 커퓨터가 흔들었을때, 폭탄했을때, 쪽/뻑/따닥이 났을때, 쓸일때 등에서 진동이 발생합니다.

진동을 하기 위해서는 SK-VM에서 제공하는 Vibration 이라는 클래스를 이용하면 간단히 해결됩니다.


이 게임에서 진동을 사용할 일은 위에서 열거한 사항들(흔듬/폭탄/쪽/뻑/따닥/쓸) 뿐이므로,

진동이 사용되는 곳이 GameView 클래스로 한정됩니다. 그러므로 GameView 클래스에,

import com.skt.m.Vibration;

위와 같이 Vibration 클래스를 import 해 주기만 하면 됩니다.


이제 진동을 사용하기 위해서는,

Vibration.start(level, timeout);

level은 진동의 강도인데, 아직 SK-VM에서 지원은 하지 않고, 임시로 만들어져 있는 것이므로,

현재는 그냥 단순히 0으로 지정을 해 주시면 됩니다.

timeout은 진동이 울릴 시간을 입력해 주시면 되며, 단위는 ms 입니다.


예를 들어 진동을 300 ms 동안 울리게 하고 싶다면,

Vibration.start(0, 300);

위와 같이 해 주시면 되겠죠?


이걸로 사운드 출력과 진동 처리는 모두 알아본 것 같네요.

이제 사운드 출력과 진동을 처리 해 주면, 게임에서 멋진 사운드가 흘러 나올겁니다.

Posted by maysent
: