본문으로 건너뛰기

CatchCatch

녹화 프로그램과 녹음 프로그램을 하나로 · v1.0.0.0

녹화 프로그램을 만들고자 한다. 화면 녹화, 회의 녹음의 기능을 하나의 프로그램에서 구현하고자 한다. 프로그램 개발 초기부터 다른 소프트웨어와의 차별적 기능을 다루지는 않겠다. 그저 녹화·녹음·저장이 제대로 기능하는 프로그램을 만들고자 한다.

2026. 09. 09

목적

화면 녹화, 회의 녹음, 강의 녹음을 하나의 프로그램에서 수행할 수 있도록 프로그램을 만들고자 한다. 파일 이름 규칙과 버전표시(marker)은 사용자가 정한 규칙대로 남긴다. 이 판에서는 기본 기능만 다루고, 다른 프로그램에 없는 기능은 다음 판으로 미룬다.

계획

  1. 기존 녹화·녹음 프로그램과의 차이를 표로 정리
  2. Python + PySide6 로 구조 잡기(mss 캡처, sounddevice 녹음, ffmpeg 인코딩·합치기)
  3. 녹화 모드(모니터·영역), 녹음 모드(MP3·M4A·WAV), 버전표시, 전역 단축키, 트레이
  4. 장치 없이 도는 엔진 시험(가짜 화면·사인파)과 진짜 화면 시험
  5. 개발일지 1편과 캡처

체크리스트 6/8

항목 상태 비고
차별성 표와 설계 문서(DESIGN.md) 완료
녹화 모드: 모니터·영역 → MP4, 마이크·시스템 소리 동시 기록 완료
녹음 모드: MP3·M4A·WAV, 버전표시 txt·csv 완료
전역 단축키·트레이·파일 이름 규칙 완료
enginetest 3묶음 전부 통과 완료
realtest: 진짜 화면을 진짜 단추로 5.10초 녹화, 153프레임 완료
Windows exe 미완 PySide6 는 64비트 Windows 전용이라 이 판은 PC 의 run0.bat 에 맡김
시스템 소리(WASAPI loopback)·배율 화면 실기 미완 실제 PC 필요

1. 왜 만드는가

화면 녹화는 기존 녹화 프로그램, 회의·강의 녹음은 기존 녹음 프로그램이나 스마트폰 앱을 썼다. 기존 녹화 프로그램 가운데 유료인 것은 무료판에 워터마크가 붙는다. 무료인 것은 방송용이라 장면·소스·믹서를 먼저 이해해야 단추가 보인다. 기존 녹음 프로그램은 버전표시가 앱 내부 저장소에만 기록되어 파일을 다른 곳으로 옮기면 버전표시가 사라진다.

이 세 프로그램에 공통으로 없는 기능이 두 가지 있다. 첫째, 파일 이름 규칙을 사용자가 정할 수 없다. 둘째, 녹음 중에 지금 이 자리를 표시해 두고 그 목록을 파일 곁에 텍스트로 남기는 기능이 없다. 이 두 가지는 GenealogyKSIC 에서 정한 「연도(0000).월(00).일(00)., 시간(00).분(00)., 주석.확장자」 규칙과 회의 전사 작업에 필요하다. 따라서 직접 만들기로 하였고, 이름은 GenealogyRecording 으로 정하였다.

2. 무엇을 만드는가

항목 기존 녹화 프로그램 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 가 일시정지이며, 녹화가 시작되면 창은 트레이로 내려간다.

이 판에 넣지 않은 기능은 마우스 커서 그리기, 특정 창 따라가기, 웹캠 덧붙이기, 전사다. 커서는 캡처 라이브러리가 찍지 않으므로 따로 덧그려야 하며, 이것은 다음 판에서 다룬다.

3. 스택과 구조

언어와 화면 도구는 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 · 변환

4. 결정 세 가지

4-1. 프레임은 파이프로 보내고, 소리는 파일로 쓰고, 마지막에 합친다

영상과 소리를 처음부터 한 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]

4-2. 박자를 놓치면 직전 프레임을 다시 보낸다

화면이 크거나 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 를 멈춘 시간만큼 뒤로 옮겨 시계를 잇는다.

