앱이 실행이 안된다는 보고가 있어서 이 문제를 살펴보았더니 인텔 CPU용 안드로이드 x86/x86_64에서 실행되지 않는 문제였다. 리포팅된 로그에 남아있는 오류는 대략 다음과 같았다.

java.lang.UnsatisfiedLinkError: dlopen failed: \
"/data/app-lib/org.dumpcookie.ringdroidclone-1/libffmpeg_jni.so" has unexpected e_machine: 40

예를 들어 아수스에서 나온 트랜스포머 TF103C는 인텔 Atom CPU를 사용하는데 현재 native library가 arm만 지원하고 있기때문에 실행이 안되고 있던 것이었다.

클론 리플레이어를 x86 기기에 사용하려면 mpg123 / sonic / soundtouch / mpg123 / lame 등등의 각종 라이브러리를 x86 용으로 다시 빌드해야 했기때문에 이를 빌드하고 테스트하기 위하여 다음과 같은 삽질성 작업을 하였다.
- 우선 x86/x86_64를 빌드하기 위해서 삽질 시작. -> ndk-build를 사용하려니 여러 라이브러리 의존성때문에 빌드하기 어려운 난점 -> 라이브러리간 의존성 문제는 ndk-build 빌드 스크립트를 수정하는 방식으로 회피하고, 실제로 빌드하면서 발견되는 컴파일 오류를 바탕으로 Android.mk 픽스하여 arm/x86 ABI로 컴파일
- 이와 더불어 AOSP x86 (Android 6.0 브랜치)프로젝트에서 소스를 가져다가 AOSP 전체 빌드하고,
- AOSP x86 환경하여 여러 라이브러리를 빌드하면서 ndk-build를 통해 수정했던 Android.mk 재수정

