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

소리를 붙인 날인데, 정작 코드를 짜는 Claude는 소리를 들을 수 없다. “됐다”고 말하려면 무엇을 근거로 삼을지부터 정해야 했고, 거기서 나온 판단들이다.


확인할 수 없는 결과물에는 확인할 수 있는 신호를 붙인다

화면은 한 장 뽑아서 보면 된다. 소리는 그럴 수가 없다. 그러면 “붙였다”는 말의 근거가 아무것도 없는 상태가 된다.

그래서 소리 자체 대신 소리가 나기 위해 참이어야 하는 것들을 신호로 만들었다.

  • 장치가 열렸나 — 안 열리면 조용한 게 정상이다. 이걸 모르면 버그와 환경을 구분 못 한다.
  • 시계가 실시간으로 도나 — 1.2초를 기다렸을 때 오디오 클럭(소리 장치가 소리를 가져간 만큼 흐르는 시간)이 1.21초를 가리키면 제 속도로 돌고 있다는 뜻이다.
  • 나가는 소리의 크기가 0이 아닌가 — 피크(출력 신호에서 가장 큰 진폭)가 0이면 시계만 돌고 소리는 안 실린 상태다. 이런 상태가 실제로 가능하다.

셋이 다 참이면 “안 들린다”의 후보가 음색·음량 문제로 좁혀진다. 그건 사람이 판단할 몫이다.

일반화하면 이렇다. 결과물을 직접 확인할 수 없을 때는 확인을 포기하지 말고, 결과물이 나오려면 참이어야 하는 중간 조건들을 값으로 뽑는다. 그러면 “모르겠다”가 “여기까지는 맞고 나머지는 사람이 본다”로 바뀐다.

이건 소리만의 이야기가 아니다. 남의 기기에서만 나는 문제, 특정 시간대에만 도는 작업, 눈으로 구분 안 되는 색 차이가 전부 같은 모양이다.


시간의 주인은 어긋남이 쌓이는 쪽에 둔다

앞선 TIL 1에서 기획 때 “재생 중 시간은 소리가 정한다”고 적어 뒀는데, 실제로 붙여 보니 이유가 더 분명해졌다.

한 프레임의 오차는 눈에 안 보인다. 문제는 그게 매 프레임 쌓인다는 것이다. 3분짜리 곡이면 만 번 넘게 더해지고, 끝에 가면 볼이 소리보다 눈에 띄게 앞서거나 뒤진다. 시작 부분만 보고 “맞네” 하면 놓친다.

그래서 실제로 내보낸 소리의 양(소리 장치에 넘긴 샘플 수를 샘플레이트 — 1초에 담는 소리 조각 수 — 로 나눈 값)을 경과 시간으로 삼는다. 화면이 몇 프레임을 건너뛰든 둘이 벌어지지 않는다.

가져갈 원리는 이것이다. 두 흐름을 맞춰야 할 때, 오차가 쌓이는 쪽을 기준으로 삼고 나머지가 따라가게 한다. 반대로 두면 처음엔 맞고 나중에 틀린다.


목적에 맞는 최소 품질로 시작한다

소리를 붙일 때 선택지가 둘이었다. 실제 피아노 음원(샘플 — 진짜 피아노를 건반마다 녹음해 둔 소리 파일)을 넣거나, 간단한 합성음(사인파에 배음(기본음 위에 겹치는 정수배 진동)을 조금 섞어 계산으로 만든 소리)으로 시작하거나.

이 소리의 목적은 감상이 아니라 “볼이 제 박자에 튀는지 귀로 확인하는 것”이다. 그 목적에는 합성음으로 충분하다. 음원을 넣으면 파일 용량과 라이선스 확인이 따라오고, 그건 배포를 생각할 때 다시 판단할 일이다.

판단 기준으로 삼을 것 — 기능을 붙일 때 “이게 무엇을 위한 것인가”를 먼저 좁히면 품질 수준이 자동으로 정해진다. 목적을 안 좁히면 처음부터 완성도를 올리다가 정작 확인해야 할 것을 못 한다.

미룬 것은 백로그에 이유와 함께 적어 뒀다. “나중에 더 좋게”가 아니라 “음원과 라이선스가 따라오므로 배포 판단과 같이”로 적어야 다음에 볼 때 판단이 선다.


없으면 없는 대로 돌아가게 둔다

소리 장치를 못 여는 환경이 있을 수 있다. 그때 앱이 멈추거나 재생이 안 되면 곤란하다.

그래서 장치가 없으면 소리 없이 화면 시간으로 돌게 뒀다. 평소라면 이런 대비를 미리 넣지 않는 편이 낫지만, 여기서는 판단이 달랐다 — 소리가 없는 상황에서는 소리와 그림이 어긋날 일도 없어서, 대체 동작이 정확히 맞는 답이 된다.

즉 대비를 넣을지 말지는 “그 상황에서 무엇이 맞는 동작인가”로 정한다. 답이 분명하면 넣고, 억지로 지어내야 하면 안 넣는다.


요약

  • 직접 확인할 수 없는 결과물은, 그것이 나오려면 참이어야 하는 조건들을 값으로 뽑는다
  • 두 흐름을 맞출 때는 오차가 쌓이는 쪽을 기준으로 삼는다. 반대로 두면 처음엔 맞고 끝에서 틀린다
  • 기능의 목적을 좁히면 필요한 품질 수준이 정해진다. 안 좁히면 확인해야 할 것을 못 한 채 완성도만 올린다
  • 미루는 것은 이유와 함께 적는다. “나중에 더 좋게”는 다음에 봐도 판단이 안 선다
  • 없는 상황의 대체 동작은 그게 정확히 맞는 답일 때만 넣는다

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