4-3. 시작 시각 차이는 itsoffset 으로

영상 스레드와 소리 스레드는 같은 순간에 뜨지 않는다. PortAudio 장치가 열리는 데 0.1~0.5초가 든다. 각 스레드가 첫 프레임·첫 블록을 받은 monotonic() 을 적어 두고, 소리가 늦은 만큼 합칠 때 -itsoffset 으로 앞에 무음을 넣는다.

off = recorder.started_at - video.started_at
args += ["-itsoffset", f"{off:.3f}", "-i", wav]

5. 화면

녹화 모드
녹화 모드. 원천(모니터·영역), 화질(FPS·CRF), 소리(마이크·시스템), 저장(폴더·주석). 아래에 레벨 미터, 시간, 시작 단추.
녹음 모드
녹음 모드. 형식은 MP3·M4A·WAV. 버전표시 설명이 형식 옆에 있다.
녹화 중
녹화 중. 설정은 잠기고, 버전표시 하나를 찍은 상태. 레벨 미터가 입력 크기를 표시한다.
정지 뒤
정지 뒤. 최근 결과 목록에 길이·파일 이름·버전표시 수. 두 번 누르면 연다.

위 그림은 클라우드의 가상 화면에서 가짜 원천으로 찍은 것이라 「화면 없음」「마이크 없음」이 보인다. 실제 PC 에서는 모니터와 장치 이름이 채워진다.

6. 디버깅

6-1. 'Event' object is not callable

첫 시험에서 정지 직후 스레드 join 이 죽었다. 원인은 이름 충돌이다. threading.Thread 는 내부에서 self._stop() 메서드를 쓴다. 정지 신호를 self._stop = threading.Event() 로 두는 바람에 그 메서드를 덮어썼다. 스레드가 끝날 때 파이썬이 _stop() 을 부르고, Event 는 호출할 수 없으니 예외다. _stop_ev 로 바꿨다. 스레드를 상속할 때 밑줄 하나짜리 이름은 부모 것과 겹치는지 먼저 본다.

6-2. 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)

6-3. 표본율이 다른 원천

마이크는 48000Hz, WASAPI loopback 은 장치 기본값(44100 인 경우가 많음)이다. WASAPI 공유 모드는 장치 기본 표본율만 받으므로 녹음 쪽은 장치 값을 따르게 했고, 합칠 때는 aresample=48000 을 각 입력에 먼저 걸고 amix 한다. 44100 과 48000 을 섞은 시험은 통과했다.

7. 시험

7-1. 장치 없이 엔진만

장치가 없는 곳에서도 엔진을 시험할 수 있어야 한다. 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 의 박자 규칙이 맞다는 증거다.

7-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 에서 1초 간격으로 뽑은 정지 화면(1·3·5초). 내용 창의 숫자가 0019 → 0039 → 0073 으로 오르고, 프로그램 창의 시계도 00:00:00 → 02 → 04 로 함께 찍혔다. 3초와 5초 사이에 숫자가 34 오른 것은 그 사이에 1초 일시정지가 들어 있기 때문이다.

마이크가 없으면 소리 항목이 저절로 꺼지고 영상 트랙만 있는 MP4 가 나온다. 이 경우 ffmpeg 합치기 단계는 -c:v copy -an 으로 지나가므로 정지 뒤 기다림이 거의 없다.

8. 남은 일

  • Windows 실기. run0.bat 을 한 번 실행하면 설치, 시험, exe 빌드, 캡처, 진짜 화면 단추 시험(realtest), 실제 장치 3초 녹화가 차례로 돈다. 결과는 docs/verify/run0.log 에 남는다.
  • 시스템 소리. PortAudio 19.7 의 [Loopback] 장치가 잡히는지, 안 잡히면 PyAudioWPatch 경로가 도는지 확인한다.
  • 다음 판에서는 마우스 커서 덧그리기를 넣고, 배율(125%·150%) 화면에서 영역 좌표가 맞는지 확인한다.
첨부파일 1개 · 1.8 MB

파일 이름을 누르면 내려받습니다.

ABOUT AUTHOR

shind

shind

소개글이 아직 없습니다.

댓글 남기기

Your email address will not be published. Required fields are marked *