NaraeAudioRecorder, AudioRecorder for Android

회사에서 프로젝트를 진행하던 도중, 음성 녹음에 대한 기능을 개발할 일이 있었다.

예전(아마 2017년쯤)에 쓰던 lameMp3 wrapper가 있었기에 그대로 활용했었는데, 최신 환경에서는 녹음된 소리가 제대로 나오지 않고 이상하게 나오는 현상이 있었다.

그래서, 이번 주말동안 기존에 AudioRecord를 만졌던 기억을 되살려 음성 녹음 라이브러리를 만들게 되었고, 그것이 바로 WindSekirun/NaraeAudioRecorder 이다.

구조

크게 바라보면 해당 라이브러리는 총 4개의 파트로 구성된다.

  • AudioChunk: AudioRecord에서 bufferSize 만큼 읽어온 만큼의 raw 데이터와 raw 데이터에 대한 정보를 담고 있다.
  • AudioRecorder: Wav, PCM, FFmpeg 등 여러 포맷에 대한 객체로 start, stop, resume, pause 이벤트에 맞춰 작업을 실행한다.
  • AudioSource: 기본과 NoiseSuppressor (배경 노이즈를 제거하는 안드로이드 API 전처리기) 에 대한 기본 속성이며, AudioRecord에 대한 정보를 갖고 있다.
  • RecordWriter: AudioSource 와 AudioRecorder 사이에 있는 중간 매개체로 AudioSource로 설정한 AudioRecord에서 bufferSize만큼 raw data를 가져와 OutputStream에 쓰고, 그 이벤트를 AudioRecorder 에 전달하는 역할을 한다.

각각 유기적으로 연결되어있어, 처리 순서는 AudioRecorder > RecordWriter > AudioChunk 이고, AudioSource는 RecordWriter를 사용하기 위한 AudioRecord 설정 객체라 이해하면 편하다.

특이점

특이점이라 한다면 AudioRecord에서 가져오는 데이터는 raw 데이터이고, 이를 wav나 mp3 등으로 변환하기 위해서는 특별한 작업이 필요하다.

예전에도 AudioRecord to Wav 란 주제로 다룬 적이 있었고, 이번에도 특별한 작업은 없다.

mp3, aac, wma 로 변환하기 위해서 FFmpeg를 사용했는데, FFmpeg에 대한 command를 구성하도록 했다

val tempFile = File(file.parent, "tmp-${file.name}")
commandBuilder.addAll(listOf("-y", "-i", file.path))

if (convertConfig.samplingRate != FFmpegSamplingRate.ORIGINAL) {
     commandBuilder.addAll(listOf("-ar", convertConfig.samplingRate.samplingRate.toString()))
}

if (convertConfig.bitRate !== FFmpegBitRate.def) {
    commandBuilder.addAll(listOf("-sample_fmt", convertConfig.bitRate.bitRate))
}

if (convertConfig.mono) {
    commandBuilder.addAll(listOf("-ac", "1"))
}

commandBuilder.add(tempFile.path)

