소리가 0.15초 앞서 있었다 · v1.0.0.1
시험이 「전부 통과」인데도 결함은 있었다. 통과한 것은 길이였지 맞춤이 아니었다. 스스로 점검해 찾은 여섯 가지와 그 고침.
1판 코드를 처음부터 다시 읽어 시험이 못 잡은 결함을 찾아 고친다. 그리고 exe 를 클라우드에서 만들어 판마다 함께 낸다.
- 값이 어디서 와서 어디로 가는지 따라가며 코드 점검
- 결함은 수치로 재현한 뒤 고치고, 그 수치를 재는 시험을 붙임
- 보완(단축키·영역·출력 크기·커서)은 고친 뒤 시험
- wine 에 Windows Python 을 올려 PyInstaller 빌드
- 빌드된 exe 로 실제 녹화
- ☑결함 1: 소리가 영상보다 0.15초 앞섬 → atrim·adelay, 섬광·삐 소리 시험으로 1.000초 일치 확인
- ☑결함 2: 표식 시각이 파일 시각과 다름 → 첫 프레임 기준으로 보정
- ☑결함 3: 합치는 중 재시작하면 세션을 잃음 → finalizing 상태로 차단
- ☑단축키 Ctrl+Alt 로 변경, settings.json 에서 수정 가능
- ☑영역 화면 밖 잘라내기 + 미리보기
- ☑출력 크기 50·75%, 마우스 커서 덧그리기
- ☑exe 189MB 클라우드 빌드(icuuc.dll 대역, numpy 2.0.2, ffmpeg 한 벌)
- ☑Windows Python 으로 enginetest·realtest 전부 통과, exe 로 5.93초 녹화
- ☐실제 Windows 에서 loopback 장치·배율 화면 확인
1. 점검 방법
1판의 시험은 「결과 파일이 나오는가, 길이가 맞는가」를 봤다. 이번에는 코드를 처음부터 다시 읽으면서 「이 값이 어디서 와서 어디로 가는가」를 따라갔다. 찾은 것은 결함 셋, 보완 넷이다. 결함은 수치로 재현한 뒤 고쳤고, 보완은 고친 뒤 시험을 붙였다.
| # | 구분 | 내용 | 재현·확인 |
|---|---|---|---|
| 1 | 결함 | 소리가 영상보다 0.15초 앞서 있음 | enginetest [4] 섬광·삐 소리 시각 비교 |
| 2 | 결함 | 표식 시각이 파일 시각과 다름 | enginetest [6] |
| 3 | 결함 | 합치는 중 단축키로 시작하면 새 세션이 화면에서 사라짐 | realtest「합치는 중엔 새로 시작 안 됨」 |
| 4 | 보완 | Ctrl+Shift+R 이 브라우저 강력 새로고침과 겹침 | Ctrl+Alt+R/P/M, 설정으로 변경 |
| 5 | 보완 | 영역이 화면 밖으로 나가면 mss 예외. 배율 어긋남을 알 길 없음 | 잘라내기 + 미리보기, realtest |
| 6 | 보완 | 4K 화면을 그대로 넣으면 인코더가 못 따라감 | 출력 크기 50·75%, enginetest [5] |
| 7 | 보완 | 커서가 안 찍힘 | 화살표 덧그리기, enginetest [5] |
2. 결함 1. 소리가 0.15초 앞서 있었다
1판의 _audio_parts 는 소리 시작이 영상보다 늦은 경우(양수)만 -itsoffset 으로 처리하고, 이른 경우(음수)는 0으로 버렸다. 그런데 세션은 소리 장치를 먼저 열고 그 다음에 ffmpeg 를 띄운다. 소리가 늘 먼저다. 재 보니 이렇다.
t_start->video 0.175
t_start->mic 0.025
mic - video -0.150 (음수 = 소리가 먼저. 1판은 이 값을 버렸다)
결과 파일에서 소리는 영상보다 0.15초 앞서 재생된다. 1판의 시험이 이것을 못 잡은 이유는 「길이」만 쟀기 때문이다. 길이는 -shortest 가 맞춰 주니 늘 통과한다.
고침
세 가지를 바꿨다. 첫째, 음수면 소리 앞머리를 그만큼 잘라낸다(atrim). 둘째, 양수면 -itsoffset 대신 adelay 를 쓴다. amix 는 늦게 시작하는 입력 앞을 무음으로 채워 주지 않으므로 -itsoffset 은 섞을 때 어긋난다. 셋째, 결과 길이는 영상 길이로 확정한다. 영상 프레임 수 ÷ FPS 를 알고 있으니 apad=whole_dur 로 채우고 atrim=end 로 자른다.
def _audio_chain(idx, off, tag):
chain = f"[{idx}:a]aresample=48000"
if off > 0.0005: chain += f",adelay={int(round(off * 1000))}:all=1"
elif off < -0.0005: chain += f",atrim=start={-off:.3f},asetpts=PTS-STARTPTS"
return chain + f"[{tag}]"
fit = f",apad=whole_dur={duration:.3f},atrim=end={duration:.3f}"
중간에 겪은 것: ffmpeg 가 끝나지 않는다
처음엔 apad(무한) + -shortest 로 했다. 영상을 다시 인코딩할 때는 되는 조합인데 -c:v copy 와 함께 쓰니 ffmpeg 가 끝나지 않았다. 2분 기다리다 죽였다. 영상 길이를 숫자로 알고 있으니 whole_dur 로 끊는 쪽이 애초에 맞다.
시험
길이가 아니라 「맞춤」을 재는 시험을 넣었다. 1.0초에 흰 프레임 3장이 있는 영상과, 소리 시계로 1.15초에 삐 소리가 있는 WAV(0.15초 일찍 켜진 상황)를 만들어 합친 뒤, 결과에서 삐 소리의 시작과 섬광의 시작을 따로 잰다.
[4] 영상·소리 맞춤 (소리가 0.15초 먼저 시작)
삐 시작 1.000s / 섬광 시작 1.00s / 길이 2.00s
ok 삐 소리가 1.00초에
ok 섬광이 1.00초에
ok 길이 = 영상 길이 2.0
3. 결함 2. 표식 시각
표식은 세션 시계(_t_start)로 쟀다. 파일의 0초는 첫 프레임을 보낸 순간이라 그보다 0.02~0.5초 뒤다. 플레이어에서 표식 시각으로 가면 그만큼 앞이다. 마무리할 때 media_start() − _t_start 만큼 빼서 적는다. 녹음 모드는 가장 먼저 블록이 온 시각이 기준이다. 두 소리 원천의 시작 차이도 같은 기준으로 adelay 한다.
4. 결함 3. 합치는 중에 다시 시작
정지를 누르면 화면의 단추는 잠기지만 단축키는 살아 있다. 그 사이에 Ctrl+Shift+R 을 누르면 session.running 이 이미 False 라 새 세션이 만들어지고, 조금 뒤 앞 세션의 마무리 신호가 self.session = None 을 해 버린다. 새 세션은 돌고 있는데 화면은 그것을 잊는다. 정지도 못 한다.
finalizing 을 따로 두고, 그것이 남아 있으면 시작을 거부하고 상태줄에 이유를 쓴다. 마무리 신호는 자기 세션이 아닌 것을 지우지 않는다. realtest 에서 정지 직후 start() 를 다시 불러 확인한다.
5. 보완
5-1. 단축키
Ctrl+Shift+R 은 Chrome·Edge 의 강력 새로고침이다. 전역 단축키는 다른 프로그램의 것을 빼앗으므로, 브라우저에서 새로고침을 누를 때마다 녹화가 켜진다. Ctrl+Alt+R/P/M 으로 바꾸고 settings.json 에서 고칠 수 있게 했다. "F9", "Ctrl+Shift+F12" 같은 글을 읽어 RegisterHotKey 의 modifier·vk 로 바꾸는 parse() 를 넣었다.
5-2. 영역: 잘라내기와 미리보기
영역을 화면 밖까지 끌면 mss 가 예외를 낸다. 가상 데스크톱 사각형으로 잘라낸다. 그리고 고른 직후 그 자리를 한 번 찍어 창 안에 작은 그림으로 보여 준다. Windows 배율 125%·150% 에서 좌표가 어긋나면 이 그림이 어긋난 자리를 보여 줄 것이다. 배율 문제를 클라우드에서는 재현할 수 없으니, 사용자가 눈으로 확인할 자리를 만든 것이다.

