MP3 재생하기

RobotActivity 클래스를 상속한 액티비티를 만들고, onInitialized(Robot) 메소드와 onExecute() 메소드를 상속받아 새로 구현하자. 프로젝트에 스마트 로봇 라이브러리를 포함하는 것을 잊지 않기를 바란다.

 public class SampleActivity extends RobotActivity
 {
     @Override
     public void onCreate(Bundle savedInstanceState)
     {
         super.onCreate(savedInstanceState);
         setContentView(R.layout.main);
     }

     @Override
     public void onInitialized(Robot robot)
     {
     }

     @Override
     public void onExecute()
     {
     }
 }

알버트 로봇에는 소리를 스피커로 출력하는 디바이스로 EFFECTOR_SPEAKER가 있다. 20ms마다 480개의 데이터를 출력하므로 480 / 0.02 = 24000 Hz로 샘플링된 PCM 데이터를 사용한다.

 public class SampleActivity extends RobotActivity
 {
     private Device mSpeakerDevice;

     @Override
     public void onCreate(Bundle savedInstanceState)
     {
         super.onCreate(savedInstanceState);
         setContentView(R.layout.main);
     }

     @Override
     public void onInitialized(Robot robot)
     {
         mSpeakerDevice = robot.findDeviceById(Albert.EFFECTOR_SPEAKER);
     }

     @Override
     public void onExecute()
     {
     }
 }

onExecute() 메소드에서 스피커 디바이스에 PCM 데이터를 쓰면 알버트 로봇의 스피커로 소리가 출력되는데, MP3 파일을 재생하기 위해서는 MP3 데이터를 PCM 데이터로 디코딩하는 과정이 필요하다.

일반적으로는 MP3 파일이 24000 Hz로 샘플링된 것이라고 하더라도 MP3 디코더가 한번에 디코딩하는 데이터의 양이 정확하게 480개가 아니며 디코딩하는데 시간이 소요되기 때문에 MP3 디코딩을 onExecute() 메소드 내에서 하는 것은 바람직하지 않다.

따라서 onExecute() 메소드에서는 디코딩된 결과를 스피커 디바이스에 쓰는 일만 하고, 실제로 MP3 파일을 디코딩하는 작업은 따로 쓰레드를 만들어서 하는 것이 좋다. MP3 파일을 디코딩하는 주기와 onExecute() 메소드가 호출되는 주기에는 차이가 있기 때문에 큐를 만들고, 쓰레드에서는 MP3 파일을 디코딩하여 얻은 PCM 데이터를 큐에 넣고 onExecute() 메소드에서는 큐에서 PCM 데이터를 꺼내어 스피커 디바이스에 쓰도록 하자.

큐는 여러 가지 방식으로 구현할 수 있는데 여기서는 간단하게 하기 위해 ArrayList를 사용하였다. 좀더 효율적인 큐는 각자 다른 방식으로 구현해 보도록 하자.

 public class SampleActivity extends RobotActivity
 {
     private Device mSpeakerDevice;
     final ArrayList mPcmQueue = new ArrayList();

     @Override
     public void onCreate(Bundle savedInstanceState)
     {
         super.onCreate(savedInstanceState);
         setContentView(R.layout.main);
     }

     @Override
     public void onInitialized(Robot robot)
     {
         mSpeakerDevice = robot.findDeviceById(Albert.EFFECTOR_SPEAKER);

         Mp3Thread thread = new Mp3Thread();
         thread.setDaemon(true);
         thread.start();
     }

     @Override
     public void onExecute()
     {
         int[] data = null;
         synchronized(mPcmQueue)
         {
             if(!mPcmQueue.isEmpty())
                 data = mPcmQueue.remove(0); // 큐에서 PCM 데이터를 꺼낸다.
         }
         if(data != null)
             mSpeakerDevice.write(data); // PCM 데이터를 스피커 디바이스에 쓴다.
     }

     private class Mp3Thread extends Thread
     {
         @Override
         public void run()
         {
         }
     }
 }

이제 쓰레드에서 MP3 파일을 PCM 데이터로 디코딩하는 일만 남았다. 하나씩 간단하게 해보기 위해서 우선 MP3 파일이 24000 Hz로 샘플링되어 있다고 가정하고 디코딩 해보도록 하자. 또한 MP3 파일(music.mp3)은 res 폴더의 raw 폴더에 있다고 가정하였는데, 다른 곳에 있더라도 InputStream만 얻으면 된다. 예를 들어, 서버에 있는 경우에는 HttpURLConnection을 통해 InputStream을 얻을 수 있고, assets 폴더에 있는 경우에는 AssetManager, SD 카드 등에 있는 경우에는 FileInputStream 등을 사용하면 된다.

