0

Opensl trong android

I. OpenSL ES là gì

OpenSL ES cung cấp một giao diện ngôn ngữ C mà cũng có thể truy cập sử dụng C ++. Nó cho thấy các tính năng tương tự như phần âm thanh của các API Android Java:

  • android.media.MediaPlayer
  • android.media.MediaRecorder Như với tất cả các Android Native Development Kit (NDK), mục đích chính của OpenSL ES trong Android là để tạo điều kiện cho việc thực hiện của các thư viện chia sẻ được gọi bằng cách sử dụng Java Native Interface ( JNI ). NDK không dành cho văn bản C / C ++ ứng dụng thuần túy. Tuy nhiên, OpenSL ES là một API đầy đủ tính năng, và chúng tôi hy vọng rằng bạn sẽ có thể thực hiện được hầu hết nhu cầu âm thanh của bạn chỉ sử dụng API này, mà không lên cuộc gọi đến mã chạy trong thời gian chạy Android.

OpenSL ES API có năm đặc điểm chính:

  • Phát lại âm thanh cơ bản và ghi âm
  • Hiệu ứng âm thanh 3D bao gồm cả âm thanh 3D định vị
  • Trải nghiệm âm nhạc tăng cường hiệu ứng bao gồm tăng độ bass và reverb môi trường
  • Âm nhạc tương tác và nhạc chuông sử dụng SP-MIDI, Mobile DLS, Mobile XMF
  • Buffer Queues

II. Thiết lập môi trường

Đối với dự án này, bạn sẽ cần phải đi đến trang web phát triển Google Android và tải về tất cả các công cụ. Chúng bao gồm các SDK, NDK và eclipse plugin. Bạn cũng sẽ cần phải nhận được các IDE Eclipse , phiên bản "cổ điển" có lẽ là thích hợp nhất cho công việc này. Hướng dẫn cài đặt những gói này là rất rõ ràng và có rất nhiều thông tin trên mạng internet để giúp bạn nếu mọi thứ đi sai.

Một công cụ hữu ích cho sự phát triển Android là SWIG , được sử dụng để tạo ra các mã Java để bọc các chức năng C chúng ta sẽ viết. Nó không phải là hoàn toàn cần thiết, bởi vì bạn có thể sử dụng JNI trực tiếp. Tuy nhiên, nó là rất tiện dụng, như JNI không phải là mảnh đơn giản nhất của phát triển phần mềm xung quanh (một số sẽ gọi nó là "một cơn ác mộng "). SWIG bọc mã C rất tốt và nó đơn giản hóa quá trình vô cùng. Chúng tôi sẽ sử dụng nó trong ví dụ thảo luận ở đây.

III. Project ví dụ

Link download dự án: https://duong_thanh_tung@bitbucket.org/audiotest/audio-test.git Dự án bao gồm một dự án NDK cho OpenSL modul I/O và một dự án Eclipse cho ví dụ ứng dụng. Các dự án NDK là lần đầu tiên được xây dựng bằng cách chạy 1 file build.sh.

$ sh build.sh

Để chạy được file .sh này đầu tiên thiết lập vị trí của NDK tải về (bạn sẽ cần phải thiết lập này để phù hợp với địa điểm hệ thống của bạn)

export ANDROID_NDK_ROOT=$HOME/work/android-ndk-r7

và sau đó tiến hành gọi SWIG để xây dựng các mã giao diện Java sẽ liên kết ví dụ mô-đun C opensl của chúng tôi để các ứng dụng. Nó tạo ra cả một tập C++, gói mã C và các lớp Java chúng ta cần phải sử dụng để chạy nó.

swig -java -package opensl_example -includeall -verbose -outdir src/opensl_example -c++ -I/usr/local/include -I/System/Library/Frameworks/JavaVM.framework/Headers -I./jni -o jni/java_interface_wrap.cpp opensl_example_interface.i

Khi điều này được thực hiện, nó gọi NDK build script,

$ ANDROID_NDK_ROOT/ndk-build TARGET_PLATFORM = android-9 V = 1

và sẽ xây dựng một mô-đun tự động-có thể nạp (.so) có chứa mã nguồn gốc của chúng tôi. Script này là điều khiển để sử dụng tập tin Android.mk trong thư mục ./jni.

