공부/인공지능

소리가 줄어드는 녹음.

TechToast 2026. 8. 5. 17:21

메뉴바 회의 녹음기부터 본다. 여덟 개로 시작해 열한 개가 된 파이프라인에, 첫 번째로 붙은 것이 이 녹음기다. 회의가 시작되면 클릭하고, 끝나면 또 클릭하는, 그게 전부인 앱이다. 5분마다 도는 동기화나 10분마다 도는 전사와 달리, 이건 손가락이 눌러야 시작되는 트리거 방식이다. 트리거로 산다는 점은 끝까지 그대로였는데, 소리를 받는 방식만 이상하게 무너졌다. 열 초를 녹음했는데 2.73초짜리 파일이 나오는, 소리가 줄어드는 실패였다. 무엇이 줄었는지, 하나씩 보자.

 

그달 중반에 붙은 세 개는 성격이 저마다 달랐다. 매일 새벽 세 시에 도는 정리 작업, 매주 월요일 아홉 시의 논문 리캡, 그리고 버튼이 눌릴 때 도는 녹음기였다. 셋 중에서 가장 손에 가까운 곳에 있는 것이 이 녹음기였다. 이 녹음기가 왜 필요한지는 지난 편에서 이미 설명했다. 회의에서 나는 소리가 유입의 한 갈래이고, 그 소리를 파일로 만드는 일은 마이크에 닿을 수 있는 맥에서만 가능하다. 파일이 떨어지면 10분짜리 전사가 받고, 15분짜리 정리가 받아서 회의록 폴더에 넣는다. 그러니 녹음기는 파이프라인의 가장 위에 서서, 뒤의 모든 작업이 기다리는 첫 관문이다. 여기 앉힌 프로그램은 rumps 라는 메뉴바 프레임워크로 짰고, launchd 가 로그인과 함께 띄운다. 메뉴바에 아이콘이 하나 생기고, 클릭하면 녹음이 시작되고, 다시 클릭하면 끝난다. 만들어진 파일은 녹음 시각이 그대로 이름이 되는 wav 로, 회의록 폴더의 audio 아래에 쌓인다. 첫 녹음에서는 마이크 권한을 묻는 창이 하나 떴고, 그다음부터는 조용했다.

소리가 줄어든다

첫 버전은 ffmpeg 에 맡겼다. avfoundation 이라는, 맥의 소리 장치를 캡처하는 입력 방식이었는데, 명령 하나로 마이크에서 파일까지 가는 가장 간단한 선택이었다. 지금 보면, 그 간단함이 실패의 절반이었다고 생각한다.

 

ffmpeg -f avfoundation -i 마이크 -ar 16000 회의.wav

 

간단했다. 그리고 그 간단함이 첫날 밤에 무너졌다. 열 초쯤 녹음하고 파일을 보니 2.73초였는데, 재생해 보면 목소리가 빠르게 돌아가면서 지지직거리는 소리가 섞여 나오고, 마치 녹음이 저 혼자 앞으로 달려간 것 같았다. 열 초가 2.73초가 되려면 소리의 70%가 넘게 사라져야 하는데, 길이가 줄어든 게 아니라 통째로 사라진 조각이 있다는 걸 알게 됐다. 샘플이 대량으로 누락된 것이다. 재생을 몇 번 반복해도 결과는 같았다.

 

누락은 어디서나 같은 정도가 아니었는데, launchd 가 띄운 환경에서 특히 심했다. 같은 스크립트인데, 어디서 태어났느냐에 따라 샘플이 사라지는 양이 달라졌다. 그런데 이 녹음기는 평소에 launchd 가 띄운 상태로 산다. 로그인과 함께 태어나는, 그 상태가 기본값이었다. 가장 자주 쓰는 조건이, 가장 심한 증상을 보여준 셈이다. 기록에는 '특히 launchd 환경에서 심함'이라는 한 줄만 남아 있고, 왜 그런지는 적지 않았다. 이 실패에서 얻은 건 하나다. 같은 코드라도 누가 띄우느냐가 결과를 바꾼다는 것.

 

리샘플이 누락을 키웠다

원인으로 지목된 것 중 하나가 -ar 16000 이라는 옵션이었다. 이걸 인라인으로 붙이면 캡처와 리샘플을 한 명령에서 동시에 처리하게 되는데, 그 조합이 누락을 키운다는 것만 기록에 남아 있다. 실시간 캡처에서 처리 한 번이 밀리면 그 프레임은 다시 채워지지 않으니 할 일이 둘일 때 밀릴 일도 둘이 된다는 게 내 짐작이지만, 원인을 끝까지 파보지는 않았다.

 

그래서 전략을 바꿨다. ffmpeg 를 통째로 빼고, 소리를 받는 일을 PortAudio 로 넘겼다. 파이썬에서는 sounddevice 라는 이름으로 쓰는 라이브러리인데, InputStream 이라는 객체가 마이크에서 들어오는 샘플을 콜백으로 흘려보내고, 그 콜백에서 받은 그대로 wave 모듈로 16k 모노 파일을 직접 기록한다. 한 줄로 줄어든 명령은, 그만큼 많은 일을 대신하고 있었다.

 

sounddevice.InputStream(samplerate=16000, channels=1, callback=버퍼를_wav에_쓰기)

 

 