우선 MP3 파일을 열고 닫는 부분부터 만들어 보자.

 public class SampleActivity extends RobotActivity
 {
     private Device mSpeakerDevice;
     final ArrayList mPcmQueue = new ArrayList();

     @Override
     public void onCreate(Bundle savedInstanceState)
     {
         super.onCreate(savedInstanceState);
         setContentView(R.layout.main);
     }

     @Override
     public void onInitialized(Robot robot)
     {
         mSpeakerDevice = robot.findDeviceById(Albert.EFFECTOR_SPEAKER);

         Mp3Thread thread = new Mp3Thread();
         thread.setDaemon(true);
         thread.start();
     }

     @Override
     public void onExecute()
     {
         int[] data = null;
         synchronized(mPcmQueue)
         {
             if(!mPcmQueue.isEmpty())
                 data = mPcmQueue.remove(0);
         }
         if(data != null)
             mSpeakerDevice.write(data);
     }

     private class Mp3Thread extends Thread
     {
         private InputStream open(int resid)
         {
             try
             {
                 return new BufferedInputStream(getResources().openRawResource(resid));
             } catch (NotFoundException e)
             {
                 return null;
             }
         }

         private void close(InputStream is)
         {
             if(is == null) return;
             try
             {
                 is.close();
             } catch (IOException e)
             {
             }
         }

         @Override
         public void run()
         {
         }
     }
 }

이제 run() 메소드 내에서 MP3 파일을 디코딩하고 큐에 넣도록 하자.

 public class SampleActivity extends RobotActivity
 {
     private Device mSpeakerDevice;
     final ArrayList mPcmQueue = new ArrayList();

     @Override
     public void onCreate(Bundle savedInstanceState)
     {
         super.onCreate(savedInstanceState);
         setContentView(R.layout.main);
     }

     @Override
     public void onInitialized(Robot robot)
     {
         mSpeakerDevice = robot.findDeviceById(Albert.EFFECTOR_SPEAKER);

         Mp3Thread thread = new Mp3Thread();
         thread.setDaemon(true);
         thread.start();
     }

     @Override
     public void onExecute()
     {
         int[] data = null;
         synchronized(mPcmQueue)
         {
             if(!mPcmQueue.isEmpty())
                 data = mPcmQueue.remove(0);
         }
         if(data != null)
             mSpeakerDevice.write(data);
     }

     private class Mp3Thread extends Thread
     {
         private static final int MAX_QUEUE_SIZE = 10;

         private InputStream open(int resid)
         {
             try
             {
                 return new BufferedInputStream(getResources().openRawResource(resid));
             } catch (NotFoundException e)
             {
                 return null;
             }
         }

         private void close(InputStream is)
         {
             if(is == null) return;
             try
             {
                 is.close();
             } catch (IOException e)
             {
             }
         }

         @Override
         public void run()
         {
             InputStream is = open(R.raw.music); // MP3 파일을 연다.
             if(is == null) return;

             // 여기서 MP3 디코더를 초기화한다.

             // MP3 디코더의 프레임 크기를 얻는다.
             // 프레임 크기는 MP3 디코더로부터 한번에 얻는 PCM 데이터의 개수이다.
             int frameSize = getDecoderFrameSize(); // getDecoderFrameSize()는 가상의 메소드
             int[] buffer = new int[frameSize];
             int[] pcmBuffer = new int[4096];
             int pcmBufferPos = 0;

             while(true)
             {
                 int sz = 0;
                 synchronized(mPcmQueue)
                 {
                     sz = mPcmQueue.size();
                 }

                 // MP3 디코딩 속도가 빠르면 큐에 너무 많이 쌓이기 때문에 일정 개수 이상이 되면 쉬어준다.
                 if(sz < MAX_QUEUE_SIZE)
                 {
                     // MP3 디코더를 사용하여 디코딩한다.
                     // MP3 파일의 InputStream인 is로부터 MP3 데이터를 읽고, 디코딩 결과인 PCM 데이터를 buffer에 쓴다.
                     // 디코딩된 PCM 데이터의 개수를 반환한다.
                     int len = decode(is, buffer); // decode(...)는 가상의 메소드
                     if(len < 0) break;

                     // 디코딩된 PCM 데이터의 개수는 정확하게 480이 아니기 때문에 480개가 얻어질 때마다 큐에 넣는다.
                     System.arraycopy(buffer, 0, pcmBuffer, pcmBufferPos, len);
                     pcmBufferPos += len;

                     while(pcmBufferPos >= 480)
                     {
                         int[] data = new int[480];
                         System.arraycopy(pcmBuffer, 0, data, 0, 480);

                         synchronized(mPcmQueue)
                         {
                             mPcmQueue.add(data); // 큐에 넣는다.
                         }

                         System.arraycopy(pcmBuffer, dataSize, pcmBuffer, 0, pcmBufferPos - dataSize);
                         pcmBufferPos -= dataSize;
                     }
                 }

                 try
                 {
                     Thread.sleep(10);
                 } catch (InterruptedException e)
                 {
                     break;
                 }
             }
             close(is); // MP3 파일을 닫는다.

             // 여기서 MP3 디코더를 종료한다.
         }
     }
 }