Một khi các phần NDK được xây dựng, chúng ta có thể chuyển sang Eclipse. Sau khi bắt đầu nó, chúng ta phải nhập khẩu dự án bằng cách sử dụng File-> Import và sau đó chọn "Import into existing workspace" ,nó sẽ mở dự án (android-audiotest). Nếu mọi thứ đều tiến hành theo đúng kế hoạch, bạn chỉ cần cắm thiết bị của bạn và chọn build (ứng dụng Android). Ứng dụng này sẽ được xây dựng và chạy trong thiết bị. Tại thời điểm này, bạn sẽ có thể nói vào mic và nghe giọng nói của bạn qua loa (hoặc, đúng hơn, một đôi tai nghe).

IV. Các mã lập trình trong file native

Hai file nguồn phần native của dự án này: opensl_io.c, trong đó có tất cả các chức năng truyền âm thanh; và opensl_example.c, trong đó sử dụng chúng để thực hiện đơn giản âm thanh. Một tài liệu tham khảo cho các đặc điểm kỹ thuật API OpenSL được tìm thấy trong các OpenSL ES 1.0.1 , mà còn được phân phối trong các thư mục docs / opensl Android NDK. Hiện chúng tôi tìm thấy một số tài liệu hướng dẫn cụ thể về việc thực hiện Android của API: https://developer.android.com/ndk/guides/audio/opensl-for-android.html#inherited.

  1. Mở các thiết bị cho đầu ra âm thanh

Các điểm vào OpenSL là thông qua việc tạo ra các âm thanh,vd:

result = slCreateEngine(&(p->engineObject), 0, NULL, 0, NULL, NULL);

Đây initialises một đối tượng động cơ của các loại SLObjectItf (mà trong ví dụ trên được tổ chức trong một cấu trúc dữ liệu được trỏ bởi p ). Khi một động cơ được tạo ra, nó cần phải được thực hiện (điều này sẽ là một quá trình phổ biến với các đối tượng OpenSL, sáng tạo tiếp theo thực hiện). Sau đó là một giao diện cơ thu được, mà sẽ được sử dụng sau đó để mở và khởi tạo các thiết bị đầu vào và đầu ra (với nguồn và bồn rửa của họ)

result = (*p->engineObject)->Realize(p->engineObject, SL_BOOLEAN_FALSE);
...
result = (*p->engineObject)->GetInterface(p->engineObject, SL_IID_ENGINE, &(p->engineEngine));

Một khi các giao diện cho các đối tượng thu được, chúng ta có thể sử dụng nó để tạo ra các đối tượng API khác. Nói chung, đối với tất cả các đối tượng API, chúng tôi:

  • tạo ra các đối tượng (instantiation)
  • nhận ra nó (khởi động)
  • có được một giao diện với nó (để truy cập bất kỳ tính năng cần thiết), thông qua các phương pháp GetInterface ()

Trong trường hợp phát lại, các đối tượng đầu tiên được tạo ra là Output Mix (cũng là một SLObjectItf), và sau đó nhận ra:

const SLInterfaceID id [] = {SL_IID_VOLUME}; 
const SLboolean req [] = {} SL_BOOLEAN_FALSE 
result = (* p-> engineEngine) -> CreateOutputMix (p-> engineEngine, 
                                    & (p-> outputMixObject), 1, id, req); 
... 
result= (* p-> outputMixObject) -> Realize(p-> outputMixObject, 
                                                 SL_BOOLEAN_FALSE);

Như vậy chúng ta sẽ không cần phải điều khiển nó để có được giao diện. Bây giờ, chúng ta cấu hình nguồn và chìm của một đối tượng người chơi, chúng tôi sẽ cần phải tạo ra. Đối với đầu ra, nguồn sẽ là một hàng đợi đệm, đó là nơi mà chúng tôi sẽ gửi mẫu dữ liệu âm thanh của chúng tôi. Chúng tôi cấu hình nó với các thông số thông thường: định dạng dữ liệu, kênh, tỷ lệ lấy mẫu (sr), vv:

SLDataLocator_AndroidSimpleBufferQueue loc_bufq = 
                           {SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, 2}; 
SLDataFormat_PCM format_pcm = {SL_DATAFORMAT_PCM, kênh, sr, 
               SL_PCMSAMPLEFORMAT_FIXED_16, SL_PCMSAMPLEFORMAT_FIXED_16, 
               loa, SL_BYTEORDER_LITTLEENDIAN}; 