받기와 쓰기가 같은 프로세스 안에서, 버퍼가 쌓이는 족족 처리된다. 중간에 끼어드는 프로그램이 없다. 콜백은 소리가 도착할 때마다 불리는 함수다. 쓰는 일은 소리와 같은 시각에 일어난다. 통역을 거치지 말고, 소리를 받는 쪽이 직접 받으라는 식의 해결이었다. 16k 를 그 자리에서 바로 요청하니, 리샘플이 끼어들 자리도 없었다. 같은 마이크, 같은 16k 였는데 받는 길만 달라졌고, 그날 기록에 남은 건 '전체 체인 검증 완료'라는 한 줄이다. 고친 뒤의 길이를 다시 재서 적어두지는 않았다. ffmpeg 은 명령 하나로 모든 걸 해주는 도구지만, 그 대가를 녹음이 치렀다. 처음부터 이렇게 했더라면 어땠을까 싶을 정도로, 해결책 자체는 단순했다. 그다음부터 녹음기는 이 방법을 쓴다.

알림이 죽어 있었다

같은 날 고친 게 하나 더 있었다. 알림이었다. 고치는 데는 한 줄이면 충분한, 그런 작은 부분이었다. 그 알림이, 이 녹음기의 마지막 고비였다. 녹음을 끝내면 메뉴바 앱이 알림을 하나 띄우는데, 처음엔 rumps 가 주는 notification 함수를 쓰고 있었다. 그런데 이 함수는 앱 번들로 묶인 프로그램에서만 제대로 동작한다. 이 녹음기는 그냥 파이썬 스크립트라 번들이 아니었고, 알림을 부르는 순간 CFBundleIdentifier 오류를 내고 죽었다.

 

죽는 것 자체는 그럴 수 있다. 알림 하나가 죽는 걸로 끝났다면 그냥 고치면 됐을 텐데. 문제는 죽은 위치였다. 알림은 녹음을 끝내는 stop 로직 안에 있었고, 그 뒤에는 전사 트리거가 기다리고 있었다. 알림이 죽는 순간, 그 뒤의 전사도 같이 죽었다. 녹음을 끝내도 전사가 시작되지 않는, 반쪽짜리 동작이었다. 고장이 소리 없이 지나가는 건 자동화에서 가장 무서운 일이다. 지금 생각해도 웃긴 게, 이 크래시는 소리와는 아무 상관이 없었다. 샘플이 사라지는 문제와 전사가 시작되지 않는 문제가 같은 교훈 블록에 나란히 적혀 있는데, 어느 쪽을 먼저 알아챘는지는 적어두지 않았다.

 

 

알림은 osascript 의 display notification 으로 바꿨다. 맥의 알림 센터에 그대로 뜨는 시스템 명령으로, 번들 여부와 상관없이 동작한다. 셸 명령이라 파이썬에서 그냥 불러 쓰면 되고 알림이 죽을 일이 없어졌다. 몇 줄짜리 교체였지만, 전사 트리거가 다시 살아나는 건 이 한 줄에서 시작됐다. 이제 녹음을 끝내면 알림이 뜨고, 10분 안에 전사가 받고, 15분 안에 정리가 붙는다.

뚜껑을 닫으면

녹음기가 가진 또 하나의 트리거는 뚜껑이었다. NSWorkspaceWillSleep 이라는, 맥이 잠들기 직전에 알려주는 시그널에 저장을 걸었다. 그 타이밍이, 저장의 마지막 기회였다. 회의가 끝나고 노트북 뚜껑을 닫으면, 그 순간 녹음이 확정된다. 뚜껑이 닫히는 순간은 곧 회의가 끝난 순간이라, 저장 시점으로도 자연스러웠다. 저장은 잠들기 직전에 끝나고, 잠든 뒤에는 할 일이 없었다. 회의가 길어지면 멈추는 버튼은 잘 잊히는데, 자리에서 일어나 뚜껑을 안 닫고 나가는 일은 거의 없어서, 기억에 맡긴 버튼과 달리 습관이 저절로 하는 동작을 트리거로 쓰는 것, 그게 이 녹음기에서 가장 쓸모있던 설계였다.

 

만든 지 하루 만에, 녹음기는 세 번 고쳐졌다. 하나는 소리를, 하나는 옵션을, 하나는 알림을. 셋 다 같은 밤에 일어났다. 그날 밤 기록에는 '전체 체인 검증 완료'라는 짧은 한 줄이 남았다. 그 한 줄에, 그 밤의 전부가 들어 있었다. 메뉴앱의 녹음에서 시작해 전사와 정리를 거쳐 회의록 폴더까지, 한 번에 흐르는 것이 확인됐다. 회의가 끝나면 뚜껑을 닫는 것만으로 파일이 볼트로 흘러 들어가고, 뒤의 작업들이 순서대로 깨어난다. 버튼 하나와 뚜껑 하나가, 하루의 회의를 브레인으로 옮겼다. 기록에는 'ffmpeg avfoundation 캡처는 사용 금지'라는 한 줄도 함께 남겼는데, 나중에 같은 실패를 반복하지 않으려고 결론을 문서에 박아둔 것이다. 회의는 더 이상 손으로 챙길 일이 아니었다. 이제는 뚜껑을 닫으면, 뒤는 기계의 몫이었다. 열한 개가 된 파이프라인의 첫 관문은, 이렇게 해서 제자리를 찾았다. 전사는 전사대로 또 다른 싸움을 준비하고 있었는데, 그 이야기는 다음 편에서.