이 글은 Today I Learn 시리즈의 70번째 기록입니다. (총 125개)

웹으로 만든 것을 설치형 데스크톱 앱으로 옮기기 시작한 날. 소리 지연을 줄이는 게 목적이었다.

옮길 것과 두고 갈 것

전부 옮기지 말고 이득이 나는 한 곳만 옮긴다

설치형으로 만든 이유는 하나였다. 앞선 TIL 4에서 잰 대로 브라우저에서는 건반을 누르고 소리가 나기까지 58밀리초가 걸려서 연주를 할 수가 없다. 그 지연만 없애면 됐다.

그러면 소리를 전부 다시 만들어야 할 것 같은데, 실제로 필요한 건 라이브 연주 한 경로뿐이었다. 파일을 틀어 보는 것과 영상으로 뽑는 것은 지연이 상관없고, 오히려 기존 방식에 단단히 묶여 있어서 건드리면 여러 곳이 깨진다.

그래서 라이브 연주가 소리를 내는 지점 딱 한 곳에서만 갈랐다. 그 한 줄 뒤로는 새 방식, 나머지는 전부 그대로다.

큰 전환을 할 때 “이왕 하는 김에 다 옮기자”가 자연스럽게 나온다. 그런데 옮기는 이유가 특정 문제 하나면, 그 문제가 생기는 지점만 옮기는 게 맞다. 갈래가 하나면 문제가 생겼을 때 어느 쪽 탓인지도 바로 안다.

화면 쪽은 아예 안 건드렸다. Tauri — 웹으로 만든 화면을 OS의 웹뷰(앱 안에 넣는 브라우저 화면)에 그대로 담아 설치형 앱으로 감싸는 프레임워크 — 를 쓴 덕에, 이미 만든 것을 다시 만들 필요가 없었다. 웹과 앱을 둘 다 낼 계획이면 처음부터 이 구조를 염두에 두는 게 이득이 크다.

실시간으로 도는 자리에는 기다릴 수 있는 것을 두지 않는다

소리를 직접 내보내는 자리(오디오 콜백 — 장치가 “다음 소리 조각 내놔” 하고 주기적으로 부르는 함수)는 정해진 시간 안에 반드시 채워 줘야 한다. 늦으면 소리가 끊긴다.

그래서 그 안에서는 “차례를 기다리는” 일을 하면 안 된다. 준비가 필요한 것은 전부 미리 해 두고, 그 자리에서는 이미 준비된 것을 꺼내 쓰기만 한다.

정해진 주기로 도는 곳은 다 성질이 같다. 화면을 그리는 자리, 소리를 내는 자리, 실시간으로 응답해야 하는 자리. 여기에 “가끔 오래 걸리는 일”을 하나 넣으면 평소에는 멀쩡하다가 하필 중요한 순간에 끊긴다.


실시간과 결과물

실시간으로 찍으면 그때의 사정이 결과물에 박힌다

라이브 연주를 녹화하는 기능이 화면을 실시간으로 담는 방식이었다. 앞선 TIL 9에서 파일 재생 영상은 한 장씩 그리는 방식으로 바꿨는데, 라이브 연주 녹화에는 같은 문제가 남아 있었다. 그래서 연주 중에 컴퓨터가 잠깐 버벅이면 그 버벅임이 영상에 그대로 남는다. 연주는 잘했는데 영상이 못 쓰게 된다.

방식을 바꿨다. 녹화 중에는 화면을 안 찍고 연주 내용만 기록한다. 어떤 건반을 언제 눌렀고 언제 뗐는지만 쌓는다. 연주가 끝나면 그 기록으로 영상을 처음부터 새로 그린다.

이러면 연주 중 화면이 버벅여도 결과물은 매끄럽다. 미리보기 품질과 결과물 품질이 분리된 것이다.

“보이는 대로 저장한다”는 단순하고 확실해 보이지만, 보이는 것이 기기 사정에 좌우되는 순간 결과물도 같이 좌우된다. 결과물 품질을 지켜야 하는 기능이면 실시간 캡처를 피하고 다시 그리는 쪽을 봐야 한다.

