리그베다위키의 전신 엔하위키를 본인이 처음 봤을때가 아마도 2008년정도 였을것이다. 그때는 "아... 이런 희안한 사이트도 있구나"라는 정도로 스쳐지나갔다가, 한 2년 후에 다시 보게 되었을때는 거의 ~8만여 페이지 규모의 국내 최대 모니위키 사이트가 되어있었다. 규모에도 놀랐지만 너무 너무 느려서 더 놀랬던 것으로 기억한다. 검색은 당연히 먹통이고, 반응 속도가 너무 느려서 이대로 두면 안되겠다 싶어서 위키 게시판에 글을 올렸고, 병목현상을 일으키던 부분을 찾아내어 수정하고 모니위키 1.1.5 개발자판으로 판올림을 했던 것으로 기억한다.(2010년 10월) 그 당시 엔하위키는 모니위키 렌더러에 자잘한 수정을 하고 (버그 수정 포함), 일부 사용자 관리/필터링 등등의 일부 기능을 추가 했을 뿐이라서, 엔하위키측에서 제공한 모니위키 변경분에 대해서 선별적으로 적용하고 추가적으로 여러 수정을 해서 모니위키 1.1.5 버전이 나왔고, 엔하위키 관리/운영진은 이를 바탕으로 어렵지 않게 판올림을 하였고 정상적인 속도도 나오게 되었다. 그 뒤에 엔하위키 사용자의 피드백을 받아서 모니위키 1.1.6CVS 개발판이 나왔다.


https://kldp.org/node/118619 참조

1.1.5 판올림 당시 구 엔하위키게시판에 올라온 짤방



그러다가 다시 15만~20만 페이지 규모가 되었던 시점이었을때에 또 다시 엔하위키는 느려져 있었고, 또 다시 병목현상의 원인을 찾아 수정하고 모니위키 1.2.0 판올림도 같이 병행하였다. (2013년 8월)


당시 병목 현상의 원인은?

2010년 당시 5만여 페이지 규모였을때에, 병목현상의 원인은 사실 별거 없었다. 매크로중에 페이지 개수를 카운팅하는 기능이 너무 느렸던 것이었다. 엔하위키의 경우 페이지 개수 표시가 오른쪽 사이드바에 항상 표시가 되었다. 그런데 카운팅 매크로는 5만여 페이지의 목록을 모두 가져오고 그것의 개수를 매번 세었던 것이였다. 그래서 이 부분에 대해서 페이지 개수를 보다 효율적으로 세도록 고치고, 페이지 개수 정보를 캐싱하게 만들었다. 그밖에 여러 개선과 수정이 물론 있었지만 이 변경이 구 엔하위키의 속도를 원상복구 시키는 주요한 고침이였다. 그밖에 5만여 페이지 규모에서도 문제없도록 간단한 n-gram 인덱서를 추가해서 5만여 페이지 규모의 위키에서도 본문 검색을 할 수 있도록 만들었다.


2013년 8월의 고침도 사실 따지고 보면 대동소이했다. 페이지 개수 세는 부분을 보다 영리하게 만들고, 전체 페이지 목록을 매번 가져올 것이 아니라 주기적으로 업데이트 하고, 페이지 삭제/추가시 페이지 개수를 부분 업데이트 하도록 고쳤다. 물론 1.2.0 판올림의 경우에는 모니위키의 캐싱 방법을 더 뜯어고쳐서 대규모 위키에 문제 없도록 하는 등등의 여러 고침도 함께 포함하고 있었다.


DBMS vs 파일 시스템

많은 사람들이 말하기를 모니위키가 DBMS 기반이 아니라서 대규모 위키 대응이 불가능할 것이라 하지만, 사실 위키의 경우에는 flat한 key-value 구조라서 DBMS보다는 NoSQL이 더 유리하고, key-value구조는 파일 시스템 친화적이다. git/cvs/rcs같은 훌륭한 버전컨트롤 시스템이 모두 파일 기반이며 이를 그대로 써먹을 수 있다. 특히 RCS의 경우는 각 페이지별로 버전 히스토리가 별도로 저장되므로 위키엔진의 버전 컨트롤용으로 적합하다. 미디어위키처럼 DBMS로 구현하는 경우에는 버전 컨트롤을 다시 구현해야 한다. (또한 미디어위키는 여러 이미지 자료들을 DBMS로 저장하거나 하지는 않으며 이미지 자료는 파일시스템에 저장하는 방식을 여전히 쓰고 있다.) 그러나 모니위키의 경우처럼 파일기반인 경우에는 버전컨트롤을 RCS와 같은 외부프로그램이 별도로 담당하고 있어서 따로 구현할 필요가 없다. 위키엔진은 페이지 이름에 해당하는 페이지의 최종 버전을 가져와서 렌더링해서 뿌려주면 그만이다. 페이지 이름이 고유한 key-value구조라서 NoSQL에 대응하기 어렵지 않다. 위키 규모가 100만페이지 이상이 되더라고 대응이 그리 어려운 일이 아니라는 얘기다. 리눅스의 경우 파일시스템의 성능이 비약적으로 좋아졌다는 사실도 한몫 하고 있다. 버전 정보는 파일시스템으로 저장하고, 프론트엔드 단에서는 NoSQL을 써서 최종 버전을 저장하고, 검색은 elastic 검색 엔진을 쓰면된다. (모니위키는 이미 개발판에 elastic 검색 엔진을 실험적으로 넣었다. 가까운 미래에 NoSQL 백엔드를 만들게 될지도 모르겠다)