MP3 디코더는 다양한 라이브러리가 공개되어 있는데, Lame 라이브러리를 사용하면 다음과 같다.

 public class SampleActivity extends RobotActivity
 {
     private Device mSpeakerDevice;
     final ArrayList mPcmQueue = new ArrayList();

     @Override
     public void onCreate(Bundle savedInstanceState)
     {
         super.onCreate(savedInstanceState);
         setContentView(R.layout.main);
     }

     @Override
     public void onInitialized(Robot robot)
     {
         mSpeakerDevice = robot.findDeviceById(Albert.EFFECTOR_SPEAKER);

         Mp3Thread thread = new Mp3Thread();
         thread.setDaemon(true);
         thread.start();
     }

     @Override
     public void onExecute()
     {
         int[] data = null;
         synchronized(mPcmQueue)
         {
             if(!mPcmQueue.isEmpty())
                 data = mPcmQueue.remove(0);
         }
         if(data != null)
             mSpeakerDevice.write(data);
     }

     private class Mp3Thread extends Thread
     {
         private static final int MAX_QUEUE_SIZE = 10;

         private InputStream open(int resid)
         {
             try
             {
                 return new BufferedInputStream(getResources().openRawResource(resid));
             } catch (NotFoundException e)
             {
                 return null;
             }
         }

         private void close(InputStream is)
         {
             if(is == null) return;
             try
             {
                 is.close();
             } catch (IOException e)
             {
             }
         }

         @Override
         public void run()
         {
             InputStream is = open(R.raw.music);
             if(is == null) return;

             if(Lame.initializeDecoder() < 0) return; // MP3 디코더를 초기화한다.

             // MP3 디코더의 프레임 크기를 얻을 수 있는지 확인한다.
             try
             {
                 if(Lame.configureDecoder(is) < 0)
                 {
                     close(is);
                     Lame.closeDecoder();
                     return;
                 }
             } catch (IOException e)
             {
                 close(is);
                 Lame.closeDecoder();
                 return;
             }

             int frameSize = Lame.getDecoderFrameSize(); // MP3 디코더의 프레임 크기를 얻는다.
             short[] leftBuffer = new short[frameSize];
             short[] rightBuffer = new short[frameSize];
             int[] pcmBuffer = new int[4096];
             int pcmBufferPos = 0;

             while(true)
             {
                 int sz = 0;
                 synchronized(mPcmQueue)
                 {
                     sz = mPcmQueue.size();
                 }

                 if(sz < MAX_QUEUE_SIZE)
                 {
                     int len = Lame.decodeFrame(is, leftBuffer, rightBuffer); // MP3 디코더를 사용하여 디코딩한다.
                     if(len < 0) break;

                     for(int i = 0; i < len; ++i)
                         pcmBuffer[i + pcmBufferPos] = (leftBuffer[i] + rightBuffer[i]) / 2;
                     pcmBufferPos += len;

                     while(pcmBufferPos >= 480)
                     {
                         int[] data = new int[480];
                         System.arraycopy(pcmBuffer, 0, data, 0, 480);

                         synchronized(mPcmQueue)
                         {
                             mPcmQueue.add(data);
                         }

                         System.arraycopy(pcmBuffer, dataSize, pcmBuffer, 0, pcmBufferPos - dataSize);
                         pcmBufferPos -= dataSize;
                     }
                 }

                 try
                 {
                     Thread.sleep(10);
                 } catch (InterruptedException e)
                 {
                     break;
                 }
             }
             close(is);

             Lame.closeDecoder(); // MP3 디코더를 종료한다.
         }
     }
 }

이제 MP3 파일이 24000 Hz로 샘플링되지 않은 경우를 살펴보자. 이 경우에는 디코딩된 PCM 데이터의 샘플링 주파수가 24000 Hz가 아니기 때문에 디코딩된 PCM 데이터를 그냥 큐에 넣으면 소리가 좀더 빨리 혹은 느리게 재생되는 것처럼 느껴질 것이다.