안드로이드의 빌드시스템을 제대로 이해하고 시작한 작업이 아니였기때문에 꽤 삽질을 했는데, AOSP의 build/core/* 스크립트 및 ndk-build의 ndk/build/core/* 스크립트 분석 및 다음 문서를 통해서 안드로이드 빌드 시스템을 좀 더 이해할 수 있었다.

http://web.guohuiwang.com/technical-notes/androidndk1 - Mastering Android NDK Build System - Part 1
- http://www.netmite.com/android/mydroid/2.0/build/core/build-system.html - Android Build System

특히 위 링크의 마지막 문서를 보면 1997년에 만들어진 Miller의 Recursive Make Considered Harmful 이라는 make에 대한 흥미로운 문서를 만날 수 있다. (2002년 2008년 개정판(?)을 pdf로 볼 수 있다)

FFmpeg

ffmpeg + android로 검색해보면 나오는 다음의 문서를 통해서 automake/autoconf + cross compile 방식을 써서 Android.mk를 만드는 방법을 어느정도 살펴볼 수 있다.

안드로이드 펍의 ffmpeg NDK 컴파일 방법 강좌 시리즈

http://www.androidpub.com/1645684
http://www.androidpub.com/1646144
http://www.androidpub.com/1646529
http://www.androidpub.com/1646540

그러나 이 강좌를 보면 알겠지만, 처음 보는 개발자라면 조금 당황할 수도 있는데, ffmpeg 프로젝트 자체가 고유의 빌드시스템을 가지고 있을 뿐만 아니라 유연성을 제공하기 위한 다양한 configure 옵션이 있으며, 여러가지 외부 라이브러리와 의존성이 있기때문에 컴파일하는 작업조차 상당히 까다로운 작업이다.

하지만 불행중 다행으로(?) 안드로이드용 FFmpeg을 보다 손쉽게 할 수 있는 방법이 있다. 공식 AOSP는 FFmpeg을 포함하지 않고 있으나 CyanogenMod 소스는 FFmpeg을 포함하고 있다. (반면 Android-x86 프로젝트 소스도 FFmpeg을 포함하고 있으나, 이 소스는 CyanogenMod에서 거의 그대로 가져온 소스였고, make파일도 불완전했다.) 그런데 CM13/CM14 소스는 이미 x86을 지원하고 있었지만 YASM에 관련된 수정을 해야 Android-x86 소스트리에서 정상적으로 컴파일이 된다. 즉, AOSP/CM 최신 빌드 스크립트는 이미 YASM을 지원하고 있기 때문에 CM소스에 들어있는 ffmpeg의 x86+YASM관련 fix가 필요 없다. YASM에 관련된 fix를 제거하고 x86_64에 대한 지원을 추가 수정 해주면 x86뿐만 아니라 x86_64 역시 컴파일이 잘 된다.

mpg123

Android용 mpg123는 검색해보면 몇가지를 찾을 수 있지만 대개 최신 mpg123이 아니고 x86도 지원하지 않는다. 기존에 있던 Android.mk + configure + Makefile을 바탕으로 Android.mk를 만들고 AOSP 소스트리에서 컴파일을 할 수 있었다.

일반적인 automake/autoconf 방식은 configure를 통해서 config.h를 생성시켜야 하는데 호스트는 x86_64이고 타겟을 arm용으로 빌드하는 cross compile 방식을 써야 한다. configure를 통한 cross compile은 대개 잘 지원하기는 하지만 이 방식의 각각의 라이브러리마다 약간씩 차이가 있으므로 configure.* 파일과 Makefile.*의 내용을 잘 살펴보면서 Android.mk를 작성해야 한다. 이 부분은 따로 설명하고 https://github.com/wkpark/mpg123에 올려두었다.

ndk-build

위에서 기술한 방식으로 x86_64로 빌드해서 성공하기는 하였지만, 이번에는 최신 스마트폰을 대응하기 위해서 arm64에 대해서 또 다시 빌드하려고 하니 또 AOSP트리를 별도로 컴파일 해도 하나 하는 고민에 빠졌다. 물론 이런 식으로 별도의 CPU마다 AOSP 소스 트리를 따로 마련해도 되겠지만 이렇게 하는 것은 뭔가 낭비인 것 같아서 이번에는 ndk-build를 조금 고치고 AOSP식의 소스를 관리하면 편리할 것 같아 ndk-build 스크립트를 조금 고쳐보았다. (ndk-build의 장점은 무었보다도 APP_ABI := armeabi armeabi-v7a arm64-v8a x86 x86_64 라는 식으로 지정하여 각기 다른 CPU별 바이너리를 보다 쉽게(?) 빌드할 수 있다는 점일 것이다.)

ndk-build 빌드 방식이 AOSP 빌드시스템과 다르게 불편한 점은 바로 외부 라이브러리의 의존성 문제이다. 예를 들어서 FFmpeg을 빌드하기 위해서 외부 라이브러리 lame을 사용하는 경우에 lame 라이브러리에 대해서 import-module 방식을 쓰거나 ($(call import-module, external/lame)과 같은 줄을 Android.mk에 추가해 줌) 혹은 PREBUILT_SHARED_LIBRARY 방식을 써야 한다. 그러나 이는 AOSP식의 빌드시스템에 비교해서 여간 불편한게 아니다. 또한 import-module 방식을 쓰는 경우에는 이미 빌드한 외부 라이브러리가 다시 빌드되는 현상이 있었다(이는 ndk-build의 버그로 보인다. 그밖에, Android.mk를 고칠때마다 다시 빌드되는 현상도 버그로 보였다. NDK_OUT을 동일하게 지정해도 여전히 다시 빌드되는 현상은 마찬가지였다. 소스를 추적해보니 아니나다를까 해당 오브젝트 파일의 디펜던시에 Android.mk 및 Application.mk가 지정되어있었다. 이를 제거하면 Application.mk / Android.mk를 수정해도 재컴파일 되거나 하지 않았다. 만약 재컴파일 해야 하는 경우라면 -B 옵션을 넣어 ndk-build -B 라는 식으로 실행해야 한다.)

import-module을 사용하는 방식이 PREBUILT_SHARED_LIBRARY를 사용하는 것 보다 Android.mk를 덜 수정하는 방식이어서 더 나아보였지만 AOSP 빌드시스템과 비교해보면 너무도 불편한 것 이었다. 즉, AOSP 빌드 시스템의 경우 external 하위에 무수한 외부 라이브러리들의 의존성이 자동으로 판별되며, 이미 빌드된 외부 라이브러리를 그대로 재사용 하게 된다는 점이 ndk-build와 비교해서 가장 큰 차이점이었다.

그리하여 삽질끝에 ndk-build 스크립트의 다음과 같은 내용을 수정하였다.

  1. LOCAL_STATIC_LIBRARIES := liblame 을 넣으면 lame 외부 라이브러리가 이미 빌드된 것이 있는 경우 해당 모듈에 대한 스크립트 로컬 변수를 자동으로 할당하게끔 수정 : ndk-build의 경우 모든 LOCAL_MODULE 및 import 된 모듈에 대해서 __ndk_modules.*라는 내부 변수가 세팅되게 된다. import-module call은 단순히 해당 모듈의 Android.mk를 include하는 방식이였는데, import-module방식을 쓰지 않더라도 __ndk_modules.* 내부 변수가 자동으로 추가되게끔 수정하게끔 하니 import-module 라인을 추가하지 않아도 되었다. 이렇게 수정을 하니 이미 빌드된 외부 라이브러리를 아무런 문제 없이 재사용 할 수 있게 되었다.
  2. 롤리팝 이후에 AOSP 빌드 시스템의 경우 LOCAL_CFLAGS_x86_64, LOCAL_SRC_FILES_x86_64 같은 CPU arch 별 LOCAL 변수를 사용할 수 있다. ndk-build 스크립트도 조금 수정해서 arch별 LOCAL_* 변수를 사용할 수 있도록 하였고 잘 동작 하였다.
  3. AOSP 빌드 시스템의 경우 external/lame 외부 모듈에서 mm 명령을 실행하면 최상위 빌드 디렉토리로 옮겨간 후에 make -C $TOPDIR build/core/main.mk lame 식으로 외부 모듈을 컴파일 하고, 모든 INCLUDE 경로를 TOPDIR 상대 경로로 인식을 하게 된다. 그러나 ndk-build의 경우 make -C $TOPDIR이 아닌 make -C external/module_name 식으로 실행이 되어 모든 LOCAL_C_INCLUDES 경로가 현재 프로젝트 경로에 대한 상대 경로로 인식된다. ndk-build도 AOSP처럼 TOPDIR에 대한 상대경로를 인식하게끔 -C $TOPDIR 방식을 적용하게 스크립트를 고쳐봤는데 큰 문제 없이 잘 빌드 되었다. (단 이 경우 ndk-build 실행 스크립트를 조금 수정해야 했으며, Android.mk 및 Application.mk, NDK_PROJECT_PATH 경로를 직접 지정해 주어야 했다.)

이정도 수정을 하니 ndk-build 시스템을 AOSP 빌드시스템과 마찬가지로 외부 라이브러리 의존성에 대해서 덜 걱정하도록 해 주었고 쓸만해졌다.

이렇게 수정을 한 ndk-build를 통해서 arm64용으로 FFmpeg + lame + 기타 jni 소스등을 빌드해서 테스트하여 잘 실행이 됨을 확인하였다.

(※주의: arm64와 같이 64비트의 경우는 AOSP 롤리팝 이후부터 지원됨)

text relocations 문제

안드로이드 6.0 이후부터는 text relocations 문제가 있는 shared 라이브러리를 사용하는 경우 앱이 실행이 되지 않는다. arm64/x86_64에서는 이 문제가 없거나 해결하기 쉬운 편이나, 유독 x86 아키텍쳐에서는 text relocations 문제를 쉽게 해결하기 어려우며 이러한 문제가 발생하게 된다.

앱이 타겟 API가 22 이하로 빌드된 경우에는 경고만 뿌리고 실행이 정상적으로 되지만, 클론 리플레이어의 경우는 타겟 API가 23이며 text relocations 오류를 내며 앱이 실행조차 되지 않는다. 그래서 원래는 x86 아키텍쳐용으로 FFmpeg/mpg123 등등을 컴파일하고 지원하려고 했지만 text relocations 문제로 인해 x86을 당장 지원하기는 어렵게 되었고 x86_64 및 arm64 지원용 라이브러리를 우선적으로 테스트하게 되었던 것이다.

Text relocation 문제가 있는 라이브러리는 다음과 같았다.

  1. FFmpeg의 libavcodec / libswresample / libswscale(클론 리플레이어는 사용 안함) (libavutil, libavformat은 정상) https://trac.ffmpeg.org/ticket/4928 참고
  2. mpg123
  3. 기타 클론 리플레이어와는 무관하지만 구글링을 해보면 x264 등등의 라이브러리도 text relocation 문제가 있었으며 shared 라이브러리로 빌드시에 text relocation 경고를 보여주었다. (x264의 경우 구 버전은 text relocation 문제가 없었다가 AMD64 + i386 어셈블리어를 통합시키는 과정에서 text relocation 문제가 발생하는 것을 우연히 발견하였다. x264 개발자는 text relocation문제를 수정할 계획이 없다고 밝혔지만 정작 원래는 문제 없던 것이 새롭게 문제가 생겼던 것...) TEXTREL 문제는 readelf -d 명령을 통해 확인할 수 있다. (readelf -d 라이브러파일경로 | grep TEXTREL)
  4. 문제가 생기는 부분은 어셈블러로 최적화된 부분이다. FFmpeg의 경우 컴파일 옵션으로 YASM 어셈블러를 사용하지 않게 처리하면 TEXTRELs 문제가 발생하지 않는다. 다만 이 경우 성능저하가 있게 된다.


by dumpcookie 2017. 3. 1. 19:01

ndk-build는 의존성이 여러개인 외부 라이브러리를 손쉽게(?) 빌드하기 어렵다. 이 경우 ndk-build를 사용하는 대신에 AOSP를 소스 트리를 그대로 가져와서 arm_aosp-eng를 빌드한 후에 external/ 하위에 외부 라이브러리를 빌드하면 여러모로 편리하다. 또한 이렇게 하는 경우에 ndk-build와는 다르게 라이브러리 뿐만 아니라 apk 패키지도 같이 빌드할 수 있는 장점도 있다.

기존에는 AOSP 트리 대신에 CyanogenMod 킷캣 소스트리를 사용하고 있었는데 킷캣의 경우 arm64 및 x86_64를 지원하지 않아서 롤리팝 소스트리를 가져다가 빌드 환경을 재구성 하였다.

(AOSP 마쉬멜로우 트리의 경우 전체 빌드는 문제 없이 잘 되었으나, 이를 사용하려 하니 하위 호환성을 무시하고 과감하게 없애버린 몇몇 라이브러리 등등이 문제가 되어 롤리팝 소스트리를 선택하였다)

아무튼 이러한 이유로 AOSP 트리를 받아서 롤리팝을 다음과 같이 컴파일 하였다.

$ repo init -u https://android.googlesource.com/platform/manifest -b android-5.1.1_r38
$ repo sync
$ . build/envsetup.sh
$ lunch aosp_arm-eng
$ make -j4
...

보통은 이렇게 하면 아무런 문제 없이 전체 빌드가 되는데, 우분투 16.04 환경에서 빌드하다고 못보던 오류가 떴다.

해당 오류를 구글로 검색해보니 대략 다음과 같은 내용을 찾을 수 있었다.

http://stackoverflow.com/questions/36048358/building-android-from-sources-unsupported-reloc-43

https://android-review.googlesource.com/#/c/223100/

롤리팝부터는 clang을 host 바이너리 빌드하는 경우에 사용하게 되는데, 이 경우 clang은 자체적으로 내장된 as를 사용하는 대신에 별도의 gcc 툴체인의 as도 같이 사용하고 있다. 그런데 clang에 -B 옵션으로 툴체인의 경로를 지정하지 않는 경우, 툴체인의 as를 사용하는 대신에 host에 이미 설치된 as를 이용해 컴파일 된다. host의 as가 버전이 낮은 경우에는 별 문제가 되지 않는데 최신의 배포판을 사용하는 경우에는 host의 as 버전이 높고,  gcc 툴체인의 ld는 높은 버전으로 컴파일된 오브젝트의 relocation 타입을 인식하지 못하는 문제가 발생하는 것이다.

우분투 16.04를 사용하는 경우 host의 as 버전 (binutils 2.26.1)

$ as -v
GNU assembler version 2.26.1 (x86_64-linux-gnu) using BFD version (GNU Binutils for Ubuntu) 2.26.1

툴체인의 as 버전 (gcc 4.6 + binutils 2.23.2)

$ prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.11-4.6/bin/x86_64-linux-as -v
GNU assembler version 2.23.2 (x86_64-linux) using BFD version (GNU Binutils) 2.23.2

위와 같이 툴체인의 as 버전이 host의 as 버전보다 낮다.

일단 위 링크에 써있듯이 -B 옵션을 build/core/clang/HOST_clang_common.mk에 추가하고 다시 컴파일 하였다. (stackoverflow 링크에 나와있듯이 롤리팝의 경우에는 다른 옵션을 건드리지 말고 패치를 수동으로 추가)

  -B$($(clang_2nd_arch_prefix)HOST_TOOLCHAIN_FOR_CLANG)/x86_64-linux/bin

주의해야 할 것은 이 경우 이미 컴파일 된 오브젝트 파일을 모두 지워줘야 한다는 점이다. 따라서 out/host/* 파일을 지워주고 make -j4 로 재빌드 한다.

by dumpcookie 2017. 2. 26. 21:25

클론 리플레이어는 FFmpeg 라이브러리 + JNI wrapper를 사용하여 FFmpeg에서 디코딩 가능한 가능한 일부 동영상 및 오디오를 함께 지원하고 있습니다.

FFmpeg의 인코딩도 가능하기때문에 이를 활용할 수 있도록 클론리플레이어 버전 2.89부터는 FFmpeg 인코딩 JNI wrapper를 함께 제공하며, MP3 / FLAC / WAV 등등의 널리 쓰이는 오디오포맷으로 보다 손쉽게 변환이 가능한 기능을 제공하게 되었으며,
버전 2.90부터는 이 기능을 좀 더 확장하여 아래와 같은 모습으로 사용하실 수 있게 되었습니다~

  

현재 오디오로 인코딩 가능할 수 있도록 설정되어있는 포맷은 WMA, M4A, FLAC, WAV 등등입니다. FFmpeg으로 변환 가능한 여러 포맷은 추후에 제공하도록 할 계획입니다~

또한 예전에는 MP4 등등의 동영상 파일의 경우 특정 구간을 잘라서 저장하는 기능을 제공하지 않았으나, 이제는 MP4 등등의 동영상의 특정 구간을 잘라서 MP3/FLAC/WAV 파일로 저장할 수 있게 되었습니다~

(위 스크린샷은 클론 리플레이어 2.90 개발판에서 찍은 것입니다.)


by dumpcookie 2015. 4. 24. 12:20

안드로이드에서는 오디오/비디오/이미지 등등의 미디어 파일을 MediaStore에 미리 등록시키고, MediaStore를 통해서 미디어의 정보를 접근하는 방식을 쓴다.

그런데, 아무래도 대부분의 유저는 폴더식 브라우징에 익숙하기 때문에 그런 것인지 폴더보기를 더 선호한다. 상당수의 음악 앱에서 지원하는 장르별/앨범별/아티스트별 브라우징을 좋아하지 않는 이유는 다름이 아니라 대부분의 오디오파일이 앨범/아티스트/장르 등등의 정보가 제대로 들어있지 않기 때문이다. wav파일에 INFO 메다 정보가 들어있는 경우는 드물고, mp3 ID2/ID3 태그 정보등은 텅텅 비어있는 경우가 많다. 요즘에는 음원 사이트에서 다운로드 받은 파일의 경우는 태그 정보가 잘 들어있지만, CD에 있던 오디오를 MP3로 인코딩 했다거나, 인터넷을 통해 다운받은 어학 자료같은 경우에는 태그 정보가 그다지 도움이 되지 않는 경우가 많다.

아무튼 이러한 이유로, 꽤 인기 있다는 오디오 앱들의 상당수는 폴더식 브라우징을 잘 지원하고 있으며, 유명한 오디오 앱은 거의 예외 없이 폴더식 보기를 지원한다. 예를 들어보면

  1. 제트오디오
  2. Player Pro

일부 앱에서는 약간 다른 폴더식 보기를 지원하는데, 미디어 파일을 포함한 모든 폴더를 한꺼번에 보여준다. 이 경우의 장점은 단 한번의 클릭으로도 해당 폴더의 모든 미디어를 볼 수 있다는 점이지만, 단점은 중복된 이름의 폴더가 있는 경우에 사용자가 혼란스럽다고 느낄 수 있다.

또 어떤 앱에서는 이상하다 싶을 정도로 폴더식 브라우징의 반응 속도가 더딘 경우도 있다.

미디어스토어(MediaStore)에 대해서도 사용자가 폴더식 브라우징의 일관된 경험을 느낄 수 있으면서도 효과적인 방식을 구현하려면 어떻게 해야 할까?

우선 미디어스토어에서 오디오 파일 리스트를 얻어오려면 다음과 같이 해야 한다.

getContentResolver().query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, columns, null, null, null);

여기서 columns에 가지고 오고싶은 Audio.Media의 필드를 넣어주면 된다. EXTERNAL_CONTENT_URI는 여기서 외장 저장장치에 대응하는 MediaStore URI이다.

위의 쿼리는 다음과 같은 식의 SQL 구문에 대응된다고 보면 된다.

SELECT * from audio

SQLite db 파일은 다음 소스에 정의되어 있으며

packages/providers/MediaProvider/src/com/android/providers/media/MediaProvider.java

실제로 저장된 위치는 /data/data/com.android.providers.media/databases/internal.db 혹은 external.db 파일이다. (http://stackoverflow.com/questions/3592497/android-mediastore-sqlite-db-location 참조)

특정 폴더 아래의 모든 오디오 파일 목록 얻기

그러면 특정 폴더에 있는 파일 목록을 얻어오려면 어떻게 해야 할까? 예를 들어 기본 내장 SD에 있는 /sdcard/Download 폴더 하위에 있는 모든 오디오 파일 목록을 가져오려면 다음과 같이 해야 한다.

SELECT * FROM audio WHERE _data LIKE '/storage/emulated/0/Download/%'

즉, /storage/emulated/0/Download로 시작하는 모든 파일 목록을 가져오는 것이다.

그러나 이렇게 하면 Download 폴더 아래의 또 다른 폴더에 들어있는 모든 오디오 파일도 가져오기때문에, 폴더식 보기를 위해서 하위 디렉토리는 제외하고 폴더 바로 아래에 있는 파일 목록을 가져오려면 다음과 같이 조건문을 만들어야 한다.

substr(_data, length('/storage/emulated/0/Download/')+1) NOT LIKE '%/%')

_data 필드(실제 파일 경로)에서 /storage/emulated/0/Download/ 부분을 잘라내고 그 나머지 파일 경로에 / 문자가 포함되지 않은 경우가 바로 해당 경로 바로 아래에 있는 오디오 파일이 된다.

선택된 경로 바로 아래에 있는 오디오 파일 목록을 가져오는 방법을 알았으니 이제는 선택된 경로 아래에 있는 폴더 목록을 가져오는 방법을 알아보자.

특정 폴더 아래의 폴더 목록 얻기

폴더명은 해당 오디오 파일 경로에서 파일 이름 부분을 지우면 된다. 즉 오디오 파일 경로에 해당하는 _data 필드값에 파일 이름값을 저장하고 있는 _display_name 필드값 부분을 잘라내어 지워주는 것이다. SQLite의 replace() 함수를 사용해서 지워준다고 하면

REPLACE(_data, _display_name, '')

물론 이렇게 하면 경로명의 다른 부분을 엉뚱하게 지워버릴 수 있으므로 다음과 같이 해야 의도된 결과를 얻을 수 있다.

SUBSTR(_data, 0, LENGTH(_data) - LENGTH(_display_name))

즉, 전체 경로에서 파일 이름 부분(_display_name)을 그 길이만큼 잘라버리면 그것이 폴더명이 된다.

그러면 다음과 같은 방식으로 오디오 파일을 포함하고 있는 모든 경로명을 가져올 수 있다.

SELECT DISTINCT SUBSTR(_data, 0, LENGTH(_data) - LENGTH(_display_name))

예를 들어서 다음과 같이 /sdcard/ 폴더가 있다고 하자.


sdcard +--Music (mp3 있음)
       +-+DownLoad (mp3 있음)
         +-- song (mp3 있음)
         +-+IU (mp3 없음)
           +-- 1st (mp3 있음)
           +-- 2nd (mp3 있음)

이 경우 다음과 같은 폴더 리스트가 얻어진다.


/storage/emulated/0/Music
/storage/emulated/0/DownLoad
/storage/emulated/0/DownLoad/song
/storage/emulated/0/DownLoad/IU/1st
/storage/emulated/0/DownLoad/IU/2nd

이때 /storage/emulated/0/DownLoad/IU 폴더가 목록에 포함되지 않음에 유의하자.

중간 단계의 모든 폴더를 포함시키려면, 예를 들어 /storage 아래의 모든 폴더 목록을 포함시키려면 getParentFile()을 이용해야 한다. 예를 들자면 /storage/emulated/0/Music 폴더에 대해서 getParentFile()을 이용해 Music, 0, emulated를 모두 얻으려면

List<File> folders = new ArrayList<File>();
File root = new File("/storage");
File dir = new File("/storage/emulated/0/Download");
do {
    if (dir.equals(root))
        break;
    if (folders.contains(dir))
        continue;
    folders.add(dir);
    dir = dir.getParentFile();
} while (dir != null);

(그런데 여기서 가만히 살펴보면, folders.contains(dir) 이 참이 되면 (폴더가 이미 포함되어 있으면) 그 상위 폴더도 모두 포함되어 있는 경우가 된다. 따라서 이 경우 continue 대신에 break를 쓰면 최종적으로 얻어지는 folders에 중복된 항목이 없고 더 간단하게 된다.

최종적으로 얻어지게 되는 모든 폴더 리스트는 다음과 같게 된다.

최상위 폴더를 /storage/라고 했을 경우
/storage/emulated
/storage/emulated/0
/storage/emulated/0/Music
/storage/emulated/0/DownLoad
/storage/emulated/0/DownLoad/song
/storage/emulated/0/DownLoad/IU
/storage/emulated/0/DownLoad/IU/1st
/storage/emulated/0/DownLoad/IU/2nd

이 폴더 목록은 /storage 하위의 모든 폴더 목록이다. 실제로 우리가 화면에 표시해야 할 목록은 선택된 폴더 바로 아래의 폴더 목록이다. 예를 들어 Download 폴더 아래의 하위 폴더는 song, IU 둘 뿐이므로 이 두개의 폴더를 얻어야 하는데, 이것은 dest 파일을 Download 폴더라고 했을 때 dir.getParentFile().equals(dest)인 dir 파일 목록을 찾는 것이다.

폴더목록 + 오디오 파일 목록 합치기

이제 우리가 얻은 폴더 목록과 오디오 파일 목록을 합해서 ListView로 보여주면 된다. 하나는 query()를 통해 얻은 Cursor이고, 다른 하나는 query()를 통해 그 결과값을 재 가공해서 얻은 배열/리스트이다. 이 둘을 ListView로 보여주려고 ArrayListAdapter()를 쓴다면 Cusor를 통해 얻은 목록을 재 가공해서 배열로 만드는 과정이 필요한데 이때 메모리를 별도로 필요로 하게 되는 등의 오버헤드가 더 발생한다. 따라서 이 방법 보다는 CursorAdapter를 써서 오버헤드가 덜 드는 방식이 나을 것이다. CursorAdapter를 쓰려면 배열(혹은 리스트)로 얻은 폴더 목록을 MaxtrixCursor 클래스를 사용해서 Cursor로 재가공해서 MergeCursor()를 사용해서 하나의 Cursor로 만들면 SimpleCursorAdapter를 사용할 수 있게 된다.

이러한 방식을 사용하여 클론 리플레이어의 오디오 보기 기본 리스트뷰를 폴더식 뷰와 함께 제공하게 되었다.


by dumpcookie 2015. 4. 14. 13:07

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

잘 만들어진 앱이라면 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

찍찍이 류의 구간 반복학습 앱을 한번쯤은 써보았을 것이다. 상당수의 앱은 A-B구간 반복만을 지원하며, 사용자가 수동으로 설정을 해야 비로소 선택된 구간이 반복되게 된다.

여기서 한 걸음 더 나아가, 오디오를 자동 분석해서 문장 단위로 자동으로 끊어서 구간반복을 보다 쉽게 지원하는 앱들이 있는데 이를 여기서 소개해보려 한다.

안드로이드에서 아마도 가장 유명한 구간반복 앱은 워크오디오북이다. 국내에서 만들어진 것도 몇가지 있었지만 게중에 유명했던 WaveLoop는 개발이 중단되고, ICS부터 플레이 속도가 조절이 안되는 등등의 몇몇 결함이 보이더니 구글 플레이 배포가 갑자기 중단되었다. (※추가: 검색하다가 우연히 발견하게 된, 2011년 경에 공개된 waveloop소스 https://code.google.com/archive/p/waveloop/)

본인이 생각하는 이상적은 구간 반복학습 앱의 기능은 다음과 같다.
- 음성의 문장단위 자동 인식 및 반복 가능
- 복수개의 문장단위 지원
- 시각적인 파형 보여주기 및 파형의 드래그를 통한 문장 구간의 손쉬운 선택
- 자막 지원 등의 보조적 기능
- 플레이 속도 조절 기능

이러한 기능을 모두 가지고 있는 앱은 현재 안드로이드에 없으며 (2014년 이 글을 쓸 당시), 이 기능중에 일부를 지원하지는 않지만 아마도 가장 잘 만들고 완성도가 높은 앱은 워크오디오북이 아닐까 싶다.
2015년 5월 현재, 이 모든 기능을 잘 지원하는 앱은 필자가 제작자인 클론 리플레이어이다.

워크오디오북
유명한 반복 어학 학습앱 워크오디오북! (2013년 출시됨)
- 플레이 속도를 조절하는 기능은 없다.
- 문장 단위 자동 인식이 매우 훌륭하다.
- 사용자가 문장 구간을 직접 설정할 수 있다. (터치 드래그)
- 자막을 잘 지원한다. (SRT, HTML 자체 포맷 지원, SAMI(smi) 미지원)
- 그밖에, 오디오 북을 받을 수 있는 기능 등등이 최신 버전에 추가됨
- 비교적 최근 유료화가 되어서 설치시간이 일정시간 지나면 유료로 업그레이드하라는 메시지가 뜬다.
- 유료버전으로 업그레이드하지 않으면 하루 10분만 쓸 수 있다.
- 플레이를 10분만 할 수 있다는 제한말고는 나머지 기능은 모두 정상 작동
- 워크오디오북 자체 자막(html) + MP3 오디오북 파일을 앱에서 손쉽게 다운로드 받을 수 있다.

워크오디오북은 파형을 분석하는 속도가 매우 빨라서, 1시간 mp3도 거의 10초 안에 처리해낸다. 놀라운 속도다. (※추가: 이 속도의 비결은 다름이 아니라, ringdroid에서 쓰던 방식과 같이 MP3의 모든 샘플을 읽는 방식이 아니라, 각 프레임에서 global_gains값을 재빠르게 읽어서 보여주던 방식어었다.)

매우 깔끔하게 잘 만들어진 워크오디오북이지만 단점이 없는 것은 아니다. 우선 배속재생을 아직 지원하지 않는다. 아마도 내부적으로 안드로이드의 기본 내장 미디어플레이어를 통해 재생하는 것으로 보이며, 안드로이드 6.0 이하 버전은 기본 플레이어가 배속재생을 지원하지 않은다는 점 때문에 배속 재생 기능이 빠진 것으로 보인다. 또한 mp3 파일만 지원한다. 요즈음 쉽게 접할 수 있는 ogg 등등을 지원하지 않는다. (다만 이는 큰 문제로 보이지는 않는다.) 또한, mp4와 같은 동영상의 오디오 재생 역시 지원하지 않는다.
기타, 저자가 함께 지원하고 있는 오디오북으 호환성 문제가 있다. 오디오북에 포함된 자막은 html이라는 독특한 형식인데, 이것은 html 파일이라서 일반 브라우져로 볼 수도 있는 등의 장점이 있으나, 이 자막을 다른 어플이 지원하지는 않는다. (클론 리플레이어는 이를 지원하고 있다.) 그밖에 함께 제공하고 있는 오디오북에 포함된 원본 mp3 파일은 유명한 리브리복스.org 사이트에서 제공하는 음원인데, 발음이 상당히 빠른 편이라 초급 영어 학습자에게는 그다지 적합하지는 않아 보인다. (물론 AP 뉴스 혹은 CNN 뉴스 등등보다는 쉬운 편)


클론 리플레이어 (영어 구간 반복 듣기 베타)

본인이 개발자이며, 기존에 나온 앱들이 불편하다고 느껴서 개발하기 시작하였다. :)
공개된 오픈소스 링드로이드 소스를 사용하여서 링드로이드의 장점을 십분 활용하였다.
(개인적으로 이름붙인 프로젝트명 내부 명칭은 링드로이드 프로그램을 기반으로 한 것에 착안하여 링드로이드클론이라고 이름 붙였고, 앱 이름은 별 특징없이 "어학반복학습기"라고 하였고, 현재 정식 명칭은 클론 리플레이어 이다.)
- 플레이 속도를 조절 0.5~2배속 (버전 2.52부터) 
- 문장 단위 자동 인식이 그럭저럭 쓸만함 (high pass 필터를 추가한 이후에는 많이 좋아졌다)
- 자막을 지원한다 (SAMI지원 외 공개 자막 라이브러리를 사용하여서, SRT/SCC/LRC등등의 여러 포맷의 자막 지원)
- MP4 파형 보기 지원 (오디오 디코딩에 ffmpeg 사용함. 버전 2.50부터. MP4의 경우는 영상 보기도 지원 가능)
- 사용자가 문장의 구간을 직접 조절 할 수 있다.
- UI가 간단하여 사용이 손쉬운 편이다.
- 오디오 파일의 특정 구간을 잘라서 파일로 손쉽게 저장 가능(링드로이드의 기능 + FFmpeg 인코딩 동시 지원)
- 워크오디오북에서 받은 자막 + MP3 재생을 잘 지원.
- 개발이 활발. 몇일에 한번씩 새로운 버전이 나오고 있다 :)
- 아직 베타버전 상태라서 일부 기능이 안정적이지 않다. (2015년 3월 이후로 상당히 안정적이 되었다)

워크오디오북은 파형 분석속도가 타의 추종을 불허한다. 1시간 이상 분량의 MP3도 파형 분석 속도가 10초 수준이다. 워크오디오북에서는 파형을 모두 읽는 방식이 아니라 MP3의 각 프레임의 global_gain값만 읽어 보여주는 방식이다. 또한, 모든 음성 구간을 분석하는 것이 아니라, 보이는 부분의 구간만 재빠르게 분석하 매우 빠르다. 반면 본인이 개발하고 있는 클론리플레이어는 MP3 공개 라이브러리중에 가장 빠르다는 libmpg123을 사용하는데도 1시간짜리 mp3 분석이 1분 가까이 소요된다. 최초 시동시에 작동 반응 속도를 빠르게 하기 위해서 2015년 5월(?) 버전부터 mp3의 앞부분 ~2분 분량 정도를 무조건 읽고나서 즉각적으로 화면으로 보여준 후에, 나머지 파일은 백그라운드로 읽고, 모든 것이 완료되면 다시 나머지 분량의 파형을 갱신해서 화면에 표시하도록 하였으며,이러한 방식으로 로딩시간으로 인해 기다리게되는 불편함을 어느정도 해소시켰다.

클론 리플레이어의 그밖에 특징

자막을 리스트로 보기 - 일반적으로 자막 한줄만 나오는 방식 대신에 자막을 모두 볼 수 있도록 하였다.
- 문장 단위 자동 인식은 서서히 개선중 - Voice segmentation에 관련된 자료/논문을 읽어보며 구현중이다.
- 현재 RMS체크 / High-pass 필터 / Log 에너지 체크 적용됨.
- 간단하고 자연스러운 UI - 아이콘 최소화. 쉬운 사용에 중점을 두었으며, Holo 스타일과 어울리게 만듦
  - 최신 버전의 경우에는 최신 안드로이드 버전의 사용자는 Material 스타일과 어울리게 하였다.
- 원래 링드로이드 앱이 가지고 있던 파형 터치 드래그의 자연스러운 애니메이션을 그대로 유지/개선
- 왠만한 자막 편집기보다 나은 간단한 자막 편집기능을 지원한다. (SRT 및 SAMI 포맷으로 저장 가능)
- 최신 버전의 경우 대본 파일도 지원한다. 즉, 자막은 아니지만 일반 텍스트 파일을 같이 볼 수 있도록 하였다.
- 대본 파일을 직접 편집하는 것도 지원한다. 따라서 사용자가 메모를 함께 적어 넣을 수도 있다.
- 2015년 4월 이후로는 내장된 웹사전+단어장을 함께 지원한다.
- 구간 반복 횟수 지정 가능 (구간 반복이 완료되면 다음 구간으로 이동)
- 폴더플레이 지원 (폴더 아래의 모든 오디오 재생) 및 셔플 및 전체 오디오 반복 회수 지원
- 포드캐스트 다운로드 지원 및 자막/스크립트 다운로드 지원. (VOA / TED / Storynory 등등)
- 최신 버전의 경우 유튜브의 특정 채널을 통해서 영상을 다운받아 볼 수 있다.
- 영상을 재생하는 경우에는 영상을 함께 보는 것도 지원한다.
- 최신 안드로이드 6.0을 사용하는 경우에는 영상도 배속 보기를 지원한다. 따라서 영어 학습뿐만 아니라 다운받을 수 있는 인터넷 강으로 빠른 속도로 재생하는 것도 가능해졌다.
- 최신 버전의 경우, 기존의 여러 A-B 구간 지정앱에서 사용하던 전통적인 방식의 AB 구간 지정도 함께 지원하게 되었다.
- ※추가: 최신 버전의 경우 구글 ExoPlayer를 사용한 보다 정확한 동영상 구간 반복을 지원한다.


by dumpcookie 2014. 11. 2. 20:04

음성파일의 재생속도를 조절하기 위해서 검색을 해보니 그중에 가장 간단한 방식을 sonic 라이브러리(LGPL)가 지원하고 있었다. sonic-ndk라는 안드로이드에 직접 적용한 간단한 앱 소스도 있었다.

https://github.com/waywardgeek/sonic-ndk

이게 약간 낡아서 최신 빌드환경에서 빌드가 제대로 안되길래 몇가지 메시지를 추가해 주었으며 변경된 소스는 다음에서 받을 수 있다.

https://github.com/wkpark/sonic-ndk

Sonic라이브러리에서 쓰는 재생속도 조절 방식은 음성에 대해서 적합하다고 하며 빠른 재생속도에도 아주 자연스러운 목소리를 들을 수 있는 장점이 있었다. (자세한 내용은 다음 링크를 참조하세요)

http://dev.vinux-project.org/sonic/

소스를 보면 매우 간단한 방식으로 처리하고 있으며, 다음과 같은 방식으로 사용된다.

...
Sonic sonic = new Sonic(sampleRate, nChannels);
// sampleRate는 오디오 파일로부터, 채널도 오디오파일로부터

byte samples[] = new byte[4096];
byte modifiedSamples[] = new byte[2048];

sonic.setSpeed(speed); // 재생속도 설정
do {
    try {
        bytesRead = soundFile.read(samples, 0, samples.length); // WAV파일로부터 읽음
    } catch (IOException e) {
        e.printStackTrace();
        return;
    }
    if (bytesRead > 0) {
        sonic.putBytes(samples, bytesRead); // sonic라이브러리로 변형.
    } else {
        sonic.flush();
    }
    int available = sonic.availableBytes();

    if (available > 0) {
        if(modifiedSamples.length < available) {
            modifiedSamples = new byte[available*2];
        }

        sonic.receiveBytes(modifiedSamples, available);
        track.write(modifiedSamples, 0, available); // AudioTrack에 쓴다.
    }
} while (bytesRead > 0);
track.flush();

...

추가: 참고로 sonic 라이브러리는 안드로이드 M 버전부터 정식으로 채용되어 안드로이드에서도 사용할 수 있게 되었고 이를 이용한 재생 속도 조절이 안드로이드 자체적으로도 가능하게 되었다.

추가된 sonic 소스: https://android.googlesource.com/platform/external/sonic/

by dumpcookie 2014. 10. 2. 23:17

JLayer가 상당히 느린 관계로 libmpg123을 적용해보기로 하였습니다.

MAD에는 저수준의 API가 있어서 ringdroid에 입맞에 맞춰서 적용하는게 손쉬웠지만 libmpg123의 경우에는 저수준의 API함수를 사용하는 예제가 없어서 소스를 훝어보던 차에 *framebyframe*류의 저수준 API함수가 비교적 최근(2009년)에 추가되어 있더군요. 이를 이용하여 libmpg123을 이용한 decoder를 만들어서 넣었고 잘 작둥함을 확인하였습니다. 관련 내용을 검색해봐도 저수준 API 함수를 사용하는 예제는 찾을 수 없었습니다만, 사용방법은 간단하였고 어렵지 않게 적용할 수 있었습니다.

소스는 https://github.com/wkpark/ringdroid master 브렌치에 올려두었습니다~

libmpg123을 사용하는 Mpg123Decoder.c JNI wrapper는 https://github.com/thasmin/android-mp3decoders를 참고하였고, 골격은 MAD에서 사용하던 방식과 거의 같고, readNextFrame() / decodeFrame()등의 저수준 API를 사용하는 부분 등등은 재작성 했습니다.


/* JNI 일부 */ JNIEXPORT float JNICALL Java_com_ringdroid_soundfile_Mpg123Decoder_readNextFrame (JNIEnv *env, jclass c, jlong handle) { MP3File *mp3 = (MP3File*)handle; int err = mpg123_framebyframe_next(mp3->handle); char buf[256]; sprintf(buf, "readNextFrame() err = %d", err); __android_log_write(ANDROID_LOG_INFO, "mp3decode-jni", buf); return err; } JNIEXPORT float JNICALL Java_com_ringdroid_soundfile_Mpg123Decoder_decodeFrame (JNIEnv *env, jclass c, jlong handle) { MP3File *mp3 = (MP3File*)handle; size_t bytes = 0; unsigned char *dummy; int err = mpg123_framebyframe_decode(mp3->handle, NULL, &dummy, &bytes); char buf[256]; sprintf(buf, "decodeFrame() bytes = %d", bytes); __android_log_write(ANDROID_LOG_INFO, "mp3decode-jni", buf); if (err != MPG123_OK) __android_log_write(ANDROID_LOG_INFO, "mp3decode-jni", mpg123_plain_strerror(err)); mp3->leftSamples = bytes / 2; /* buffer는 단지 저장장소일 뿐 실제로 short로 저장되므로 (short *)로 cast해야 하고 실제 데이터 개수는 bytes 수의 1/2 */ mp3->offset = 0; sprintf(buf, "decodeFrame() err = %d", err); __android_log_write(ANDROID_LOG_INFO, "mp3decode-jni", buf); return err != MPG123_OK ? 0 : bytes; } ...

(framebyframe* 저수준 API는 위에서 볼 수 있듯이 매우 간단합니다)

by dumpcookie 2014. 9. 15. 17:44
| 1 2 |