Browse Source

Add support files that will be used for video recording.

Pavel Machek 10 months ago
parent
commit
fcba2b33c2
5 changed files with 446 additions and 0 deletions
  1. 14 0
      medianame.h
  2. 22 0
      meson.build
  3. 76 0
      movie.sh.in
  4. 93 0
      movie_audio_rec.c
  5. 241 0
      mpegize.py

+ 14 - 0
medianame.h

@@ -0,0 +1,14 @@
+#include <sys/time.h>
+
+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);
+}

+ 22 - 0
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',

+ 76 - 0
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

+ 93 - 0
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 <http://www.gnu.org/licenses/>.
+
+  * Copyright 2022, 2024 Pavel Machek
+***/
+#include <stdio.h>
+#include <unistd.h>
+#include <string.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <stdlib.h>
+#include <pulse/simple.h>
+#include <pulse/error.h>
+
+#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;
+}

+ 241 - 0
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)