따라서 디코딩된 PCM 데이터를 24000 Hz로 다시 샘플링해야 한다. 다시 샘플링하는 방법은 여러 가지가 있는데 여기서는 쉽게 하기 위해 선형 보간으로 샘플링해보도록 하자.

 public class SampleActivity extends RobotActivity
 {
     private Device mSpeakerDevice;
     final ArrayList mPcmQueue = new ArrayList();

     @Override
     public void onCreate(Bundle savedInstanceState)
     {
         super.onCreate(savedInstanceState);
         setContentView(R.layout.main);
     }

     @Override
     public void onInitialized(Robot robot)
     {
         mSpeakerDevice = robot.findDeviceById(Albert.EFFECTOR_SPEAKER);

         Mp3Thread thread = new Mp3Thread();
         thread.setDaemon(true);
         thread.start();
     }

     @Override
     public void onExecute()
     {
         int[] data = null;
         synchronized(mPcmQueue)
         {
             if(!mPcmQueue.isEmpty())
                 data = mPcmQueue.remove(0);
         }
         if(data != null)
             mSpeakerDevice.write(data);
     }

     private class Mp3Thread extends Thread
     {
         private static final int MAX_QUEUE_SIZE = 10;

         private InputStream open(int resid)
         {
             try
             {
                 return new BufferedInputStream(getResources().openRawResource(resid));
             } catch (NotFoundException e)
             {
                 return null;
             }
         }

         private void close(InputStream is)
         {
             if(is == null) return;
             try
             {
                 is.close();
             } catch (IOException e)
             {
             }
         }

         @Override
         public void run()
         {
             InputStream is = open(R.raw.music);
             if(is == null) return;

             if(Lame.initializeDecoder() < 0) return;

             try
             {
                 if(Lame.configureDecoder(is) < 0)
                 {
                     close(is);
                     Lame.closeDecoder();
                     return;
                 }
             } catch (IOException e)
             {
                 close(is);
                 Lame.closeDecoder();
                 return;
             }

             int sampleRate = Lame.getDecoderSampleRate(); // MP3 파일의 샘플링 주파수를 얻는다.
             // 24000 Hz일 때 480개인데 MP3 파일의 샘플링 주파수로 하면 몇 개가 되는지 계산한다.
             int dataSize = 480 * sampleRate / 24000;
             int frameSize = Lame.getDecoderFrameSize();
             short[] leftBuffer = new short[frameSize];
             short[] rightBuffer = new short[frameSize];
             int[] pcmBuffer = new int[4096];
             int pcmBufferPos = 0;

             while(true)
             {
                 int sz = 0;
                 synchronized(mPcmQueue)
                 {
                     sz = mPcmQueue.size();
                 }

                 if(sz < MAX_QUEUE_SIZE)
                 {
                     int len = Lame.decodeFrame(is, leftBuffer, rightBuffer);
                     if(len < 0) break;

                     for(int i = 0; i < len; ++i)
                         pcmBuffer[i + pcmBufferPos] = (leftBuffer[i] + rightBuffer[i]) / 2;
                     pcmBufferPos += len;

                     float pos, step = (dataSize - 1) / 479.0f;
                     int index, current;
                     // 디코딩된 PCM 데이터가 dataSize 개수만큼 얻어질 때마다 큐에 넣는다.
                     while(pcmBufferPos >= dataSize)
                     {
                         int[] data = new int[480];

                         // 다시 샘플링한다.
                         // 즉, dataSize 개수의 PCM 데이터를 480개의 PCM 데이터로 만든다.
                         data[0] = pcmBuffer[0];
                         pos = step;
                         for(int i = 1; i < 479; ++i)
                         {
                             index = (int)pos;
                             current = pcmBuffer[index];
                             data[i] = (int)(((pcmBuffer[index + 1] - current) * (pos - index) + current));
                             pos += step;
                         }
                         data[479] = pcmBuffer[dataSize - 1];

                         synchronized(mPcmQueue)
                         {
                             mPcmQueue.add(data);
                         }

                         System.arraycopy(pcmBuffer, dataSize, pcmBuffer, 0, pcmBufferPos - dataSize);
                         pcmBufferPos -= dataSize;
                     }
                 }

                 try
                 {
                     Thread.sleep(10);
                 } catch (InterruptedException e)
                 {
                     break;
                 }
             }
             close(is);

             Lame.closeDecoder();
         }
     }
 }