앱에서 네이버 사전 앱을 호출하는 방법을 찾아보았습니다.

잘 만들어진 앱이라면 intent action 및 intent filter를 AndroidManifest.xml을 통해 추정할 수 있습니다. 자세한 내용은 다음 문서를 참조하세요

http://developer.android.com/guide/components/intents-filters.html

앱에서 AndroidManifest.xml을 열어보려면 aapt dump기능을 통해 간단히 보거나, apktool d 명령을 사용하여 아예 AndroidManifest.xml파일을 추출할 수도 있습니다. 자세한 과정은 생략하고...

그러나 네이버 사전 앱의 경우에는 AndroidManifest.xml를 통해 사전을 간단히 호출할 수 있는 방법을 찾을 수 없었고

AndroidManifest.xml의 일부 정보 및 구글 검색을 통해서 몇가지만 알아내었습니다. (AndroidManifest.xml은 간단히 하기 위해서 일부 정리함)


<activity android:label="@string/app_name" android:name=".WelcomeActivity" android:screenorientation="portrait">
            <intent-filter>
                <data android:scheme="naverdic" android:path="com.nhn.android.naverdic">
                <action android:name="android.intent.action.VIEW">
                <category android:name="android.intent.category.DEFAULT">
                <category android:name="android.intent.category.BROWSABLE">
            </intent-filter>
        </activity>
이것은 URL이 naverdic://com.nhn.android.naverdic일 경우에 해당 activity가 활성화된다는 얘기이며, 다음과 같이 코드를 호출하면 네이버 사전 앱이 호출된다는 것을 말합니다.

            Intent intent = new Intent();
            intent.setAction("android.intent.action.VIEW");
            intent.setData(Uri.parse("naverdic://com.nhn.android.naverdic"));
            startActivity(intent);

일단 위와 같이 넣어보니 네이버 사전 앱이 잘 호출됩니다. 그러나 이것을 원하는 것이 아니지요. 특정 단어를 전달하려면 뭔가 좀 더 알아야 하기때문에, apktool을 이용해 디스어셈블하거나 jad를 통해서 디컴파일을 하는 수밖에 없었습니다. 역시.. 자세한 것은 생략하고,

            Intent intent = new Intent();
            intent.setAction("android.intent.action.VIEW");
            String url = "http://m.endic.naver.com/search.nhn?query=" + query;
            String encoded = Base64.encodeToString(url.getBytes(), 0);
            intent.setData(Uri.parse("naverdic://com.nhn.android.naverdic?launchingPage=commonDict&dictUrl=" + encoded));
            startActivity(intent);

위와 같이 하여 네이버 사전 앱을 호출 할 수 있었습니다

그러면 다음 사전 앱은 어떻게 호출할 수 있을까요? 검색해보면 다음 사전앱의 경우에는 다음의 문서가 있으며 네이버 사전앱보다 훨씬 쉽게 안드로이드 앱에서 호출할 수 있다는 걸 알 수 있습니다. 즉

Intent intent = new Intent();
intent.setAction("android.intent.action.VIEW");
intent.setData(Uri.parse("daummldapp://open?word=" + query));
startActivity(intent);

http://daumdna.tistory.com/780

by dumpcookie 2015. 3. 19. 14:56

Sonic 라이브러리를 사용하여 오디오 재생 속도를 제어하는 방법을 일전에 살펴보았었다. 이번에는 SoundTouch라는 또다른 유명한 라이브러리를 이용하여 재생 속도를 제어하는 방법을 살펴보려고 한다.

SoundTouch여기에서 보는 바와 같이 수많은 어플리케이션에서 사용하고 있다.

Sonic 라이브러리는 소스파일이 단촐하게 단 하나의 메인 소스로 구성되어있는 반면 SoundTouch 소스코드는 이보다 더 많고, 여러가지 추가적인 기능 (WAV 파일 다루기 및 필터 등등으로 구성)이 같이 포함되어 있다.