결과가 아니라 과정을 기록하면 여러 형태로 낼 수 있다

연주를 기록해 두고 나니 뜻밖의 것이 따라왔다. 같은 기록으로 MIDI 파일도 뽑을 수 있었다. 음악 프로그램에서 열어 편집할 수 있는 형태다.

영상만 저장했으면 절대 못 했을 일이다. 영상은 이미 구워진 결과라 되돌릴 수가 없다.

무언가를 저장할 때 결과를 저장할지 과정을 저장할지 정하게 된다. 과정을 저장하면 용량이 훨씬 작고 나중에 다른 형태로 낼 수 있는데, 대신 그걸 결과로 만드는 코드를 계속 유지해야 한다. 결과물 형태가 앞으로 늘어날 것 같으면 과정 쪽이 맞다.

같은 기록에서 나가는 두 형태의 성격도 달랐다. 영상에는 서스테인 페달(밟는 동안 소리를 유지하는 페달)을 밟은 효과를 음 길이로 녹여 넣어야 하고, MIDI에는 페달을 페달 이벤트 그대로 남겨야 한다. 받는 쪽이 해석할 수 있으면 원본 정보를 남기고, 못 하면 구워 넣는다.


원인을 찾는 자리

바깥부터 의심하면 로그 안의 단서를 늦게 본다

오디오 인터페이스(악기·마이크를 컴퓨터에 연결하는 전용 장비)의 ASIO 드라이버를 쓰면 지연을 더 줄일 수 있는데, 붙여 보니 그 장비가 목록에 안 나타났다. 다른 음악 프로그램에서는 잘 쓰는 장비였다.

처음 의심한 것은 전부 바깥이었다. 장비 설정, 다른 프로그램이 붙잡고 있는지, 드라이버 버전. 실제 원인은 안쪽이었다 — 장비 목록을 훑는 코드가 목록의 모든 장비를 한꺼번에 붙잡고 있었고, 그 방식은 한 번에 하나만 붙잡을 수 있는 것이었다. 첫 번째를 쥔 채로 두 번째를 열려니 조용히 실패했다.

로그에는 “하나만 잡혔다”가 처음부터 찍혀 있었다. 바깥을 의심하는 동안 그 줄을 “장비가 하나뿐이구나”로 읽었지, “우리가 하나밖에 못 잡고 있구나”로 읽지 않았다.

바깥을 의심하는 게 틀린 건 아닌데, 그 상태로 로그를 보면 로그가 바깥 얘기로 읽힌다. 안 될 때 최소한 한 번은 “우리가 뭘 잘못 쥐고 있나”로 가정을 뒤집고 같은 로그를 다시 봐야 한다.

장비를 붙이고 나서 알게 된 제약도 있다. ASIO는 한 프로그램이 장비를 독점한다. 음악 프로그램을 켜 둔 채로는 우리 앱이 그 장비를 못 쓴다. 성능을 얻는 대신 동시 사용을 잃는 거래다.

같은 일을 두 곳에서 시작할 수 있으면 진행 표시가 안 붙는다

시작할 때 피아노 소리를 불러오는 몇 초 동안 진행 막대를 띄우려 했는데 아무것도 안 찼다.

불러오는 일이 두 군데서 시작되고 있었기 때문이다. 한쪽은 진행 상황을 알려 주게 돼 있었고 다른 쪽은 아니었는데, 알려 주지 않는 쪽이 먼저 시작해 버렸다. 결과는 공유되니 나중에 부른 쪽은 그냥 기다리기만 한다.

시작 지점을 하나로 모으니 그제야 진행률이 들어왔다.

진행 표시가 안 붙을 때는 표시하는 코드보다 “이 일을 누가 시작하나”를 세어 보는 게 빠르다. 둘 이상이면 먼저 시작한 쪽이 이기고, 그쪽에 표시가 안 달려 있으면 표시는 영영 안 붙는다.


손잡이 하나로 둘을 밀당할 때

