화면 녹화는 반디캠, 회의·강의 녹음은 Windows 녹음기나 스마트폰 앱을 썼다. 반디캠은 유료이고 무료판은 워터마크가 붙는다. OBS 는 무료이지만 방송용이라 장면·소스·믹서를 먼저 이해해야 단추가 보인다. Windows 녹음기는 표식 기능이 앱 안에 갇혀 있어 파일을 다른 곳으로 옮기면 표식이 사라진다.
세 프로그램에 공통으로 없는 것이 두 가지다. 첫째, 파일 이름 규칙을 내가 정할 수 없다. 둘째, 녹음 중 「지금 이 자리」를 남기고 그 목록을 파일 곁에 텍스트로 두는 기능이 없다. 이 둘이 GenealogyKSIC 에서 정한 「0000.00.00., 00.00., 주석.확장자」 규칙과 회의 전사 작업에 필요한 것이라, 직접 만들기로 했다. 이름은 GenealogyRecording.
| 항목 | 반디캠 | OBS | Windows 녹음기 | GenealogyRecording |
|---|---|---|---|---|
| 라이선스 | 유료(무료판 워터마크) | 무료(GPL) | 무료 | 자체 제작 |
| 녹화+녹음 한 프로그램 | 녹화 위주 | 녹화 위주 | 녹음만 | 모드 전환 |
| 표식(marker) | 없음 | 없음 | 앱 내부 | 파일 곁 txt·csv |
| 파일 이름 | 날짜 고정 | 날짜 고정 | 「녹음 (1)」 | 날짜.시각.주석 |
| 내부 수정 | 불가 | C++ | 불가 | Python, 판올림 기록 |
1판(v1.0.0.0)의 범위는 다음과 같다.
이름.표식.txt 와 .csv 로 남음넣지 않은 것: 마우스 커서 그리기, 특정 창 따라가기, 웹캠 덧붙이기, 전사. 커서는 캡처 라이브러리가 찍지 않아 따로 덧그려야 하고, 그것은 다음 판의 일이다.
GenealogyImage 와 같은 Python + PySide6 이다. 이유도 같다. 화면 캡처는 mss, 소리는 sounddevice(PortAudio), 인코딩·합치기·변환은 ffmpeg 다. ffmpeg 는 imageio-ffmpeg 패키지가 실행 파일을 동봉하므로 따로 내려받을 것이 없다.
src/genealogyrecording/
app.py QApplication, 팔레트, 아이콘(코드로 그림)
mainwindow.py 창. 모드 전환 · 설정 · 큰 단추 · 최근 결과
settings.py %APPDATA%\GenealogyRecording\settings.json
naming.py 0000.00.00., 00.00., 주석.확장자
screen.py mss 모니터 목록 · BGRA 프레임
videowriter.py 프레임 → ffmpeg 표준입력 → MP4
audio.py 장치 목록 · WASAPI loopback 탐지 · WAV 기록
session.py 한 번의 녹화를 통째로(시작·일시정지·표식·정지·마무리)
markers.py 표식 목록 · txt/csv
regionselect.py 화면 위 반투명 덮개에서 영역 끌기
hotkeys.py RegisterHotKey(Windows) / QShortcut(그 밖)
levelmeter.py 입력 레벨 막대
ffmpegtool.py ffmpeg 찾기 · mux · 변환
영상과 소리를 처음부터 한 ffmpeg 프로세스에 넣는 방법도 있다. 그러면 소리 장치가 ffmpeg 의 dshow 로 잡혀야 하는데, 장치 이름이 한국어 Windows 에서 깨지거나 시스템 소리(스테레오 믹스)가 기본으로 꺼져 있는 PC 가 많다. 그래서 갈랐다. 영상은 mss 프레임을 ffmpeg 표준입력으로 흘려 임시 MP4 로, 마이크와 시스템 소리는 각각 WAV 로, 정지하면 셋을 ffmpeg 로 합친다.
cmd = [exe, "-f", "rawvideo", "-pix_fmt", "bgra",
"-s", f"{w}x{h}", "-r", str(fps), "-i", "-",
"-an", "-c:v", "libx264", "-preset", "veryfast", "-crf", "23",
"-pix_fmt", "yuv420p", "-movflags", "+faststart", out]
캡처는 늦을 수 있다. 큰 화면, 느린 PC, 다른 프로그램이 CPU 를 쓰는 순간. 그때 프레임을 건너뛰면 영상이 실제 시간보다 짧아지고 소리와 어긋난다. 그래서 캡처 스레드는 시계를 보고, 놓친 박자 수만큼 같은 프레임을 다시 보낸다. 영상 길이 = 프레임 수 ÷ FPS 가 항상 실제 시간과 같아진다.
due = base + tick * period
if now < due: sleep(min(due - now, 0.05)); continue
frame = source.grab()
pipe.write(frame); tick += 1
behind = int((monotonic() - base) / period) - tick
for _ in range(min(behind, fps)): # 놓친 박자만큼 채움
pipe.write(frame); tick += 1
일시정지는 박자를 세지 않는다. 재개할 때 base 를 멈춘 시간만큼 뒤로 옮겨 시계를 잇는다.
영상 스레드와 소리 스레드는 같은 순간에 뜨지 않는다. PortAudio 장치가 열리는 데 0.1~0.5초가 든다. 각 스레드가 첫 프레임·첫 블록을 받은 monotonic() 을 적어 두고, 소리가 늦은 만큼 합칠 때 -itsoffset 으로 앞에 무음을 넣는다.
off = recorder.started_at - video.started_at
args += ["-itsoffset", f"{off:.3f}", "-i", wav]