SLDataSource audioSrc = {& loc_bufq, & format_pcm};

và output mix, chúng tôi tạo ra ở trên:

SLDataLocator_OutputMix loc_outmix = {SL_DATALOCATOR_OUTPUTMIX, 
                                                p-> outputMixObject}; 
SLDataSink audioSnk = {& loc_outmix, NULL};

Các đối tượng người chơi âm thanh sau đó được tạo ra với nguồn này

const SLInterfaceID ids1 [] = {SL_IID_ANDROIDSIMPLEBUFFERQUEUE}; 
const SLboolean req1 [] = {SL_BOOLEAN_TRUE}; 
result= (* p-> engineEngine) -> CreateAudioPlayer (p-> engineEngine, 
                    & (p-> bqPlayerObject), & audioSrc, & audioSnk, 
                     1, ids1, req1); 
... 
result = (* p-> bqPlayerObject) -> Realize(p-> bqPlayerObject,SL_BOOLEAN_FALSE);

Sau đó, chúng tôi có được giao diện đối tượng người chơi,

result = (*p->bqPlayerObject)->GetInterface(p->bqPlayerObject,  SL_IID_PLAY,&(p->bqPlayerPlay));

và giao diện hàng đợi đệm (loại SLBufferQueueItf)

result = (*p->bqPlayerObject)->GetInterface(p->bqPlayerObject, SL_IID_ANDROIDSIMPLEBUFFERQUEUE, &(p->bqPlayerBufferQueue));

Các API OpenSL cung cấp một cơ chế gọi lại cho âm thanh IO. Tuy nhiên, không giống như hiện thực khác không đồng bộ âm thanh IO, giống như trong CoreAudio hoặc Jack, gọi lại không vượt qua các bộ đệm âm thanh cho chế biến, là một trong những đối số của nó. Thay vào đó, các cuộc gọi lại chỉ được sử dụng để báo hiệu các ứng dụng, chỉ ra rằng hàng đợi đệm đã sẵn sàng để nhận dữ liệu.

Các giao diện hàng đợi đệm thu được ở trên sẽ được sử dụng để thiết lập một cuộc gọi lại (bqPlayerCallback, được thông qua p như ngữ cảnh):

result = (*p->bqPlayerBufferQueue)->RegisterCallback( p->bqPlayerBufferQueue,bqPlayerCallback, p);

Cuối cùng, các giao diện người chơi được sử dụng để bắt đầu phát lại âm thanh:

result = (*p->bqPlayerPlay)->SetPlayState(p->bqPlayerPlay, SL_PLAYSTATE_PLAYING);
  1. Mở các thiết bị cho đầu vào âm thanh

Quá trình bắt đầu ghi dữ liệu âm thanh rất giống với phát lại. Đầu tiên chúng ta thiết lập nguồn của chúng tôi và chìm, đó sẽ là đầu vào âm thanh và một hàng đợi đệm, tương ứng:

SLDataLocator_IODevice loc_dev = {SL_DATALOCATOR_IODEVICE, 
                      SL_IODEVICE_AUDIOINPUT, 
                      SL_DEFAULTDEVICEID_AUDIOINPUT, NULL}; 
SLDataSource audioSrc = {& loc_dev, NULL}; 
... 
SLDataLocator_AndroidSimpleBufferQueue loc_bq = 
                      {SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, 2}; 
SLDataFormat_PCM format_pcm = {SL_DATAFORMAT_PCM, kênh, sr, 
          SL_PCMSAMPLEFORMAT_FIXED_16, SL_PCMSAMPLEFORMAT_FIXED_16, 
          loa, SL_BYTEORDER_LITTLEENDIAN}; 
SLDataSink audioSnk = {& loc_bq, & format_pcm};

Sau đó, chúng tôi tạo ra một máy ghi âm thanh, nhận ra nó và có được giao diện của nó:

const SLInterfaceID id [1] = {SL_IID_ANDROIDSIMPLEBUFFERQUEUE}; 
const SLboolean req [1] = {SL_BOOLEAN_TRUE}; 
result = (* p-> engineEngine) -> CreateAudioRecorder (p-> engineEngine, 
                              & (p-> recorderObject), & audioSrc, 
                               & audioSnk, 1, id, req); 
... 
result = (* p-> recorderObject) -> Nhận ra (p-> recorderObject, 
                                          SL_BOOLEAN_FALSE); 