목표가 둘이면 손잡이도 둘이어야 한다

세게 친 것과 약하게 친 것의 차이를 키우려고 velocity(건반을 얼마나 세게 눌렀는지) 곡선을 눌렀더니, 중간 세기가 너무 조용해져서 제대로 된 소리를 내려면 건반을 세게 쳐야 했다. 되돌리면 이번엔 차이가 안 난다.

원인은 하나의 곡선으로 두 가지를 동시에 맞추려 한 것이었다. 하나는 “세기 차이가 얼마나 벌어지나”고 다른 하나는 “전체가 얼마나 큰가”다. 곡선 하나로는 둘 중 하나만 맞출 수 있다.

차이는 곡선으로 정하고 전체 크기는 볼륨 조절로 빼니 둘 다 맞았다.

한 값을 올렸다 내렸다 하며 두 가지를 맞추려 하고 있으면, 그건 손잡이가 하나 부족하다는 신호다.


요약

  • 옮기는 이유가 문제 하나면 그 문제가 생기는 지점만 옮긴다 — 갈래가 하나면 원인도 바로 안다
  • 정해진 주기로 도는 자리에는 가끔 오래 걸리는 일을 두지 않는다
  • 실시간으로 찍으면 그때의 기기 사정이 결과물에 박힌다 — 다시 그리면 분리된다
  • 결과 대신 과정을 기록하면 나중에 다른 형태로도 낼 수 있다
  • 안 될 때 한 번은 “우리가 뭘 잘못 쥐고 있나”로 뒤집어 같은 로그를 다시 본다
  • 진행 표시가 안 붙으면 그 일을 시작하는 곳이 몇 군데인지 세어 본다
  • 한 값으로 두 가지를 맞추려 하고 있으면 손잡이가 하나 부족한 것이다

Series: Today I Learn

