From fcba2b33c2ea1342a178cbd62a908012710dfe9e Mon Sep 17 00:00:00 2001 From: Pavel Machek Date: Fri, 26 Apr 2024 23:11:14 +0200 Subject: [PATCH] Add support files that will be used for video recording. --- medianame.h | 14 +++ meson.build | 22 +++++ movie.sh.in | 76 +++++++++++++++ movie_audio_rec.c | 93 ++++++++++++++++++ mpegize.py | 241 ++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 446 insertions(+) create mode 100644 medianame.h create mode 100755 movie.sh.in create mode 100644 movie_audio_rec.c create mode 100755 mpegize.py diff --git a/medianame.h b/medianame.h new file mode 100644 index 0000000..96d4863 --- /dev/null +++ b/medianame.h @@ -0,0 +1,14 @@ +#include + +static inline unsigned long long time_usec(void) +{ + struct timeval t; + gettimeofday(&t, NULL); + + return t.tv_sec * 1000000 + t.tv_usec; +} + +static inline void get_name(char *buf, char *dir, char *templ) +{ + sprintf(buf, "%s/%lld.%s", dir, time_usec(), templ); +} diff --git a/meson.build b/meson.build index 5711d5a..d63ceb7 100644 --- a/meson.build +++ b/meson.build @@ -68,6 +68,28 @@ executable('megapixels', install: true, link_args: '-Wl,-ldl') +executable('movie_audio_rec', + 'movie_audio_rec.c', + dependencies: [ dependency('libpulse-simple') ], + install : true, + install_dir: get_option('libexecdir') / 'megapixels/', +) + +install_data( + [ + 'mpegize.py' + ], + install_dir: get_option('libexecdir') / 'megapixels/', +) + +configure_file( + input: 'movie.sh.in', + output: 'movie.sh', + configuration: {'LIBEXECDIR': join_paths(get_option('prefix'), get_option('libexecdir')) / 'megapixels/'}, + install_dir: get_option('datadir') / 'megapixels/', + install_mode: 'rwxr-xr-x', +) + install_data( [ 'config/pine64,pinephone,rear.dcp', diff --git a/movie.sh.in b/movie.sh.in new file mode 100755 index 0000000..43d5ba8 --- /dev/null +++ b/movie.sh.in @@ -0,0 +1,76 @@ +#!/bin/bash +# Copyright 2022 Pavel Machek, GPLv2+ + +# needs sudo apt install dcraw + +jpegize() { + DNG_DIR="$1" + BURST_DIR="$GIGA_DIR/sm/" + mkdir $BURST_DIR + + DCRAW=dcraw + TIFF_EXT="tiff" + set -- + + CONVERT="convert" + + cd $DNG_DIR + I=0 + NUM=0 + for DNG in *.dng; do + NUM=$[$NUM+1] + done + + for DNG in *.dng; do + PERC=$[(100*$I)/$NUM] + echo $PERC + BASE=${DNG%%.dng} + # -w Use camera white balance + # +M use embedded color matrix + # -H 2 Recover highlights by blending them + # -o 1 Output in sRGB colorspace + # -q 0 Debayer with fast bi-linear interpolation + # -f Interpolate RGGB as four colors + # -T Output TIFF + ( + $DCRAW -w +M -H 2 -o 1 -q 0 -f -T "$DNG" + $CONVERT "$BASE.tiff" "$BASE.jpeg" + rm "$BASE.tiff" + mv "$BASE.jpeg" "$BURST_DIR/$BASE.jpeg.sv" + ) & + # dcraw -h -> half size -- fast! + # can do ppm output + I=$[$I+1] + if [ 0 == $[ $I % 16 ] ]; then + echo "Batch $I -- $PERC %" 1>&2 + wait + fi + done +} + +SCRIPT_DIR=$(cd $(dirname "${BASH_SOURCE[0]}") && pwd) +GIGA_DIR="$2" +DEST_NAME="$3" +FPS="$4" + +echo script_dir $SCRIPT_DIR 1>&2 +echo GIGA_DIR $GIGA_DIR 1>&2 +echo DEST_NAME $DEST_NAME 1>&2 +echo FPS $FPS dfps 1>&2 + +if [ "-$1" == "-start" ]; then + mkdir $GIGA_DIR/sm + cd $GIGA_DIR/sm + @LIBEXECDIR@/movie_audio_rec $FPS & + echo $! > $2/audio.pid +elif [ "-$1" == "-stop" ]; then + mkdir $GIGA_DIR/sm + kill `cat $2/audio.pid` + jpegize $2 # | zenity --progress "--text=Converting, phase 1, dng -> jpeg" --time-remaining + cd $GIGA_DIR/sm + @LIBEXECDIR@/mpegize.py convertall $GIGA_DIR/ $FPS + mv $GIGA_DIR/smo/*.mp4 $DEST_NAME + rm -r $GIGA_DIR +else + echo "Unrecognized command" +fi diff --git a/movie_audio_rec.c b/movie_audio_rec.c new file mode 100644 index 0000000..45c0dd2 --- /dev/null +++ b/movie_audio_rec.c @@ -0,0 +1,93 @@ +/* -*- c-file-style: "linux" -*- */ +/*** + This file is part of PulseAudio. + PulseAudio is free software; you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published + by the Free Software Foundation; either version 2.1 of the License, + or (at your option) any later version. + PulseAudio is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + You should have received a copy of the GNU Lesser General Public License + along with PulseAudio; if not, see . + + * Copyright 2022, 2024 Pavel Machek +***/ +#include +#include +#include +#include +#include +#include +#include +#include + +#include "medianame.h" + +/* gcc xx.c -o xx $(pkg-config --cflags --libs libpulse-simple) + */ + +int main(int argc, char*argv[]) { + int fps = 305; /* fps * 10 */ + /* 48000 * 2 * 2 bps, we want chunks corresponding to 30 fps */ + const uint32_t bufsize = ((48000 * 2 * 2 * 10) / fps); + + if (argc != 2) { + printf("usage: prog fps*10, run in recording directory\n"); + exit(1); + } + fps = atoi(argv[1]); + + /* The sample type to use */ + static const pa_sample_spec ss = { + .format = PA_SAMPLE_S16LE, + .rate = 48000, + .channels = 2 + }; + static pa_buffer_attr attr = { + .minreq = (uint32_t) -1, + .prebuf = (uint32_t) -1, + .tlength = (uint32_t) -1, + }; + pa_simple *r = NULL; + int ret = 1; + int error; + const pa_buffer_attr *p_attr = &attr; + int opt = 0; // | PA_STREAM_ADJUST_LATENCY; + uint8_t *buf = malloc(bufsize); + + attr.fragsize = bufsize; + attr.maxlength = bufsize; + + /* Create the recording stream */ + if (!(r = pa_simple_new(NULL, argv[0], PA_STREAM_RECORD | opt, NULL, "record", &ss, NULL, p_attr, &error))) { + fprintf(stderr, __FILE__": pa_simple_new() failed: %s\n", pa_strerror(error)); + goto finish; + } + + for (;;) { + char name[1024]; + int fd, res; + + /* Record some data ... */ + if (pa_simple_read(r, buf, bufsize, &error) < 0) { + fprintf(stderr, __FILE__": pa_simple_read() failed: %s\n", pa_strerror(error)); + goto finish; + } + + get_name(name, ".", "44800-s16le-stereo.sa"); + fd = open(name, O_WRONLY | O_CREAT | O_EXCL, 0666); + res = write(fd, buf, bufsize); + if (res != bufsize) { + fprintf(stderr, __FILE__": could not write samples: %m\n"); + goto finish; + } + close(fd); + } + +finish: + if (r) + pa_simple_free(r); + return ret; +} diff --git a/mpegize.py b/mpegize.py new file mode 100755 index 0000000..e68e1c2 --- /dev/null +++ b/mpegize.py @@ -0,0 +1,241 @@ +#!/usr/bin/python3 +# Copyright 2022 Pavel Machek, GPLv2+ + +import os, sys, time, copy, subprocess + +# https://stackoverflow.com/questions/11779490/how-to-add-a-new-audio-not-mixing-into-a-video-using-ffmpeg +# https://ottverse.com/create-video-from-images-using-ffmpeg/ + +# https://github.com/kkroening/ffmpeg-python/issues/95 + +# sudo apt install ffmpeg + +# Usage: mpegize convert +# head -c 1000 < /dev/zero > /tmp/delme.sm/1.foo.sa + +class Mpegize: + base = '/tmp/delme.' + fps = 30.5 + + def prepare(m): + m.source = m.base+'sm' + m.work = m.base+'smt' + m.output = m.base+'smo' + + def prepare_work(m): + m.prepare() + if not os.path.exists(m.output): + os.mkdir(m.output) + if not os.path.exists(m.work): + os.mkdir(m.work) + os.chdir(m.work) + os.system("rm *.jpeg output.*") + + def prepare_source(m): + m.prepare() + m.out_index = 0 + l = os.listdir(m.source) + print("Have", m.display_frames(len(l)), "frames") + l.sort() + m.frames = l + m.unused_frames = copy.deepcopy(l) + + def parse_frame(m, n): + if n[-5:] != ".mark" and n[-3:] != ".sa" and n[-3:] != ".sv": + return "", "", 0, + s = n.split(".") + i = int(s[0]) + return s[2], s[1], i + + def help(m): + print("mpegize command base-dir") + + def run(m, argv): + if len(argv) > 2: + m.base = argv[2] + mode = argv[1] + if mode == "stat" or mode == "convert" or mode == "gc" or mode == "convertall": + m.process(mode) + return + if mode == "gaps": + print("Video gaps") + m.stat_gaps("sv") + print("Audio gaps") + m.stat_gaps("sa") + return + if mode == "jpegize": + m.jpegize() + m.help() + + def stat_gaps(m, e): + m.prepare_source() + last = 0 + num = 0 + total = 0 + limit = 1000000 / m.fps + 15000 + for n in m.frames: + ext, mid, i = m.parse_frame(n) + if ext != e: + continue + if i - last > limit: + print("Gap at", i, (i - last) / 1000., "msec") + num += 1 + last = i + total += 1 + print("Total", num, "gaps of", total) + print("Expected", (1000000 / m.fps) / 1000., "msec, limit", limit / 1000., "msec") + + def process(m, mode): + m.prepare_source() + photos = 0 + video_frames = 0 + start = 0 + for n in m.frames: + ext, mid, i = m.parse_frame(n) + if ext != "mark": + continue + print(n) + if mid == "start": + start = i + if mid == "stop": + video_frames += m.extract_video(start, i, mode) + start = 0 + if mid == "wow": + if start: + start -= 5000000 + else: + photos += 5 + m.extract_photo(i - 1000000, mode) + m.extract_photo(i - 2000000, mode) + m.extract_photo(i - 3000000, mode) + m.extract_photo(i - 4000000, mode) + m.extract_photo(i - 5000000, mode) + if mid == "photo": + photos += 1 + m.extract_photo(i, mode) + if mode == "convertall": + video_frames += m.extract_video(0, 9999999999999999, "convert") + return + print("Total", photos, "photos and", m.display_frames(video_frames)) + print(len(m.unused_frames), "/", len(m.frames)) + if mode == "gc": + os.chdir(m.source) + for n in m.unused_frames: + os.unlink(n) + print(m.unused_frames) + + def display_usec(m, v): + return "%.2f sec" % (v/1000000.) + + def display_frames(m, v): + return "%d frames %s" % (v, m.display_usec(v * 1000000 / 30.)) + + def frame_used(m, n): + if n in m.unused_frames: + m.unused_frames.remove(n) + + def extract_photo(m, around, mode): + print("Photo:", around) + best = None + for n in m.frames: + ext, mid, i = m.parse_frame(n) + if ext != "sv": + continue + if i < around: + best = n + continue + best = n + break + + m.frame_used(best) + out_file = m.output+"/image-%04d.jpeg" % m.out_index + m.out_index += 1 + if mode == "convert": + os.system("ln "+m.source+"/"+best+" "+out_file) + + def extract_video(m, start, end, mode): + print("Searching video", start, end, "--", m.display_usec(end-start)) + if mode == "convert": + m.prepare_work() + t1 = time.time() + seen_audio = False + seen_video = False + count = 0 + skip_audio = 0 + skip_video = 0 + num = 0 + for n in m.frames: + num += 1 + if not num % 1000: + print("Frame", num) + ext, mid, i = m.parse_frame(n) + if ext != "sa" and ext != "sv": + m.frame_used(n) + continue + if i < start - 1000000 or i > end: + continue + if ext == "sa": + seen_audio = True + if not seen_video: + continue + if mode == "convert": + os.system("cat "+m.source+"/"+n+" >> "+m.work+"/output.raw") + if ext == "sv": + if not seen_video: + first_video = i + seen_video = True + if mode == "convert": + os.system("ln "+m.source+"/"+n+" "+m.work+"/image-%06d.jpeg" % count) + count += 1 + while i >= first_video + count * 1000000 / m.fps: + print("Duplicating video frame at", i) + if mode == "convert": + os.system("ln "+m.source+"/"+n+" "+m.work+"/image-%06d.jpeg" % count) + count += 1 + m.frame_used(n) + + if mode == "convert": + os.chdir(m.work) + print("Converting", m.display_frames(count), "skipped", skip_audio, "audio and", skip_video, "video frames") + os.system("ffmpeg -f s16le -ac 2 -ar 48000 -i output.raw output.wav") + options = "-b:v 4096k -c:v libx264 -preset ultrafast" + os.system("ffmpeg -framerate %d -i image-%%06d.jpeg -i output.wav %s output.mp4" % (m.fps, options)) + os.system("rm output.raw") + out_file = m.output+"/video-%04d.mp4" % m.out_index + m.out_index += 1 + os.system("mv output.mp4 "+out_file) + print("Converted", m.display_frames(count), "in", "%.1f" % (time.time()-t1), "seconds") + if mode == "convert": + print("Original size -> new size") + os.system("du -sh .; du -sh "+out_file) + return count + + def jpegize(m): + i = 0 + os.chdir(m.source) + l = os.listdir(m.source) + l = filter(lambda n: n[-4:] == ".dng", l) + l = list(l) + l.sort() + print("Have", m.display_frames(len(l)), "dngs") + for n in l: + if n[-4:] != ".dng": + print("Something went terribly wrong") + continue + i += 1 + print(i, '/', len(l)) + base = n[:-4] + subprocess.run(['dcraw', + '-w', # -w Use camera white balance + '+M', # +M use embedded color matrix + '-H', '2', # -H 2 Recover highlights by blending them + '-o', '1', # -o 1 Output in sRGB colorspace + '-q', '0', # -q 0 Debayer with fast bi-linear interpolation + '-f', # -f Interpolate RGGB as four colors + '-T', n]) # -T Output TIFF + subprocess.run(['convert', base+'.tiff', base+'.jpeg']) + os.unlink(base+'.tiff') + os.rename(base+'.jpeg', base+'.jpeg.sv') + +m = Mpegize() +m.run(sys.argv)