SoundTouch를 안드로이드용 NDK 라이브러리로 컴파일하는 방법 및 JNI 소스는 https://github.com/svenoaks/SoundTouch-Android 사이트를 통해서 그 정보를 얻을 수 있었으며, 일전에 소개했던 Sonic 라이브러리용 테스트 앱을 살짝 변형시켜서 SoundTouch 라이브러리를 사용한 간단한 재생 속도 조절 앱을 만들 수 있었다. 소스는 https://github.com/wkpark/soundtouch-ndk를 통해서 받을 수 있다. soundtouch는 별도의 레포지터리 https://github.com/wkpark/soundtouch에 최신 soundtouch 소스코드를 올려두었으니 참고하시기 바란다.

Sonic 홈페이지에 설명되어있는 것처럼, Sonic 라이브러리는 PICOLA 알고리즘을 사용하여 음성 속도 제어에 유리한 반면, SoundTouch 라이브러리는 WSOLA 알고리즘을 사용하여서 음성 속도 제어보다는 일반적인 음악의 속도 제어에 유리하다고 한다.

다음은 SoundTouch JNI의 간단한 사용법 예제이다.

.... if (soundFile != null) { soundtouch.setSpeed(speed); soundtouch.setPitch(pitch); soundtouch.setRate(rate); do { try { bytesRead = soundFile.read(samples, 0, samples.length); // WAV 파일을 읽음 } catch (IOException e) { e.printStackTrace(); return; } if (bytesRead > 0) { soundtouch.putBytes(samples, bytesRead); // soundtouch 라이브러리 통해 프로세싱 } else { soundtouch.finish(); // 프로세싱이 완료되면 finish()를 호출 soundtouch.flush(); // 남아있는 버퍼를 지움 } int available = (int) soundtouch.availableBytes(); if (available > 0) { if (modifiedSamples.length < available) { modifiedSamples = new byte[available*2]; // 버퍼 크기가 부족하면 늘려준다. } soundtouch.getBytes(modifiedSamples, available); // 프로세싱된 결과를 가져옴 device.writeSamples(modifiedSamples, available); // AudioTrack에 쓴다. } } while(bytesRead > 0); device.flush(); // 오디오 트랙 끝. ...

Sonic 라이브러리와 함께 SoundTouch는 링드로이드클론 학습기에 기능이 포함되었으며 최신 링드로이드 클론에서 사용해볼 수도 있다.

by dumpcookie 2014. 11. 28. 01:54

링드로이드 클론(영어학습기)에 MP4 동영상의 오디오를 시각화해서 보여주는 기능을 추가해보았다. TED 혹은 VOA의 동영상은 MP4 포맷이며, AAC 오디오가 들어있으므로 안드로이드의 미디어플레이어를 이용해도 별 문제 없이 재생되기때문에, 링드로이드 클론에서 오디오를 파형(waveform)으로 보여주기 기능을 추가하려면 ffmpeg 라이브러리인 libavcode을 사용하는 것이 가장 손쉬운 방식으로 생각하였다. ffmpeg을 안드로이드에서 사용하는 방법에 대한 자료가 꽤 많아서 비교적 손쉽게 이를 적용할 수 있었다.

우선 ffmpeg을 컴파일하는 방법을 검색해보면 그 방식도 꽤 복잡하여 겁이 나게 마련이다. ffmpeg을 컴파일하면 그 라이브러리 전체 크기만해도 10MB를 넘어간다. 라이브러리 크기도 크지만 라이브러리가 방대해서 다른 라이브러리간의 디펜던시 문제가 걸리면 복잡해지지 않을까 하고 지례 겁부터 먹었다. 안드로이드 펍에 수년전 올라온 방법부터 읽기도 전에 머리가 아프다.

그러나, CyanogenMod 트리를 살펴보니 이미 ffmpeg 라이브러리가 external 아래에 들어있는것이 아닌가!! 본인이 ffmpeg 적용을 주저하다가 당장에 코딩을 시작한 주된 요인중 하나이다.

(본인은 안드로이드 개발에서 흔한 이클립스 + 윈도우 빌드환경을 쓰지 않고, 리눅스 콘솔로 거의 대부분의 작업을 한다. 효율성은 떨어지지만 남들이 이미 개발해놓은 것들을 주워먹기 좋은 환경 ^^)

external/ffmpeg 디렉토리에 들어가서 mm명령으로 컴파일을 하니 몇십분 후에 libavutil / libavcodec / libavformat 등등의 ffmpeg 라이브러리들이 빌드가 되었다. (※여기서 주의할 점 한가지는, 빌드하기 전에 -U_FORTIFY_SOURCE 옵션을 CFLAGS에 반드시 넣어주어야 한다는 것이다. ffmpeg/libavutil 디렉토리에는 time.h 헤더가 들어있는데, 이상하게도 gcc의 -isystem 옵션이 말을 듣지 않아서 <time.h>를 인클루드 할 때에 엉뚱한 libavutil/time.h가 인클루드 된다. 이 문제를 회피하기 위해서 bionic/libc/include를 인클루드 경로로 추가하면 컴파일이 잘 되는데 이렇게 bionic의 헤더 경로를 추가하는 경우 GB/ICS와 호환되는 바이너리 파일을 얻기 위해서는 _FORTIFY_SOURCE를 define하지 말아야 한다는 것이다. 또한 추가적으로 GB/ICS에서 문제 없는 바이너리를 얻기 위해서는 LOCAL_SDK_VERSION := 9를 넣어주어야 한다. 이 경우 build/core/* 스크립트를 수정해서 INCLUDE_PATH에서 LOCAL_PATH를 제외시켜야 정상적으로 system의 <time.h>가 인크루드 된다. gcc는 컴파일하고 있는 현재 위치를 include path로 자동으로 지정하므로 $(LOCAL_PATH)를 LOCAL_C_INCLUDE 패스로 추가하고 있는 build/core/* 스크립트를 수정해야 하는 것)

이렇게 ffmpeg 라이브러리가 완성되었으니 가장 기본적인 작업이 완료되었다. 이제는 ffmpeg을 사용하여 오디오를 재생하는 예제를 찾아서 실행하고, 이를 링드로이드에 적용해봐야 할 차례이다. ffmpeg을 이용해서 오디오를 재생하는 방법 자체를 잘 모르므로 예제를 컴파일 및 실행해보면서 ffmpeg의 작동 방식을 이해해야 하는 것이다.

구글로 검색해보니 다음과 같은 자료를 stackoverflow에서 찾을 수 있었으며 이를 개선시킨 예제도 찾을 수 있었다. (아래 링크에 stackoverflow 관련 링크도 같이 있으니 참고하시기 바랍니다)

FFmpeg Audio Playback Sample (http://0xdeafc0de.wordpress.com/2013/12/19/ffmpeg-audio-playback-sample/)

이를 간단히 고쳐서 AO 라이브러리를 사용하여 재생하는 부분을 빼버리고 리눅스에서 컴파일하니 별 문제 없이 컴파일되고 실행되는 것을 확인하였다.

※ 변경된 실행 예제 : 

ffmpeg_audio_decode.cpp

혹은 C소스로 간단히 변경한것:

ffmpeg_audio_decode.c

예제를 보면서 ffmpeg이 어떤 식으로 작동하는지 간단히 알아보고자 한다.


/*
 * from http://0xdeafc0de.wordpress.com/2013/12/19/ffmpeg-audio-playback-sample/
 *
 * g++ -std=c++11 this_example.cpp `pkg-config --libs --cflags libavcodec libavformat libavutil`
 *
 * ./a.out foobar.mp4
 */
#include <iostream>
#include <limits>
#include <stdio.h>

extern "C" {
#include "libavutil/mathematics.h"
#include "libavutil/samplefmt.h"
#include "libavformat/avformat.h"
#include "libavformat/avio.h"
#include "libswscale/swscale.h"
//#include <ao/ao.h>
}

#define DBG(x) std::cout<<x<<std::endl
#define AVCODEC_MAX_AUDIO_FRAME_SIZE 192000

void die(const char *msg) {
    printf("%s\n",msg);
    exit(-1);
}

int main(int argc, char **argv) {
    const char* input_filename = argv[1];

    av_register_all();

    AVFormatContext* container = avformat_alloc_context();
    if (avformat_open_input(&container, input_filename, NULL, NULL) < 0) {
        die("Could not open file");
    }

    if (avformat_find_stream_info(container, NULL) < 0) {
        die("Could not find file info");
    }

    av_dump_format(container, 0, input_filename, false);
...

여기까지가 기본적인 ffmpeg을 사용하기 위한 초기화 과정이다. 마지막 줄 av_dump_format()은 파일의 기본적인 정보를 콘솔로 출력해준다.

그 다음에는 입력파일에 오디오 스트림 정보가 있는지 찾는 부분이다.

...
    int stream_id = -1;
    int i;
    for (i = 0; i < container->nb_streams; i++) {
        if (container->streams[i]->codec->codec_type == AVMEDIA_TYPE_AUDIO) {
            stream_id = i;
            break;
        }
    }
    if (stream_id == -1) {
        die("Could not find Audio Stream");
    }
...

AVMEDIA_TYPE_AUDIO 타입이 입력파일 스트림 (container->streams[])에 있는지 찾아보고, 있으면 stream_id에 그 값을 저장해둔다.

...
    AVCodecContext *ctx = container->streams[stream_id]->codec;
    AVCodec *codec = avcodec_find_decoder(ctx->codec_id);

    if (codec == NULL) {
        die("cannot find codec!");
    }

    if (avcodec_open2(ctx, codec, NULL) < 0) {
        die("Codec cannot be found");
    }
...

그 다음은 코덱 타입에 맞는 디코더를 찾아서 가져오고 디코딩을 시작하게 되는데 다음과 같이 디코딩 작업을 위한 몇가지 메모리 할당 및 초기화가 필요하다.

...
    AVPacket packet;
    av_init_packet(&packet);

    AVFrame *frame = avcodec_alloc_frame();

    int buffer_size = AVCODEC_MAX_AUDIO_FRAME_SIZE + FF_INPUT_BUFFER_PADDING_SIZE;

    // MSVC can't do variable size allocations on stack, ohgodwhy
    uint8_t *buffer = new uint8_t[buffer_size];
    packet.data = buffer;
    packet.size = buffer_size;

    uint8_t *samples = new uint8_t[buffer_size];
    int len;
    int frameFinished = 0;

    int plane_size;

    while (av_read_frame(container, &packet) >= 0) {
....

ffmpeg에서는 AVFrame 및 AVPacket을 할당하고 초기화를 하고 있고, 그 다음에 av_read_frame()을 사용하여 데이터를 packet/frame에 저장하고 있으며, frame/packet에 저장된 데이터가 오디오인 경우에 다음처럼 디코딩 과정을 거치고 frame->extended_data[]에 디코딩된 오디오 샘플이 저장되게 된다.

...
    while (av_read_frame(container, &packet) >= 0) {
        DBG((packet.pos)); // print out bytes offset

        if (packet.stream_index == stream_id) {
            int len = avcodec_decode_audio4(ctx, frame, &frameFinished, &packet);
            int data_size = av_samples_get_buffer_size(&plane_size,
                                ctx->channels,
                                frame->nb_samples,
                                ctx->sample_fmt, 1);
            uint16_t *out = (uint16_t *)samples;

avcodec_decode_audio4()가 성공을 하면 frameFinished에 0이상의 값이 설정이 된다. 이제 디코딩된 오디오 샘플을 적절히 변형하여서 안드로이드의 AudioTrack에 write()해주거나, 샘플을 읽어들여 파형을 출력하는 데이터로 사용할 수 있게 되었다. (이하 코드는 디코딩된 샘플의 포맷을 SHORT 포맷으로 바꿔주는 부분이다.)

by dumpcookie 2014. 11. 20. 14:56