1 C++ 자료형(Data Type) 2 MD5 vs pHash 3 C++에서 함수의 선언과 정의 4 Tkinter padx, pady 5 메모리와 포인터 변수 6 Call by Value, Call by Reference, Call by Pointer 비교 7 const 8 Gemfile — Jekyll 프로젝트의 의존성 파일 9 kramdown-parser-gfm — Jekyll의 GFM 파서 10 파서(Parser) 11 AHU vs OHU 12 I might try it vs I'll try it 뉘앙스 차이 13 SESSION_EXPIRE_AT_BROWSER_CLOSE=True 14 configuration key 15 Git stash vs discard 16 subprocess.Popen으로 Windows 탐색기에 명령어를 전달 17 Post 잔디 분석하기 18 Google Sheets Sync 최적화 19 DSL (Domain Specific Language)과 GPL (General Purpose Language) 20 마크다운 표 그리는 방법 21 쿼리 파라미터(Query Parameter). 기존 QR코드 재활용 22 Django 보안 취약점 점검 및 수정 23 OOP Object-Oriented Programming 객체 지향 프로그래밍 24 Fernet 대칭 암호화 25 Jekyll 코드블록 안의 Liquid 태그 26 insertOnConflictUpdate vs DoUpdate(target) 27 세션 필터 28 아코디언(Accordiaon) UI를 펼친상태로 만들기 29 input의 step 30 Word Cloud 31 Google Sheets를 데이터 버스로(with AppSheet) 32 Django 모델 텍스트 필드 자동 수집 패턴 33 localStorage로 섹션 토글 상태 유지 34 순차 ID 생성(`select_for_update()` + `max()` 조합) 35 역참조 검색과 distinct() 36 xlsx 다운로드와 로딩 오버레이 충돌 37 Android 파일 공유 MIME 타입 38 AssetManifest — Flutter 빌드 타임 asset 목록 런타임 조회 39 UTF-8 BOM과 PowerShell 파일 쓰기 40 소리꽃 KeyBloom TIL 1 41 소리꽃 KeyBloom TIL 2 42 소리꽃 KeyBloom TIL 3 43 메트로놈 Simple Metronome TIL 1 44 소리꽃 KeyBloom TIL 4 45 메트로놈 Simple Metronome TIL 2 46 소리꽃 KeyBloom TIL 5 47 메트로놈 Simple Metronome TIL 3 48 정적 블로그 SEO 정비와 Pagefind 검색 도입 49 메트로놈 Simple Metronome TIL 4 50 안드로이드 AudioTrack 연속 재생, 실측 피커 정렬, 카메라 토치 플래시 51 메트로놈 Simple Metronome TIL 5 52 모래게임 Sandrop TIL 1 53 메트로놈 Simple Metronome TIL 6 54 모래게임 Sandrop TIL 2 55 모래게임 Sandrop TIL 3 56 메트로놈 Simple Metronome TIL 7 57 모래게임 Sandrop TIL 4 58 모래게임 Sandrop TIL 5 59 모래게임 Sandrop TIL 6 60 소리꽃 KeyBloom TIL 6 61 모래게임 Sandrop TIL 7 62 모래게임 Sandrop TIL 8 63 소리꽃 KeyBloom TIL 7 64 모래게임 Sandrop TIL 9 65 소리꽃 KeyBloom TIL 8 66 모래게임 Sandrop TIL 10 67 소리꽃 KeyBloom TIL 9 68 소리꽃 KeyBloom TIL 10 69 소리꽃 KeyBloom TIL 11 70 소리꽃 KeyBloom TIL 12 읽는 중 71 소리꽃 KeyBloom TIL 13 72 온실 GreenHouse TIL 1 73 소리꽃 KeyBloom TIL 14 74 온실 GreenHouse TIL 2 75 소리꽃 KeyBloom TIL 15 76 소리꽃 KeyBloom TIL 16 77 소리꽃 KeyBloom TIL 17 78 소리꽃 KeyBloom TIL 18 79 소리꽃 KeyBloom TIL 19 80 소리꽃 KeyBloom TIL 20 81 로그스톤 상표 셀프 출원 TIL 1 82 소리꽃 KeyBloom TIL 21 83 소리꽃 KeyBloom TIL 22 84 소리꽃 KeyBloom TIL 23 85 소리꽃 KeyBloom TIL 24 86 MiniMacro TIL 1 87 Astro가 무엇인지, 왜 옮기는지 88 MiniMacro TIL 2 89 메트로놈 Simple Metronome TIL 8 90 메트로놈 Simple Metronome TIL 9 91 블로그 Astro 이관 TIL 92 오픈데이 Openday TIL 1 93 오픈데이 Openday TIL 2 94 Unreal Engine MCP TIL 1 95 콘티온 Conti On TIL 11 96 오픈데이 Openday TIL 3 97 오픈데이 Openday TIL 4 98 오픈데이 Openday TIL 5 99 ScorePlayer TIL 1 100 BuildingHub TIL 1 101 ScorePlayer TIL 2 102 BuildingHub TIL 2 103 ScorePlayer TIL 3 104 Unreal Engine MCP TIL 2 105 궁 미로 PalaceMaze TIL 1 106 메트로놈 Simple Metronome TIL 10 107 메트로놈 Simple Metronome TIL 11 108 오픈데이 Openday TIL 6 109 BuildingHub TIL 3 110 Obsidian vault에서 코드만 빼기 — directory junction 111 ScorePlayer TIL 4 112 오픈데이 Openday TIL 7 113 URL은 바뀔 수 있는 것에 묶지 않는다 114 도구는 기능이 아니라 내 일에 맞는지로 고른다 115 돌이키기 힘든 변경은 작게 먼저 확인한다 116 궁 미로 PalaceMaze TIL 3 117 궁 미로 PalaceMaze TIL 2 118 소리꽃 KeyBloom TIL 29 119 오픈데이 Openday TIL 8 120 BuildingHub TIL 4 121 ScorePlayer TIL 5 122 BuildingHub TIL 5 123 MiniMacro TIL 3 124 소리꽃 KeyBloom TIL 30 125 궁 미로 PalaceMaze TIL 4