미디어위키는 훌륭한 위키엔진이다. 그러나 이것을 개인사용자가 사용하기 적합할까? 미디어위키를 처음 설치해보고는 그 속도가 생각보다 느리다는 사실에 놀랬었다. 미디어위키의 경우 자신의 위키 규모에 맞춰서 적절하게 설정을 해주어야 한다. (미디어위키의 속도를 향상시키는 법은 다른 글 #1 #2 참조.) 도쿠위키도 아주 좋은 위키엔진임에 분명하다. 파일기반이라서 단촐하고 개인사용자에게 적합하다. 그런데 본인의 테스트에 의하면 5천 페이지 정도만 되어도 쉽게 대응할 방법을 찾지 못하였다. 물론 개인위키 사용자라면 1천 페이지 만들기도 어렵기때문에 사용하는데 지장이 없을 것이다.


엔하 미러의 속도와 비교

종종 리그베다 본관의 속도와 엔하미러의 속도가 비교대상이 되고는 하였다. 사실 엔하미러의 속도는 상상을 초월할 정도로 빠르다. 엔하미러가 얼마나 빠르냐 하면, 로컬에서 아파치 벤치로 속도를 측정한다고 했을때에 거의 static html 페이지 속도급으로 빠르다. 이정도로 빠르기때문에 사실 엔하미러 속도를 다른 위키엔진과 비교하는 것은 무의미할 수 있다. 모니위키의 속도는 여러 최적화 옵션을 키면 엔하미러의 속도의 1/10에 해당한다 보면 된다. 사양에 따라 다르겠지만 로컬에서 static html 속도가 ~2000RPS 정도라고 한다면 모니위키의 경우에는 ~200RPS이다. (만약 모니위키를 아파치서버 + varnish 캐싱 서버와 함께 사용하면 ~7000RPS 정도로 올라가게 된다.)


그렇다면 미디어위키의 속도는 어떨까? 기본 설정으로 설치가 끝난 상태에서는 서버사양에 따라 달라지겠지만 위에서 언급한 것처럼 static html이 ~2000RPS정도 나온다고 했을때에 미디어위키는 ~5RPS도 안된다. 최적화 옵션을 켜면 ~10~15RPS 수준으로 올라간다. (이 경우도 아파치 + varnish캐시서버를 사용하면 비약적으로 빨라지게 되는데 자세한 내용은 미디어위키의 문서를 참고하라)


마치며

지금은 상당히 좋은 위키 엔진이 많이 있기때문에 모니위키의 인기가 많이 사라진 편이지만, 모니위키가 최초 나왔을 무렵 노스모크의 위키 엔진이었던 python으로 구현된 모인모인의 클론을 표방하고, 웹 호스팅에 좀 더 유리했던 PHP로 새롭게 작성했었다. 모니위키는 기존의 모인모인을 금방 대신하여 자리를 차지하게 되었고, 한동안 모니위키는 개인위키 사용자들의 대세였던 적도 있었다. 뿐만 아니라 지인의 증언(?)에 의하면 모니위키는 많은 회사 인트라넷에서도 사용되어졌고 처음 위키위키를 사용하는 사용자들에게 영향을 끼쳤다.


모니위키는 여전히 개인위키를 지향하며, 코어 코드를 좀더 줄이려고 노력하고 있다. 그러면서도 리그베다위키의 규모에 대응하기 위해서 확장이 보다 손쉽도록 개선하고 있으며, 최근에는 마크다운의 문법과 거의 유사한 것에 착안하여 마크다운 문법을 섞어서 쓰더라도 문제가 없도록 MixDown 파서를 개발하고 테스트하였고 한편으로는 기존 모니위키 문법 파서를 재작성 및 테스트 하였다.


개인 사정으로 2014년 거의 개발 중단 상태였다가 다시 모니위키 소스를 건드리고 있는데, 모니위키가 앞으로 어떠한 방향으로 개발되게 될지는 나도 장담하지 못하겠지만, 리그베다위키가 사라지지 않는한, 일부 개인 사용자의 피드백이 지속되고 있는 이상 계속 개발하게 되지 않을까 생각한다.


다음에는 리그베다위키와 그 클론들에 대한 이야기와 전망에 대해 써보려 한다.

by dumpcookie 2015. 4. 28. 14:44

최근 리그베타위키 발 모니위키 보안 문제가 이슈였습니다. 2003년에 처음 만들어져서 벌써 13년째 장수(?) 자유소프트웨어 프로젝트인 모니위키에서 이정도 심각했던 보안 이슈는 지금까지 없었습니다. 그런데 이번에 발견된 보안문제는 문제가 심각할뿐만 아니라 PHP 관련 보안문제중에 가장 기초적인 부분중에 한가지라서 그 충격이 컸던 것도 사실입니다.

그러면 어찌해서 이런 기초적인 보안오류가 최초 모니위키가 릴리스된 2003년 이후 10년이 지난 시점에서야 발견된 것일까요?





이번에 발견된 보안 문제는 모니위키를 사용하기위해 필수적으로 필요한 RCS 버전관리 프로그램을 popen()으로 호출하기 위해 만들어진 RCS 버전관리 모듈에서 발견되었습니다. 최초로 발견하신 분은 강성훈님이며, 이 이슈는 2013년에 만들어진 모니위키 1.2.0 버전부터 1.2.3 버전까지 영향을 받습니다. 모니위키 1.2.0 버전 이전에는 다음과 같았기때문에 쉘코드 인젝션이 불가합니다.

wiki.php의 savePage() method에서 RCS 모듈 호출 if (strlen($comment)>$this->anonymous_log_maxlen) $comment=''; // restrict comment le $REMOTE_ADDR=$_SERVER['REMOTE_ADDR']; $comment=escapeshellcmd($comment); // 이 부분때문에 문제가 없음 $myid=$user->id; if (!empty($user->info['nick'])) {


그러던 것이 모니위키 1.2.0부터 다음과 같이 바뀌었습니다.

--- a/lib/version.RCS.php
+++ b/lib/version.RCS.php
@@ -63,7 +63,9 @@ class Version_RCS {
       _mkdir_p($dir.'/RCS', 2777);
       umask($om);
     }
-    $fp=@popen("ci -l -x,v/ -q -t-\"".$key."\" -m\"".$log."\" ".$key.$this->NULL,"r");
+    // $log = escapeshellarg($log); // win32 not work
+    $log = '"'.preg_replace('/([\\\"])/', "\\\\\\1", $log).'"';
+    $fp = @popen("ci -l -x,v/ -q -t-\"".$key."\" -m".$log." ".$key.$this->NULL,"r");
     if (is_resource($fp)) pclose($fp);
   }