... 
result = (* p-> recorderObject) -> GetInterface (p-> recorderObject, 
                           SL_IID_RECORD, & (p-> recorderRecord));

Các giao diện hàng đợi đệm thu được và gọi lại các thiết lập:

result = (* p-> recorderObject) -> GetInterface (p-> recorderObject, 
     SL_IID_ANDROIDSIMPLEBUFFERQUEUE, & (p-> recorderBufferQueue)); 
... 
result = (* p-> recorderBufferQueue) -> RegisterCallback ( 
                   p-> recorderBufferQueue, bqRecorderCallback, p);

Bây giờ chúng ta có thể bắt đầu ghi âm:

result = (* p-> recorderRecord) -> SetRecordState ( p-> recorderRecord, SL_RECORDSTATE_RECORDING);

  1. Âm thanh IO

Streaming audio đến từ các thiết bị được thực hiện bằng phương thức Enqueue () của SLBufferQueueItf:

SLresult (* Enqueue) (SLBufferQueueItf tự, 
                     const void * Kích thước pBuffer, SLuint32);

Điều này nên được gọi là bất cứ khi nào hàng đợi đệm đã sẵn sàng cho một bộ đệm dữ liệu mới (hoặc cho vào hay đầu ra). Ngay sau khi các cầu thủ hoặc ghi đối tượng được đặt vào chơi hoặc quay nhà nước, các hàng đợi bộ đệm sẽ được sẵn sàng cho dữ liệu. Sau này, các cơ chế gọi lại sẽ chịu trách nhiệm về hiệu ứng mà các hàng đợi đệm là sẵn sàng cho một khối dữ liệu. Chúng ta có thể gọi phương thức Enqueue () trong việc gọi lại chính nó, hoặc ở nơi khác. Nếu chúng ta quyết định cho trước đây, để có được những cơ chế gọi lại chạy, chúng ta cần phải enqueue một bộ đệm khi chúng ta bắt đầu ghi hoặc chơi, nếu không gọi lại sẽ không bao giờ được gọi.

Một cách khác là sử dụng các cuộc gọi lại chỉ để thông báo cho các ứng dụng, chờ đợi nó như chúng ta có một bộ đệm đầy đủ để cung cấp. Trong trường hợp này, chúng ta sẽ sử dụng một bộ đệm đôi, do đó trong khi một nửa được enqueued, người kia là nhận được đầy hoặc tiêu thụ bởi ứng dụng của chúng tôi. Điều này cho phép chúng ta tạo ra một giao diện đơn giản mà có thể nhận được một khối âm thanh sẽ được sử dụng để viết thư cho bộ đệm hoặc để nhận được các mẫu từ bộ đệm.

Dưới đây là những gì chúng ta làm cho đầu vào. Việc gọi lại là rất nhỏ, nó chỉ thông báo cho chủ đề xử lý chính của chúng tôi là hàng đợi đệm sẵn sàng:

void bqRecorderCallback(SLAndroidSimpleBufferQueueItf bq, void *context) { 
    OPENSL_STREAM *p = (OPENSL_STREAM *) context;
      notifyThreadLock (p-> inlock); 
}

Trong khi đó, một vòng lặp xử lý sẽ gọi hàm đầu vào âm thanh để có được một khối lượng mẫu. Khi bộ đệm được làm trống, chúng tôi chờ đợi thông báo để enqueue một bộ đệm để được lấp đầy bởi các bộ đệm và thiết bị chuyển mạch:

int android_AudioIn(OPENSL_STREAM *p,float *buffer,int size){ 
    short *inBuffer;
     int i, bufsamps = p-> inBufSamples, index = p-> currentInputIndex; 
  if (p == NULL || bufsamps == 0) return 0; 

  inBuffer = p-> inputBuffer [p-> currentInputBuffer]; 
  for (i = 0; i <size ; i ++) { 
    if (index> = bufsamps) { 
      waitThreadLock (p-> inlock); 
      (* p-> recorderBufferQueue) -> Enqueue (p-> recorderBufferQueue, 
                     inBuffer, bufsamps * sizeof (short)); 
      p-> currentInputBuffer = (p-> currentInputBuffer 0: 1); 
      index = 0; 
      inBuffer = p-> inputBuffer [p-> currentInputBuffer]; 
    }
    buffer[i] = (float) inBuffer[index++]*CONVMYFLT;
}
p->currentInputIndex = index;
if(p->outchannels == 0) p->time += (double) size/(p->sr*p->inchannels);
return i;
}