FFmpeg를 다뤄보는 것은 두번째이기 때문에, 다소 그렇게 삽질하지는 않은 것 같다. (아마도… (._.

물론 FFmpeg를 사용한다면 라이센스 문제가 나오기 때문에, 멀티 모듈로 구성해서 core만을 사용한다면 FFmpeg에 의존성이 없게 구성했다. 아무래도 FFmpeg를 프로덕션에서 바로 사용하기에는 부적절한 면도 있기도 하다.

마지막으로 조금 특이한 점이라면, 목적 파일의 확장자에 따라 wav, mp3 등 각각의 recorder로 넘기는데 이 과정에서 리플렉션을 통해 접근했다는 점이다.

class FFmpegRecordFinder : RecordFinder {

    /**
     * see [RecordFinder.find]
     */
    override fun find(extension: String, file: File, writer: RecordWriter): AudioRecorder {
        return when (extension) {
            "wav" -> WavAudioRecorder(file, writer)
            "pcm" -> PcmAudioRecorder(file, writer)
            "aac" -> FFmpegAudioRecorder(file, writer)
            "mp3" -> FFmpegAudioRecorder(file, writer)
            "m4a" -> FFmpegAudioRecorder(file, writer)
            "wma" -> FFmpegAudioRecorder(file, writer)
            "flac" -> FFmpegAudioRecorder(file, writer)
            else -> PcmAudioRecorder(file, writer)
        }
    }

}

RecordFinder 란 클래스에서 find 메서드를 통해 Recorder 로 넘기고, 이 RecordFinder를 Class 객체를 사용하여 바로 인스턴스를 만들고 활용하는 방법으로 접근했다.

 try {
            val finder = recordFinder.getConstructor().newInstance() as? RecordFinder
                    ?: throw IllegalArgumentException(LogConstants.EXCEPTION_FINDER_NOT_HAVE_EMPTY_CONSTRUCTOR)

            val file = recorderConfig.destFile ?: return // it can't be null
            audioRecorder = finder.find(file.extension, file, recordWriter)
} catch (exception: Exception) {
            throw IllegalArgumentException(LogConstants.EXCEPTION_FINDER_NOT_HAVE_EMPTY_CONSTRUCTOR, exception)
}

물론 해당 RecordFinder 클래스를 정확한 형식에 맞추지 않고 개발을 진행했을 경우 사용하지 못한다는 문제점이 있지만, 가능한 멀티모듈로 구성하면서도 유기적으로 연계되기 위해서 해당 방법을 사용했다. (적어도 개발자 자신이 확정할 수 있으면 괜찮지 않을까.. 라는 조금 어린 생각을 가지기도 했다.)

마무리

총 3일동안 개발하면서 나름대로 코드를 최대한 깔끔하게 짜고, 문서화도 제대로 하려고는 노력은 했지만 아직까지 표현력이 부족한 점도 많긴 많았다.

그래도 2019년 한 해의 시작을 라이브러리로 시작했으니, 이번 년도 말에도 라이브러리 개발로 끝날 것 같기도 하다.

마지막으로 개발했던 모든 소스와 샘플들은
WindSekirun/NaraeAudioRecorder 에 공개되어 있다. JCenter에도 배포되어있으니, 바로 다운 받아 사용이 가능하다.

2018 한 해를 돌아보며…

2018년.

기나고도 짧은 1년이 지나가고, 이제 2019년이 다가옵니다.

그런 기념으로, 2018년의 중요하고 변화점이나 느낀 점을 살펴보고 2019년에 달성하고 싶은 목표를 적어보려 합니다. (물론 개발적으로요.)

1월 – PyxisBaseApp (MVVM + Dagger)

2018년 1월 20일, PyxisBaseApp라는 MVVM + Dagger + Databinding + Retrofit 스택을 가진 베이스 앱을 작성하기 시작, 2018년 12월 31일 기준으로 배포 버전 1.3.3, 커밋 수 316개의 적당한 규모의 베이스 앱을 제작하였습니다.

이 베이스앱의 전/후로 지금까지의 본인 안에서의 개발 패러다임이 바뀌었는데, 전에는 일정한 패턴 없이 Activity 하나에 집중하는 형태였다면 지금은 일정한 패턴을 따라서 MVVM 과 Continous Integration을 목적으로 개발하고 있습니다.

이 때 MVP를 거치지 않고 MVVM으로 바로 거친 이유로는 당시 같은 회사에 계셨던 개발자분이 MVP를 사용하셨는데 이 구조를 보니까 Activity – Contract – Presenter가 강제되는 형태로 조금 비효율적으로 보였기 때문입니다 . 그리고 마침, 안드로이드에서 Android Architecture Component 라고 하는 구조가 나왔었기에 MVVM을 좀 더 쉽게 작성할 수 있었습니다.

이렇게 PyxisBaseApp는 계속해서 변화를 거쳐 9월 29일에는 JFrog Artifactory를 통해 본격적인 라이브러리 형태의 베이스 앱을 제작했고, 나름대로 만족할 구조가 탄생하였습니다.

3월 – Annotation Processor

1월과 2월에는 MVVM 구조를 중점으로 적용했다면, 3월에는 중복되는 코드를 줄이기 위한 Annotation Processor에 대해 집중했습니다.

지금까지 어노테이션을 Reflection 을 통한 접근방법만 사용했었는데, 타겟으로 하는 어노테이션이 있는 클래스나 메서드 대상으로 코드를 생성하는 Annotation Processor의 도입으로 Dagger의 Activity, Fragment, ViewModel의 주입이 어노테이션 하나를 부착하는 것 만으로도 끝나고, CustomView의 Attribute 파싱 등을 빠르게 할 수 있었습니다.

물론, 이 떄 공부한 지식으로 Annotation Processor 기반 기술을 도입하거나 분석할 때 큰 도움이 되었기도 했습니다. (Databinding나 Dagger 등)

7월 – RxJava

계속 미루고 왔었던 RxJava 도입을 이 때부터 시작하였습니다. 처음에는 Listener 형태의 Callback를 Observable로 변경하는 것에서부터 Socket 통신의 RxJava wrapper까지 작성하면서 이전에는 쉽게 달성하지 못했던 스레드 관리나 데이터 소스로부터의 효율적인 합성을 달성하였습니다.

그리고 이 때, SocialLogin 기능과 RxJava를 합친 RxSocialLogin(https://github.com/WindSekirun/RxSocialLogin)을 개발하고 배포하였습니다.

8월

산업기능요원으로 근무하다보면 언젠가 찾아오는 시련인 훈련소(._. )를 다녀왔습니다. 따라서 이 때는 기록이 없습니다.

9월 – Docker를 통한 SaaS 컨테이너 배포

이 때까지 PyxisBaseApp는 프로젝트마다 멀티모듈로 포함되어 있었기에 버전 관리가 안되고, 여러 프로젝트마다 다른 베이스 앱 코드를 가지고 있었기에 코드 관리도 더더욱 힘들었습니다.

그래서 Docker를 공부하고 새 인스턴스에 JFrog Artifactory를 올리고 PyxisBaseApp를 모듈 단위 배포가 아닌 라이브러리 형태 배포로 변경하였습니다.

추가적으로, 처음으로 BLE 앱을 원개발하면서 RxAndroidBle(https://github.com/Polidea/RxAndroidBle) 도 사용해보았습니다. 다소 어려웠지만 notify, write 이벤트를 쉽게 처리할 수 있었던 것이 가장 마음에 들었습니다.

12월 – Jenkins 도입

이전에는 CircleCI를 사용했었으나 Private project에는 적용하지 못하고 오픈 소스 프로젝트만 사용하였습니다. 그래서, 꽤나 이전부터 계획은 있었음에도 쉽게 도입할 수 없었던 Jenkins를 Docker의 힘을 빌려 사용하게 되었습니다.

그리고 12월 25일, 대망의 확장 공사를 시작하면서 지금까지 분리되어있던 블로그 인스턴스 + (Artifactory/Jenkins) 인스턴스를 합쳐 지금의 인스턴스로 이사하면서 메일 서버나 FTP 서버 등도 전부 Docker를 통해 관리하게 되었습니다.

마무리

2018년에만 공부하고, 새로 도입해서 실제 사용한 기술이 MVVM, DataBinding, RxJava, Dagger (DI), Retrofit, ObjectBox, Docker, Jenkins, AndroidX, Static Analysis 로 작은 것 까지 포함하면 20개는 족히 넘을 것 같습니다.

2019년에는 빠르게 변화하기 보다는 기존에 사용하던 기술을 좀 더 가다듬어 좀 더 전문적인 지식을 가질 수 있도록 열심히 갈고 닦아야 될 것 같습니다.

그리고, 대망의 그 날(2019-04-19)도 얼마 남지 않았고요. 후후…

새 인스턴스로 확장 이전과 후기

이번 연휴(2018. 12. 22 ~ 2018. 12. 25) 동안 지금까지 운영해오던 UzukiLive 서버를 확장이전 하여 새롭게 환경을 구축하게 되었습니다.

기존 인스턴스와 문제점

기존 환경은 이렇게 설정되어 있습니다.

  • Vultr VC2 1core 2GB (UzukiLive 인스턴스) -> 블로그, nextcloud. 도커로 되있지 않음
  • Vultr VC2 2core 4GB (Artifactory 인스턴스) -> Artifactory, Jenkins. 도커로 되어있음

그리고 메일 서버의 경우 Zoho 플랫폼을 빌려 사용하였고, 처음에 삽질을 너무 크게 한 나머지 uzuki.live 로 접속하면 SSL 에러가 발생하곤 했습니다.
그 외 문제점이라고 하면 FTP를 쉽게 사용할 수 없거나, 너무 설정이 여기저기 있어 확장하려고 해도 쉽지 않았습니다. (오죽하면 인스턴스를 두개 생성해서 관리를 했지만요… (._. )

당연히 비용도 비용이니 좋지 않은 사양에 월 30달러나 지불해야 한다는 것은 이해가 가지도 않긴 합니다.

따라서 이번 연휴때 계획을 잡고 신규 인스턴스에 전부 이전하기로 결정했습니다.

신규 인스턴스

신규 인스턴스는 Vultr의 VC2 4core 8GB, 100GB SSD 입니다. 이전 사양보다 약 2(Artifactory 인스턴스 기준) ~ 4(UzukiLive 인스턴스 기준)배 이상 사양이 증가하였습니다.

사양을 대폭 올린 이유로는 Jenkins 때문인데, 2core 4GB 사양에서 gradle 빌드를 두 개 이상 돌리는 순간 컨테이너가 메모리 부족으로 죽어버리는 대참사가 발생하여, 빌드를 두 개 이상 동시에 돌리더라도 문제가 없도록 구성하였습니다.

신규 인스턴스는 모든 요소가 Docker에 의해 관리되는 Dockerize 환경을 사용할 것이고, 아래 범주로 서비스를 관리할 예정입니다.

  • Reverse Proxy -> nginx
  • Stack: Infra -> adminer, pure-ftpd, docker-telegram-notifier
  • Stack: Blog -> MariaDB + WordPress
  • Stack: Build-Automation -> Jenkins, Artifactory
  • Stack: Mail -> docker-mailserver(IMAP/SMTP) + rainloop (Web UI)
  • Stack: Intro -> uzukilive-intropage
  • Stack: Octobox -> Octobox + Redis + PostgreSQL

그러면, 각 범주마다 설명을 약간 추가하면서 기능을 소개하려 합니다.

Reverse Proxy + SSL

기존 UzukiLive 에서도 사용했던 Nginx를 사용하고, 여기에 LetsEncrypt를 이용하여 모든 외부용 서비스에 SSL를 제공할 계획입니다.

보통 nginx-proxy + letsencrypt-companion 조합으로 사용하지만 이 조합의 경우 docker 소켓에 붙어 일정 시간마다 스캔을 해서 추가하는 방식으로 되어있어, 그거보다는 좀 더 귀찮지만 하나의 설정 파일(conf)로 존재하는 것이 좋다고 생각되었습니다.

따라서 새 서비스를 연동할 때 마다 아래의 명령어를 통해 인증서를 발급받았습니다.

docker run -it --rm -v /data/cert:/etc/letsencrypt -v /data/cert-data:/data/letsencrypt certbot/certbot certonly --webroot --webroot-path=/data/letsencrypt -d uzuki.live

저 방법을 사용하기 위해서는 일정의 예제 conf 파일을 만들어 nginx를 가동시킬 필요가 있는데, 샘플 nginx 파일을 만들고 DONAME_NAME 만 교체해서 생성하도록 했습니다.

server {
    listen      80;
    listen [::]:80;
    server_name uzuki.live;

    location / {
        rewrite ^ https://$host$request_uri? permanent;
    }

    location ^~ /.well-known {
        allow all;
        root  /data/letsencrypt/;
    }
}

Stack: Infra

여기에는 3개의 서비스가 들어가는데, adminer 는 MySQL/MariaDB의 정보를 보거나 수정할 수 있는 프로그램으로 phpmyadmin 대체용입니다

그 다음으로 pure-ftpd는 FTP 서버로 /home/pyxis 경로에 접근하여 데이터를 주고 받을 수 있게 설정했습니다.

마지막으로 docker-telegram-notifier 는 미리 만들어둔 Telegram bot로 컨테이너의 시작/중지 등의 상태를 보내줍니다.

가령 Jenkins가 시작되었다면 아래의 메세지를 보내줍니다.

Started container jenkins
Image: windsekirun/jenkins-android-docker:1.0.2
Container ID: ae8f526e8b42d216146fb68ee49f6506631259780f3c75195718786da6245cb6
오늘도 약 400개의 메세지를 받아내는 우즈키(..)

Stack: Blog

여기에는 2개의 서비스가 들어가는데, 워드프레스와 그 DB로 MariaDB를 선택했습니다. 기존 인스턴스에는 MySQL로 되어있고, 이 데이터를 그대로 가져갈 필요가 있었습니다.
따라서 PostgreSQL 보다는 MariaDB를 선택했습니다.

주소는 기존의 blog.uzuki.live 에서 pyxispub.uzuki.live 로 변경했는데, 기존 주소로 들어가면 새로운 주소로 리다이렉션 되도록 기존 인스턴스의 nginx 단을 수정했습니다.

기존 인스턴스는 2주 뒤인 1월 11일쯤에 제거될 예정입니다.

Stack: Build-Automation

여기에는 2개의 서비스가 들어가는데, 각각 Gradle Repository용 Artifactory와 Jenkins가 들어갑니다. 이 두개 서비스에 대해서는 일전 블로그 글로 설명한 적이 있으므로 별도로 설명할 것은 없다고 생각됩니다.

다만 아주 잉여롭지만 봇을 만든 겸에 빌드 시작 / 결과를 알려주는 별도의 Jenkins 호환용 그루비 스크립트를 작성해서 적용했습니다

Stack: Mail

기존 메일(pyxis@uzuki.live) 는 Zoho Mail에서 수신/발신하도록 되어있어 내 것이 아니기도 한 존재였습니다.
따라서 이번 인스턴스에는 IMAP/SMTP 서버를 직접 구축하여 메일을 수신/발신할 수 있도록 설정하였습니다.

…다만, vultr가 SMTP 포트인 25번을 막아버리는 바람에 일단은 Gmail에 의존하여 발신하도록 설정하였으나 좀 더 Postfix를 공부하여 수정할 계획입니다.

메인 화면
글쓰기 화면

웹 메일 UI는 rainloop를 사용하였고, 특출나게 UI가 세련된 건 아니지만 나름대로 들어갈 기능은 전부 포함되어 있습니다.

Stack: Intro

물론 반응형입니다.

CSS Toolkit인 타키온(https://tachyons.io/) 을 이용하여 5~10분만에 대충 만든 인트로 페이지로, 이 서버에서 제공하는 서비스의 링크와 Github, Telegram, Contact 링크를 포함합니다.

물론, 이 인트로 페이지(라고 해도 HTML 1개 + CSS 1개) 도 docker 이미지를 만들어서 제공했으며, 도커 이미지는 https://github.com/WindSekirun/uzukilive-intropage 입니다.

Stack: Octobox

Octobox 라고 하는 Github의 알림을 관리하는 서비스가 있는데, 이 서비스가 self hosted로 하면 프라이빗 프로젝트까지 포함되어 설치하게 되었습니다.

이 서비스의 자세한 설명은 공식 홈페이지 (https://octobox.io/) 를 들어가는 것이 편리합니다.

마무리

일전에도 Jenkins나 Artifactory를 도커로 활용하고 있었지만 이번 기회를 통해 도커를 통한 관리가 얼마나 확장성과 자유성을 가져주는지 깨달았습니다.

개인 목적으로는 나름대로 규모가 있고, 따로 설치하라고 한다면 아마 멘탈 여러번 나가서 쉽게 던졌을텐데, 명령어 한 두줄 만으로 모든 것을 설정할 수 있다는 것이 큰 장점인 것 같습니다.

컨테이너가 14개가 되다보니 docker-compose로 각 스택별로 모아 필요한 스택만 켜고 끄고 할 수 있게 설정해두니, 패치할 때도 다운타임이 5~10분 내로 할 수 있다는 것도 좋았습니다.

열심히 가동되는 컨테이너(들)

그러면, 새로운 인스턴스와 새로운 환경에서 남은 6일, 앞으로 다가올 2019년에도 PyxisPub와 이 주인장(._. … )을 잘 부탁드립니다.