위 그림은 클라우드의 가상 화면에서 가짜 원천으로 찍은 것이라 「화면 없음」「마이크 없음」이 보인다. 실제 PC 에서는 모니터와 장치 이름이 채워진다.
'Event' object is not callable첫 시험에서 정지 직후 스레드 join 이 죽었다. 원인은 이름 충돌이다. threading.Thread 는 내부에서 self._stop() 메서드를 쓴다. 내가 정지 신호를 self._stop = threading.Event() 로 두는 바람에 그 메서드를 덮어썼다. 스레드가 끝날 때 파이썬이 _stop() 을 부르고, Event 는 호출할 수 없으니 예외다. _stop_ev 로 바꿨다. 스레드를 상속할 때 밑줄 하나짜리 이름은 부모 것과 겹치는지 먼저 본다.
flush of closed file, 그리고 moov atom not found두 번째 시험에서 MP4 가 48바이트짜리 빈 파일로 남았다. ffmpeg 표준입력을 close() 한 뒤 communicate() 를 불렀는데, communicate() 는 입력이 없어도 stdin 을 한 번 더 flush 하려 든다. 이미 닫힌 파일이라 예외, 그 예외 처리에서 kill(), ffmpeg 가 moov 를 못 쓰고 죽음. 순서를 「stdin 닫기 → stderr 읽기 → wait」로 바꿨다.
p.stdin.close()
err = p.stderr.read()
p.wait(timeout=120)
마이크는 48000Hz, WASAPI loopback 은 장치 기본값(44100 인 경우가 많음)이다. WASAPI 공유 모드는 장치 기본 표본율만 받으므로 녹음 쪽은 장치 값을 따르게 했고, 합칠 때는 aresample=48000 을 각 입력에 먼저 걸고 amix 한다. 44100 과 48000 을 섞은 시험은 통과했다.
장치가 없는 곳에서도 엔진을 시험할 수 있어야 한다. FakeSource(색이 바뀌는 회색 프레임)와 FakeRecorder(440Hz 사인파를 실시간으로 흘림)를 두고 scripts/enginetest.py 가 세 묶음을 돈다.
[1] 파일 이름
ok 주석 있음 / 주석 없음 / 금지 문자 치환 / 중복 이름 (2)
[2] 녹화 모드 (가짜 화면 + 마이크 + 시스템 소리)
ok 결과 ok
2026.09.09., 09.57., 시험.mp4 streams=['video', 'audio'] dur=3.03
ok 영상+소리 트랙 / 길이 3초 안팎 (일시정지 1초 제외) / 세션 시간 3.00
ok 프레임 60 (중복 0) / 표식 txt / 임시 파일 정리 / 짝수 크기
[3] 녹음 모드 (mp3 + 표식)
ok 결과 ok
2026.09.09., 09.57., 회의.mp3 streams=['audio'] dur=2.04
ok mp3 / 길이 2초 안팎 / 표식 2개 / 표식 csv 3줄 / 원본 WAV 보존
전부 통과
「1.5초 녹화 → 1초 일시정지 → 1.5초 녹화」가 3.03초 영상으로 나온 것이 4-2 의 박자 규칙이 맞다는 증거다.
가짜 원천 시험은 엔진만 본다. 화면 캡처 경로(mss)와 창의 단추가 실제로 이어지는지는 따로 봐야 한다. scripts/realtest.py 는 0.1초마다 숫자가 오르는 「내용 창」을 띄우고, 프로그램 창의 「시작」「표식」「일시정지」「재개」「정지」 단추를 차례로 누른다. 마이크가 없는 PC 에서도 돌아야 하므로 소리는 끈다. 클라우드의 가상 X 화면(1280×800)에서 돌린 결과다.
ok 시작 단추로 세션 시작 mic: None system: None
ok 마이크 없으면 소리 트랙 없이 진행
ok 진짜 ScreenSource
ok 표식 단추 / 일시정지 단추 / 일시정지 중 프레임 안 늘어남 / 재개
ok 마무리 중엔 단추 잠김
2026.09.09., 10.41., 실기.mp4 streams=['video'] dur=5.10 elapsed=5.10
ok 길이 = 실제 녹화 시간(일시정지 제외)
ok 1초 간격 정지 화면 5장 이상 / 정지 화면이 서로 다름
전부 통과 (30fps × 5.10초 = 153 프레임, 인코딩된 프레임 153)

마이크가 없으면 소리 항목이 저절로 꺼지고 영상 트랙만 있는 MP4 가 나온다. 이 경우 ffmpeg 합치기 단계는 -c:v copy -an 으로 지나가므로 정지 뒤 기다림이 거의 없다.
run0.bat 한 번으로 setup → 시험 → exe 빌드 → 캡처 → 진짜 화면 단추 시험(realtest) → 실제 장치 3초 녹화. 결과는 docs\verify\run0.log[Loopback] 장치가 잡히는지, 안 잡히면 PyAudioWPatch 경로가 도는지