Đối với đầu ra, chúng ta làm ngược lại. Việc gọi lại là giống hệt nhau, nhưng bây giờ nó thông báo rằng thiết bị đã tiêu thụ đệm của chúng tôi. Vì vậy, trong vòng lặp xử lý, chúng ta gọi chức năng này mà lấp đầy bộ đệm đầu ra với các khối, chúng tôi vượt qua nó. Khi bộ đệm đầy, chúng tôi chờ đợi thông báo để chúng tôi có thể enqueue các dữ liệu và bộ đệm chuyển đổi:

int android_AudioOut (OPENSL_STREAM * p, float * buffer, int size) { 

short * outBuffer, * inBuffer; 
int i, bufsamps = p-> outBufSamples, index = p-> currentOutputIndex; 
if (p == NULL || bufsamps == 0) return 0; 
outBuffer = p-> outputBuffer [p-> currentOutputBuffer]; 

for (i = 0; i <size; i ++) { 
outBuffer [index ++] = (short) (buffer [i] * CONV16BIT); 
if (index> = p-> outBufSamples) { 
waitThreadLock (p-> outlock); 
(* p-> bqPlayerBufferQueue) -> Enqueue (p-> bqPlayerBufferQueue, 
outBuffer, bufsamps * sizeof (short)); 
p-> currentOutputBuffer = (p-> currentOutputBuffer 0: 1); 
index = 0; 
outBuffer = p-> outputBuffer [p-> currentOutputBuffer]; 
} 
} 
P-> currentOutputIndex = index; 
p-> thời gian + = (double) size / (p-> sr * p-> outchannels); 
return i;
}
  1. Giao diện

Mã thảo luận ở trên được cấu trúc thành một API tối thiểu cho streaming audio với OpenSL. Nó có năm chức năng (và một cấu trúc dữ liệu mờ đục):

/*
  Open the audio device with a given sampling rate (sr), input and
  output channels and IO buffer size in frames.
  Returns a handle to the OpenSL stream
*/
OPENSL_STREAM* android_OpenAudioDevice(int sr, int inchannels,
                                int outchannels, int bufferframes);
/*
Close the audio device
*/
void android_CloseAudioDevice(OPENSL_STREAM *p);
/*
Read a buffer from the OpenSL stream *p, of size samples.
Returns the number of samples read.
*/
int android_AudioIn(OPENSL_STREAM *p, float *buffer,int size);
/*
Write a buffer to the OpenSL stream *p, of size samples.
Returns the number of samples written.
*/
int android_AudioOut(OPENSL_STREAM *p, float *buffer,int size);
/*
Get the current IO block time in seconds
*/
double android_GetTimestamp(OPENSL_STREAM *p)
  1. Processing Các ví dụ được hoàn thành bởi một chức năng xử lý tầm thường, start_processing (), mà chúng tôi sẽ quấn trong Java để nó có thể được gọi bởi ứng dụng. Nó sử dụng các API được mô tả ở trên:
p = android_OpenAudioDevice(SR,1,2,BUFFERFRAMES);
...
while(on) {
   samps = android_AudioIn(p,inbuffer,VECSAMPS_MONO);
   for(i = 0, j=0; i < samps; i++, j+=2)
     outbuffer[j] = outbuffer[j+1] = inbuffer[i];
   android_AudioOut(p,outbuffer,VECSAMPS_STEREO);
  }
android_CloseAudioDevice(p);

Một chức năng stop_processing () cũng được cung cấp, do đó chúng ta có thể ngăn chặn dòng để đóng ứng dụng.

  1. Application code Cuối cùng, hoàn thành dự án, chúng tôi có một lớp Java nhỏ, mà là dựa trên các auto Eclipse tạo ra mã ứng dụng Java, với sự bổ sung của một sợi thứ cấp và kêu gọi hai gói chức năng bản địa được mô tả ở trên:
public class AudiotestActivity extends Activity {
    /** Called when the activity is first created. */
    Thread thread;
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        thread = new Thread() {
            public void run() {
                setPriority(Thread.MAX_PRIORITY);
                opensl_example.start_process();
            }
        };
        thread.start();   
    }
    public void onDestroy(){
        super.onDestroy();
        opensl_example.stop_process();
        try {
            thread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        thread = null;
    }
}

All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí