화면 녹화는 기존 녹화 프로그램, 회의·강의 녹음은 기존 녹음 프로그램이나 스마트폰 앱을 썼다. 기존 녹화 프로그램 가운데 유료인 것은 무료판에 워터마크가 붙는다. 무료인 것은 방송용이라 장면·소스·믹서를 먼저 이해해야 단추가 보인다. 기존 녹음 프로그램은 버전표시가 앱 내부 저장소에만 기록되어 파일을 다른 곳으로 옮기면 버전표시가 사라진다.
이 세 프로그램에 공통으로 없는 기능이 두 가지 있다. 첫째, 파일 이름 규칙을 사용자가 정할 수 없다. 둘째, 녹음 중에 지금 이 자리를 표시해 두고 그 목록을 파일 곁에 텍스트로 남기는 기능이 없다. 이 두 가지는 GenealogyKSIC 에서 정한 「연도(0000).월(00).일(00)., 시간(00).분(00)., 주석.확장자」 규칙과 회의 전사 작업에 필요하다. 따라서 직접 만들기로 하였고, 이름은 GenealogyRecording 으로 정하였다.
| 항목 | 기존 녹화 프로그램 A (유료) | 기존 녹화 프로그램 B (무료) | 기존 녹음 프로그램 A (무료) | GenealogyRecording |
|---|---|---|---|---|
| 라이선스 | 유료 (무료판 워터마크) | 무료 (GPL) | 무료 | 무료 (자체 제작) |
| 화면 녹화 | O | O | X | O |
| 음성 녹음 | X | X | O | O |
| 버전표시(marker) | 없음 | 없음 | 앱 내부 | 파일 곁 txt·csv |
| 파일 이름 | 날짜 고정 | 날짜 고정 | 번호 붙은 고정 이름 | 날짜.시각.주석 |
| 내부 수정 | 불가 | C++ | 불가 | Python, 판올림 기록 |
1판(v1.0.0.0)의 범위는 다음과 같다. 녹화 모드는 모니터 전체, 특정 모니터, 끌어서 고른 영역을 MP4(H.264)로 기록하고, 마이크와 시스템 소리를 함께 담는다. 녹음 모드는 마이크 소리를 MP3·M4A·WAV 로 기록하며, 선택하면 시스템 소리도 함께 담는다. 버전표시는 Ctrl+Shift+M 으로 찍고 메모를 붙일 수 있으며, 결과 파일 곁에 이름.표식.txt 와 이름.표식.csv 로 남는다. 전역 단축키는 Ctrl+Shift+R 이 시작·정지, Ctrl+Shift+P 가 일시정지이며, 녹화가 시작되면 창은 트레이로 내려간다.
이 판에 넣지 않은 기능은 마우스 커서 그리기, 특정 창 따라가기, 웹캠 덧붙이기, 전사다. 커서는 캡처 라이브러리가 찍지 않으므로 따로 덧그려야 하며, 이것은 다음 판에서 다룬다.
언어와 화면 도구는 GenealogyImage 와 같이 Python 과 PySide6 를 쓴다. 선택한 이유도 GenealogyImage 와 같다. 화면 캡처는 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 을 한 번 실행하면 설치, 시험, exe 빌드, 캡처, 진짜 화면 단추 시험(realtest), 실제 장치 3초 녹화가 차례로 돈다. 결과는 docs/verify/run0.log 에 남는다.[Loopback] 장치가 잡히는지, 안 잡히면 PyAudioWPatch 경로가 도는지 확인한다.