diff --git a/wiki.php b/wiki.php
index 536b2a1..43826be 100644
--- a/wiki.php
+++ b/wiki.php
@@ -1020,7 +1020,6 @@ class WikiDB {
       if (strlen($comment)>$this->anonymous_log_maxlen) $comment=''; // restrict comment le

     $REMOTE_ADDR=$_SERVER['REMOTE_ADDR'];
-    $comment=escapeshellcmd($comment);

     $myid=$user->id;
     if (!empty($user->info['nick'])) {

잘 보시면 escapeshellcmd()가 제거되었으나 RCS모듈에서는 win32 문제때문에 escaheshellarg()를 사용하지 않고, 어찌된 영문인지 쌍따옴표만 escape하는 실수가 보입니다. (커멘트에도 나와있듯이)

그리고 곧 바로 다음과 같이 수정하여 win32에서 문제점을 수정했으나 escapeshellarg()를 다시 적용하지 않는 실수를 저지르고 있습니다.

--- a/lib/version.RCS.php
+++ b/lib/version.RCS.php
@@ -63,10 +63,27 @@ class Version_RCS {
       _mkdir_p($dir.'/RCS', 2777);
       umask($om);
     }
-    // $log = escapeshellarg($log); // win32 not work
-    $log = '"'.preg_replace('/([\\\"])/', "\\\\\\1", $log).'"';
-    $fp = @popen("ci -l -x,v/ -q -t-\"".$key."\" -m".$log." ".$key.$this->NULL,"r");
+    $mlog = '';
+    $plog = '';
+    if (getenv('OS') == 'Windows_NT' and isset($log[0])) {
+      // win32 cmd.exe arguments do not accept UTF-8 charset correctly.
+      // just use the stdin commit msg method instead of using -m"log" argument.
+      $logfile = tempnam($this->DB->vartmp_dir, 'COMMIT_LOG');
+      $fp = fopen($logfile, 'w');
+      if (is_resource($fp)) {
+        fwrite($fp, $log);
+        fclose($fp);
+        $plog = ' < '.$logfile;
+      }
+    }
+    if (empty($plog)) {
+      // $log = escapeshellarg($log); // win32 does not work correctly
+      $log = '"'.preg_replace('/([\\\"])/', "\\\\\\1", $log).'"'; // 이미 win32 문제가 해결되었으므로 이 줄을 지우고 윗 줄을 써야 했음
+      $mlog = ' -m'.$log;
+    }
+    $fp = @popen("ci -l -x,v/ -q -t-\"".$key."\" ".$mlog." ".$key.$plog.$this->NULL,"r");
     if (is_resource($fp)) pclose($fp);
+    if (isset($plog[0])) unlink($logfile);
   }

   function rlog($pagename,$rev='',$opt='',$oldopt='') {

이렇게 해서 2013년 버전 1.2.0부터 1.2.3버전까지 보안 이슈가 영향을 받게 된 것이고, 리그베타위키발 보안이슈는 2015년에 비로소 터지게 됩니다. 





개인위키 사용자는 안전한가?

그러면 모니위키를 사용하는 대다수 개인위키 사용자들에게 미치는 영향은 어느정도 일까요?


모니위키는 개발 초기부터 개인위키를 지향하였으며 이에 대응하기 위해 개인 사용자들을 위한 보안모듈을 손쉽게 만들 수 있게 하였고, 모니위키를 설치한 개인 사용자들은 대게 자신만이 편집하고 사용할 수 있도록 모니위키를 설정하였습니다. 예를 들어 모니위키를 최초 설치하면 비로그인 사용자를 포함한 모든 사람들이 편집할 수 있도록 설정이 되지만, 개인 사용자들은 다음과 같이 로그인을 한 사용자, 특히 관리자이면서 소유자인 개인만 편집을 하게끔 설정을 바꾸어 사용하는 패턴이었습니다. 게다가 일부 사용자들은 자신만 사용자 등록을 마친 후에 다른 사용자들은 사용자 등록을 하지 못하도록 하는 옵션을 만들어 달라고 제안하기까지 하였습니다. 그래서 만들어진 옵션이 $no_register 옵션인데, 개인위키 사용자들은 다음과 같은 식의 설정으로 모니위키를 운영하고 있습니다.

.... $security_class="needtologin"; // 로그인해야 일반적인 기능을 쓸 수 있도록 설정 $no_register=1; ...

혹은 다음과 같이

.... $security_class="mustlogin"; // 반드시 로그인하도록 설정. $no_register=1; ...


그런데 이러한 설정을 해놓으면 비로그인 사용자는 모니위키의 edit/diff 기능등을 사용할 수 없고, 모니위키의 보안 이슈가 된 RCS모듈은 아무런 영향력을 발휘하지 못하게 되어 위와 같은 식으로 설정된 개인위키는 RCS 보안문제와 상관 없게 됩니다.


이번 보안 이슈가 드러나게된 또다른 원인으로는 리그베타위키는 엔하위키 시절부터 익명사용자의 편집을 장려하는 정책을 펼치고 있었기 때문입니다. 익명사용자가 편집(edit) 및 변경점(diff)을 볼 수 있기때문에 RCS 모듈의 보안버그가 노출되게 되었던 것입니다.


개인위키 사용자의 대처 방법은?

위에서 기술했다시피 이번에 발견된 보안 이슈를 회피하려면 다음의 설정으로 config.php에서 개인위키 설정을 먼저 바꿔주시기 바랍니다. 그리고 최대한 빠른 시일 안에 모니위키를 백업하고 모니위키 최신 1.2.4p2로 업그레이드 하시길 권장해 드립니다.

.... $security_class="mustlogin"; // 반드시 로그인하도록 설정. $no_register=1; ...


다른 위키엔진은 문제 없나?

그렇다면 모니위키 이외의 다른 PHP 위키 엔진들은 이러한 이슈가 많은 편일까요? 개발자들은 실수를 하게 마련이며, 방대한 기능의 미디어위키는 외부프로그램에 의존하는 경우가 많아서 이런 이슈에 더 자주 노출되게 됩니다.


http://www.cvedetails.com/version/129227/Mediawiki-Mediawiki-1.19.0.html 쉘 인젝션 버그 있음.

https://gerrit.wikimedia.org/r/#/c/110069/2/includes/media/Bitmap.php 쉘 인젝션 버그가 있던 파일을 고치는 패치

(이 경우 미디어위키 1.19.0이 2012-05-02에 나왔고 문제 해결은 2014년 1월)


도쿠위키의 경우도 찾아보면 다음과 같이 나옵니다.

http://www.opennet.ru/base/linux/1159548011_6882.txt.html 2006년 쉘 인젝션 버그

https://www.freelists.org/post/dokuwiki/SECURITY-ALERT-problems-in-fetchphp

https://bugs.dokuwiki.org/index.php?do=details&task_id=926 코드 패치


개발자가 완벽한 천재가 아닌이상 그것이 쌍팔년도 보안 취약점이라고 할지라도 보안버그가 아예 없을수만은 없는 것입니다. 모니위키 코드는 지난 10여년간 꽤 지속적으로 관리되었고, 코드가 방대하지 않고 단촐하기때문에 이러한 버그에 대응이 더 쉽습니다.


모니위키 1.2.4p2

이번에 강성훈님이 발견하신 보안버그를 수정한 버전 1.2.4를 내놓았다가, 패키징 실수 및 RCS 보안버그를 추가로 수정한 패치 릴리스 1.2.4p2를 내놓았습니다. 모니위키 개인 위키 사용자분들은 위 내용을 참조하시어 config.php에서 개인위키 전용으로 편집을 할 수 있도록 설정을 바꾼 후에 모니위키를 업그레이드하시기 바랍니다~


http://dev.naver.com/projects/moniwiki/download/note/7020


P.S.

본인은 이번 리그베다위키 사건에 관련된 보안버그 이슈를 과거 토끼군으로 알려졌던 강성훈님이 버그리포팅하신 25일에 처음 알았으며, 버그가 심각한 만큼 이 문제를 해결한 모니위키 1.2.4 버전을 곧바로 내놓았습니다. 또한 이자리를 빌어 본인은 리그베다위키의 운영에 전혀 관여하지 않고 있다는 사실을 다시한번 밝힙니다.


물론 저는 리그베다위키 관리자 청동님과 종종 모니위키 문제점에 대한 이메일을 주고받고 있으며, 이번 보안 패치도 리그베다위키에 개인메일로 가장 먼저 알리고 제공하였습니다. 또한 리그베다위키 미러관리자로 알려진 퍼즐릿정님에게도 모니위키 버그 및 패치에 대해 알려드렸는데, 이는 다름이 아니라 퍼즐릿정님은 과거 국내 최대 위키였던 노스모크 위키의 관리자이기도 하기 때문입니다.


마지막으로 이번 보안버그를 알려주신 강성훈님에게 다시한번 감사를 드리는 바입니다~

by dumpcookie 2015. 4. 27. 10:57

클론 리플레이어는 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

이번에는 히브리어 읽기를 도전해봅시다.

구약성경은 히브리문자로 쓰여졌으며 독특해보이는 알파벳을 가지고 있습니다.

from 위키백과

AlefGimelDaletZayinHetTetYodKaf
אבגדהוזחטיכ
ך
LamedMemSamekhAyinTsadiQofReshTav
למנסעפצקרשת
םןףץ

그냥 보아도 도통 이해가지 않는 모양의 문자지만, 맨 처음 네개의 문자의 명칭이 의외로 알파벳과 유사한 것을 볼 수 있습니다. 알파 Aα는 Alef / 베타 는 Bet / 감마 Γγ는 Gimel / 델타 Δδ는 Dalet

사실 히브리문자는 고대 페니키아문자(기원전11세기)에서 갈라져나와 기원전10세기부터 사용되어졌다고 추정되고 있는 고대 히브리문자에서 비롯되었다고 합니다. 고대 히브리문자는 페니키아문자와 거의 비슷하게 생겼는데, 히브리문자는 이를 바탕으로 네모 모양으로 변형된 형태입니다. 자세한 모양은 위키백과 문서를 참조하세요.

알파A는 소 머리 모양에서 유래하였으며, 베타B는 집을 의미하며, 감마는 낙타(gimel, camel)을 의미하며, 델타Δ는 문(door)에서 유래했다고 하는데, 델타 삼각주는 델타의 모양과 유사해서 붙여진 이름이라고 하지요. (알파벳 ABC에서 왜 세번째 문자가 G가 아닌 C일까 의문을 가지신 분이 있다면, 원래는 낙타는 Gimel이었고 Camel이라고 발음이 변한 것을 상기해봅시다. 게다가 G는 C 문자에 감마의 Γ모양이 덧붙여진 형태입니다.)

아무튼 한번에 다 외우기는 어려울테니 알레프/베트/기멜/달레트 네개의 문자 모양을 우선 외워봅시다.

그러면 문자를 읽어볼 차례입니다.

אָלֶף 알레프

첫 문자부터 ABCD가 아닌 문자가 나와서 당황스럽습니다만, 히브리문자는 오른쪽에서부터 읽는 문자입니다. 그러므로 맨 오른쪽의 א알레프 문자를 먼저 읽는 것입니다. 그 다음 문자는 ל라메드(Lamed)인데 그리스문자 λ람다에 해당하며 자음 ㄹ에 대응합니다. 마지막 문자 ף는 위에서 살펴보면 Pe에 해당하며 그리스 문자 파이 π 알파벳 P에 해당합니다. 발음을 이어서 하면 아+ㄹ+ㅍ가 됩니다. 그런데 아래에 점이 보입니다. 아래 위로 붙이는 점은 모음의 발음을 뜻합니다. (한글도 작대기 앞 뒤 혹은 위 아래에 점을 붙여 발음이 바뀌는 형태라는 것을 상기해보면 재미있습니다.)

알레프 문자 아래에 붙은 T 모양은 모음 "아" 발음이며, 라메드 문자 아래의 세개의 점은 "에" 발음이고, 마지막 문자에는 모음 기호가 붙어있지 않습니다. 따라서 발음을 이어붙이면 아+ㄹ+ㅔ+ㅍ가 됩니다.

이제 히브리문자를 읽는 법을 이해했으니 다른 문자들도 읽어봅시다.

אָמֵן 아멘

히브리어를 어원으로 가지는 가장 유명한 단어일 것입니다. 알레프א가 첫 문자이고, 두번째 문자는 מ이 나오고 (한글 ㅁ과 모양이 유사하고 발음도 ㅁ 발음), 눈ן순서 입니다. 모음은 위에서 한차례 나온 T 받침이 아 발음이고, 멤מ 아래 모음점 두개는 "에" 발음이고 눈ן 문자는 "ㄴ" 발음입니다. 그래서 아멘이 됩니다. 그 뜻은 확신하다, 그러하다, 진실하다를 뜻합니다. 예수는 "내가 진실로 진실로 너희에게 말하는데"라는 말을 많이 썼는데, 여기서 "진실로"가 바로 아멘입니다.

שָׁלוֹם 샬롬

아마도 아멘 다음으로 유명한 히브리어일 것입니다. 첫문자 ש은 ㅅ 발음이고 두번째 문자는 앞에서도 나왔던 ל라메드 세번째 문자는 ו바브인데 v-w 발음이 나며, 이 경우에 위에 점이 하나 찍혀있고 o 발음이 됩니다. 마지막 문자는 ם멤이 또 나왔습니다. ש쉰의 오른쪽 위에 점이 찍혀있는데 이 경위 "쉬" 발음이며, 종합하면 쉬+ㅏ+ㄹ+ㅇ+ㅁ이 됩니다.
여기서 주의깊에 보면 멤은 두가지 폼이 있는 것을 볼 수 있습니다. 중간에 나오면 
מ 끝에 오면 ם이됩니다. 히브리문자는 다섯개의 모양이 살짝 다른 final 폼이 있습니다.

הים 하얌 - 바다

ה헷은 ㅎ, י요드는 y, 또 나온 ם멤은 ㅁ 발음이며 하얌이 됩니다. (헤엄치다의 헤엄과 발음이 유사)

אָדָם 아담 - Adam

이제 이것은 쉽게 읽을 수 있게 되었습니다. 받침 T모양은 "ㅏ"가 되고, 모음인 א알레프, ד달레트, ם멤은 "ㅇㄷㅁ" 이므로 아담이 됩니다.

히브리어를 배울 수 있는 사이트

다음 사이트에 가면 히브리어의 어원을 비롯하여 보기 쉽게 되어 있습니다. 아래 예시 이미지에서 모음이 빠져있는 것을 볼 수 있습니다. 히브리문자는 자음문자로서 모음이 생략되어 표기된 경우가 많습니다.

http://gifkg.org/hebrewword/adam.html



by dumpcookie 2015. 4. 21. 06:21


ἀγάπη 아가페 - 사랑


아가페아가페

아가페라는 단어는 많이 들어봤을 것입니다. 영어로 agape입니다. (발음이 일대일로 매칭되는 사실에 유의). 원어는 헬라어인데, 고대 그리스인이 쓰던 말입니다. (고대 그리스인은 자신들을 Έλλας(헬라스)로 불렀기 때문에 헬라어라 한다고 합니다. 한자 음역으로는 희랍어)

그러면 이제부터 그리스어/헬라어 읽는 방법을 살펴봅시다

조금 눈썰미가 있는 사람이라면 α알파 / γ감마 / π파이 정도는 알 것이고, η는 에타 입니다.
한글로 발음을 쓰자면 "아+ㄱ+아+ㅍ+에"가 됩니다.

그리스 알파벳은 아시다시피 로마자의 조상격입니다. (알파베타감마델타...Αα Ββ Γγ Δδ....)  (그리스 문자에 대해서 관심이 있으신 분이라면 한국 위키백과의 그리스 문자 문서를 참조하세요)

(감마는 대문자로 Γ인데, 재밌게도 한글로 ㄱ 과 발음이 동일하고 모양도 비슷합니다.)
에타
η는 그리스 대문자로 Η이며, 발음이 "에" 에 해당합니다.

아가페를 αγαπη로 쓰지 않고 ἀγάπη처럼 위에 점이나 획이 있는 것은 발음의 강약을 나타내는  ́(/상승) ̀(\하강) 혹은 발음의 방법(거친 숨소리 ̔ /약한 숨소리 ̓)을 나타냅니다.

그러면 다음을 살펴보죠.

πρῶτος 프로토스 - 처음의


프로토스 질롯프로토스 질롯 - from 위키백과


어디서 많이 들어 본 듯한 이 단어는 스타크래프트에서 프로토스 종족을 뜻하는 말이 아니라 원래는 "처음의"라는 뜻의 헬라어 입니다. 프로토스 종족의 뜻이 "첫번째 창조물"이라고 하니 헬라어를 차용해서 이름 붙인 것이지요.
이것도 읽어봅시다. ρ로, 로마자 R에 해당하고 R발음이고, ω오메가는 "알파와 오메가ΑΩ/αω"의 그 오메가이고, ㅗ 발음에 해당합니다. τ타우는 T발음, o오미크론은 약한 오 발음. ς시그마는 S발음인데 Σ대문자와 소문자σ와 더불어서 단어의 가장 맨 끝에 올 때에 ς로 쓰입니다. (음가 S에 해당하는 로마자 S에 더 비슷)

여기까지 읽은 분들이라면 글자와 발음이 일대일 대응되기 때문에, 그냥 소리내어 읽는 것을 조금 배우게 되면 읽는 것이 의외로 쉽다는 사실을 알 수 있습니다. 다음은 그리스어 알파벳과 발음입니다.

그러면 몇가지 상당히 익숙할 법한 단어를 살펴봅시다.

  • ἐκκλησία 에클레시아 - 교회
  • ἀνθρωπος 안드로포스 - 사람
  • ἀγγελός 앙겔로스 - 엔젤, 천사 - (아+ㄱㄱ+ㅔ+ㄹ+오+스 악겔로스라고 읽지 않고 앙겔로스라고 읽음에 유의. 즉 받침으로 위치할 때는 영어 G처럼 응 발음이 난다.)
    εὐαγγέλιον 에반겔리온(?) 유앙겔리온 - 복음, 윕실론 υ는 v와 유사하게 생겼으나 u 혹은 y발음이고, 람다
    λ는 ㄹ발음. ν뉴는 알파벳V의 소문자와 유사하게 생겼으나 N에 대응하고 발음도 이에 대응하며, 유명한 에니메이션 에반겔리온의 어원이기도 합니다.
  • ἀπόστολος 아포스또로스 - 사도 apostle: 고대어  ἀποστέλλω(아포스뗄로)에서 왔다고 하며, ἀπο(아포) "from"을 뜻하고 στέλλω(스뗄로) "I sent"를 뜻한다. (윅셔너리 참조)
  • λόγος 로고스 - 말씀.
  • κοσμος 코스모스 - 세상. 우주.
  • ειρήνη 에이레네 - 평강. (히브리어어로 샬롬)
  • χαρά 카라 - 기쁨.
  • παρά 파라 - para- 곁에
  • μικρός 미크로스 - 작은 (micro)

이 문서의 그리스어 입력은 http://www.typegreek.com/ 입력기를 이용했으며, 발음은 http://latina.bab2min.pe.kr/xe/hPron 사이트를 참조했습니다.


by dumpcookie 2015. 4. 20. 09:44

무리를 보시고 민망히 여기시니 이는 저희가 목자 없는 양과 같이 고생하며 유리함이라 (개역 한글, 마태복음 9:36)

꽤 오래전에 이 말씀을 보고 "민망히 여기다"라는 말이 요즘에 의미하는 것과 조금 다른 것 같아서 찾아보았던 적이 있다. 금방 알게 되었지만 여기서 의미하는 민망히 여기다는 다음 번역들에 의하면 "불쌍히 여기다"는 의미이다.

또 목자 없는 양과 같이 시달리며 허덕이는 군중을 보시고 불쌍한 마음이 들어 (공동번역)

무리를 보시고 불쌍히 여기시니 이는 그들이 목자 없는 양과 같이 고생하며 기진함이라 (개역 개정)

또 예수님은 목자 없는 양같이 흩어져 고생하는 군중들을 보시고 불쌍히 여기셨다. (현대인의 성경)

영문을 보면 단순히 불쌍히 여기다는 뉘앙스가 아니라 compassion이라는 뜻이다.

When he saw the crowds, he had compassion on them, because they were harassed and helpless, like sheep without a shepherd. (NIV)

compassion의 어원을 살펴보면 com(together 같이)+passion(고통)을 뜻하며 같이 아파한다는 뜻이 된다. 한국말로 동정심이라는 말로 표현되기에는 조금 부족한 듯 보인다.

또 인터넷으로 검색해보니 다음과 같은 내용도 나온다.

(크리스쳔 투데이 - 한글성경의 번역오류의 실례)에서 발췌

...

자비(慈悲), 인자(仁慈), 동정심(불쌍히 여김)이라는 어휘들은 각각 그 분명한 의미가 있는 어휘들이다. 그런데 한글개역성경은 이들 어휘들을 긍휼(矜恤)이라는 이해하기 힘든 어휘에 섞어서 써서 분명해야 할 의미들을 완전히 혼란시켰다. 영어성경으로 볼 때, tenderness, compassion, mercy, pity, kindness가 모두 때로는 긍휼, "민망히 여기다, 자비, 인자로 혼용되었다. love도 때로는 인자로 번역되었다. 그러니 누가 긍휼이나 민망히 여기사의 분명한 의미를 말할 수 있겠는가? 긍휼은 교회 내에서만 사용되는 중국식 어휘인 것을 유의할 필요가 있다. 긍휼은 새로이 번역되는 성경에서는 그 사용이 완전히 배제되어야 한다.

...

compassion의 성경 원문은?

그러면 성경 원문은 뭐라고 되어있을까?

민망히 여기다 혹은 compassion의 원문 ἐσπλαγχνίσθη은 마음, 간장, 허파, 창자(σπλάγχνον)를 의미하는 말에서 유래한 단어라고 한다. 다음 링크를 올려두니 궁금하신 분은 참조하시라.

그런데 왜 하필 창자가 그 어원일까? 그것은 사랑과 연민의 감정이 내장에서 나온다고 생각했었기 때문이라 한다. 현대에도 우리들은 마음이 우러나는 곳을 머리라고 하기도 하지만 심장 혹은 가슴이라고 말하는 것에 익숙하지 않은가.

아무튼 이러한 내용을 알게된 후에 성경에 나오는 민망하다는 말이 나오는 구절을 유심히 살펴보곤 했고, 예수가 느끼는 동정심이야 말로 사람이 가져야 할 가장 기본적인 부분이 아닐까 라는 생각을 했다.

잠잠하라 하되 더욱 소리질러

신약 성경의 복음서를 보다 보면 예수가 compassion을 느꼈다는 부분이 상당수가 나오는데, 그러면 어떤 내용이 있었는지 조금 더 살펴보자.

소경 둘이 길 가에 앉았다가 예수께서 지나가신다 함을 듣고 소리질러 가로되 주여 우리를 불쌍히 여기소서 다윗의 자손이여 하니 무리가 꾸짖어 잠잠하라 하되 더욱 소리질러 가로되 주여 우리를 불쌍히 여기소서 다윗의 자손이여 하는지라 예수께서 머물러 서서 저희를 불러 가라사대 너희에게 무엇을 하여주기를 원하느냐 가로되 주여 우리 눈 뜨기를 원하나이다 예수께서 민망히 여기사 저희 눈을 만지시니 곧 보게 되어 저희가 예수를 좇으니라. (마태복음 20장 개역한글)

아...

4월16일

오늘은 4월 16일. 작년 4월 16일 세월호가 침몰했던 사건을 얘기하지 않을 수 없다. 나는 그 당시 그런 어처구니 없는 사건을 보고, 가장 기본적인 원칙, 기본 안전수칙, 메뉴얼 등등의 단어들을 떠올리며 한국은 그 기본조차도 안되있는 나라라는 것을 새삼 느꼈었다. 그 예전 삼풍사고의 충격보다 더했고, 대구 지하철 참사보다 더한 사건이 터졌던 것이다. 그런데 자세한 내용을 살펴보면 더욱 안타깝고 어처구니 없는 것이, 초동 대응만 잘 했었도 모두 살아날 수 있었다는 사건이었다는 것에 있었다.

가까운 일본도 세월호와 유사한 사건이 있었다. 1955년 시운마루호 사건으로 168명의 소중한 생명이 사망하였다. 이 사건에 대한 일본의 대응은 주목해볼만 하다. 일본은 선박 안전 기준/안전 대책을 비롯하여 이와 연관된 안전 기준 재정비. 선원 자격 기준 강화 등등 이루 말할 수 없이 많은 것이 바뀌었고, 심지어는 시운마루호 사건에서 익사자가 가장 많았다고 하는데 이를 막기 위해서 각급 학교에 수영장을 설치하고 수영 교육을 필수적으로 하게 되었고, 구조,구난 훈련교육을 주당 4시간을 하고 있다고 한다. (from 시사저널)

그런데 지난 1년간의 박근혜 정부의 대응은 어떠했나. 기본 안전장치가 없어 보이는 듯한 대형 안전사고가 몇차례 더 터졌는데도 이상할 정도로 잠잠하다. 세월호 이후의 행태를 보면 거의 이해가 불가 수준이다. 이 정부는 인간으로 갖춰야 할 기본이 안되어있는 것일까? 동정심도 모르고 불쌍해할줄 몰라 보인다.

compassion...

예수는 동정과 연민을 느끼기만 하고 그냥 모른채 지나치지 않았다. 시각 장애자의 눈을 만졌고, 한센병(나병) 환자를 만지고 고쳤다. 개역한글 성경의 민망하다는 단어는 총 8회 나오는데, 구약 요엘서에 한번 나오고 나머지는 모두 신약성경 복음서(예수의 행적을 기록한 성경)에 나온다. (물론 compassion은 NIV에서 찾아보면 신구약 성경에 두루 나온다.)

동정심, 연민, 불쌍해함, compassion은 사람으로 모범을 보이신 예수의 모습을 비춰볼 때 인간이라면 가질 수 있는 매우 자연스러운 감정이라고 생각한다. 세월호 가족들이 느끼는 고통을 알 수는 없겠지만, 오늘 단 하루만이라도 그들이 느꼈을 고통을 함께 느끼고 싶으며, 더 나아가 세월호 사건과 같은 안전 사고가 더이상 일어나지 않게 하기 위하여 그 원인을 성역없이 철저히 분석하여 그 진상을 규명하고, 썩은 곳을 찾아 도려내고 곪아터진 곳을 찾아내 고칠 수 있기를 기대하고 기도해 본다.





by dumpcookie 2015. 4. 16. 08:47

안드로이드에서는 오디오/비디오/이미지 등등의 미디어 파일을 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
| 1 2 3 4 5 6 7 |