5-3. 출력 크기와 커서
4K 화면은 프레임 하나가 33MB 다. 30fps 면 초당 1GB 를 파이프로 넘기고 libx264 가 그것을 따라가야 한다. 「출력 크기 50·75%」를 두어 ffmpeg 의 scale 로 줄인다. 커서는 mss 가 찍지 않으므로 Windows GetCursorPos 위치에 화살표 하나를 numpy 로 덧그린다. 실제 커서 그림을 뽑는 대신 흰 테두리·검은 속의 화살표 하나로 통일했다.

6. 그 밖에 고친 것
- 일시정지 표시의 ⏸ 글리프가 Consolas 에 없어 네모로 나옴 → 숫자 색을 회색으로
- 품질 콤보가 잘려 「보통 (CRF」까지만 보임 → 화질 묶음을 두 줄로
- 녹화 모드와 녹음 모드가 장치 선택을 한 자리에 저장해 모드를 바꾸면 잃음 → 따로 저장
- 창을 닫으면 트레이로 내려가는데 녹화가 계속되는지 알 길 없음 → 트레이 알림 한 번
- 시험 스크립트가 사용자의 설정 파일(저장 폴더)을 덮어씀 → 시험은 임시 설정 파일로
- capture.py 의
processEvents()가 다음 단계 타이머를 먼저 돌려 한 단계 앞선 화면이 찍힘 → 제거
7. 시험 결과
enginetest [1] 파일 이름 · [2] 녹화 · [3] 녹음 · [4] 맞춤 · [5] 커서·크기 · [6] 표식 전부 통과
realtest 화면 밖 영역 잘라냄 · 미리보기 · 시작 · 표식 · 일시정지 · 재개 · 합치는 중 재시작 차단
· 길이 = 경과 시간 · 정지 화면 서로 다름 · 세션 정리 전부 통과

8. exe 를 클라우드에서 만들다
1판은 exe 를 사용자 PC 의 run0.bat 에 맡겼다. 이번에는 클라우드에서 만들어 넣는다. wine 9.0 에 64비트 Windows Python 3.11.9(NuGet 배포본)를 올렸다. 그 위에 PySide6·mss·sounddevice·numpy·imageio-ffmpeg·PyInstaller 를 Windows 휠로 설치했다. 막힌 곳이 둘이었다.
8-1. icuuc.dll 이 없다
Qt6Core.dll 이 icuuc.dll 을 정적으로 끌어온다. Windows 10 부터 System32 에 들어 있는 시스템 ICU 라 실제 PC 에서는 문제가 없지만 wine 에는 없다. PyInstaller 의 PySide6 훅은 QtCore 를 실제로 import 해 경로를 묻기 때문에 이것이 안 뜨면 빌드가 깨진다. Qt6Core 가 쓰는 함수를 objdump 로 세어 보니 ucnv_* 스무 개다. 그 이름만 내보내고 전부 「없음」을 돌려주는 대역 DLL 을 mingw 로 만들어 wine 의 system32 에 넣었다. Qt 는 ICU 코덱이 없으면 자체 코덱으로 간다.
API void* ucnv_open(const char* n, UErrorCode* e){ if(e) *e = 4; return NULL; }
API int ucnv_countAvailable(void){ return 0; }
... /* 20개, 빌드 전용. 배포본에는 안 들어간다 */
8-2. numpy 2.4 가 wine 에서 안 뜬다
ucrtbase.dll.crealf 가 wine 9.0 에 구현돼 있지 않다. 빌드 환경만 numpy 2.0.2 로 내렸다. 프로그램의 요구는 1.26 이상이라 상관없다.
8-3. 두 벌 들어간 ffmpeg
첫 빌드가 273MB 였다. PyInstaller 훅이 imageio-ffmpeg 의 바이너리(84MB)를 넣고, build.bat 이 exe 옆에 또 복사했다. 스펙에서 훅이 넣는 것을 빼고 _internal\ffmpeg.exe 한 벌만 두었다. 안 쓰는 Qt 파일(소프트웨어 OpenGL 20MB, 번역 6.7MB)도 뺐다. 189MB.
8-4. 한글 파일 이름
Windows Python 으로 enginetest 를 돌리니 회의.mic.tmp.wav 를 못 열었다. wine 의 win32 파일계통이 한글 경로를 못 받는 것이라 실제 Windows 에서는 없는 일이지만, 덕분에 구조를 고쳤다. 임시 파일은 gr_시각_pid.* 같은 ASCII 이름으로 쓰고 마지막에 os.replace 로 최종 이름을 준다. 그것마저 안 되면 주석을 뺀 이름으로 물러선다. ffmpeg 와 wave 에 한글 경로를 직접 주는 자리가 없어졌다.
8-5. exe 로 실제 녹화
Windows Python 으로 enginetest 6묶음과 realtest 20항목이 전부 통과했다. 빌드된 exe 는 가상 화면에 띄워 xdotool 로 시작·커서 이동·표식·정지를 눌러 확인했다. 5.93초 MP4 와 표식 파일이 나왔고, 정지 화면에 덧그린 커서 화살표가 찍혀 있다.

9. 남은 일
- Windows 실기: exe 를 바로 실행. WASAPI loopback 장치가 잡히는지, 배율 화면의 영역 미리보기가 맞는지. 다시 빌드하려면 run1.bat
- 커서 위치는 Windows GetCursorPos 다. wine 에서는 잡혔고, 실제 PC 의 배율 화면에서 자리가 맞는지 본다.