1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
let u16 (s : string) (i : int) : int = String.get_uint16_le s i
let u32 (s : string) (i : int) : int = Int32.to_int (String.get_int32_le s i) land 0xFFFFFFFF
let chunks (s : string) (from : int) (upto : int) : (string * int * int) list =
let upto = min upto (String.length s) in
let rec go i acc =
if i + 8 > upto then List.rev acc
else
let id = String.sub s i 4 and size = u32 s (i + 4) in
let size = min size (upto - i - 8) in
let entry = if id = "LIST" && size >= 4 then (String.sub s (i + 8) 4, i + 12, size - 4) else (id, i + 8, size) in
go (i + 8 + size + (size land 1)) (entry :: acc)
in
go from []
let find (name : string) (cs : (string * int * int) list) : (int * int) option =
List.find_map (fun (n, at, size) -> if n = name then Some (at, size) else None) cs
type stream = { kind : string; handler : string; scale : int; rate : int; format : int * int }
let of_string (s : string) : header * Movie.t * Signal.t option =
if String.length s < 12 || String.sub s 0 4 <> "RIFF" || String.sub s 8 4 <> "AVI " then failwith "AVI: not an AVI file";
let top = chunks s 12 (String.length s) in
let hdrl = match find "hdrl" top with Some (at, size) -> chunks s at (at + size) | None -> failwith "AVI: no headers (hdrl)" in
let movi = match find "movi" top with Some (at, size) -> (at, size) | None -> failwith "AVI: no data (movi)" in
let streams =
List.filter_map
(fun (n, at, size) ->
if n <> "strl" then None
else
let cs = chunks s at (at + size) in
match (find "strh" cs, find "strf" cs) with
| Some (h, _), Some f -> Some { kind = String.sub s h 4; handler = String.sub s (h + 4) 4; scale = u32 s (h + 20); rate = u32 s (h + 24); format = f }
| _ -> None)
hdrl
in
let numbered = List.mapi (fun i st -> (i, st)) streams in
let video = match List.find_opt (fun (_, st) -> st.kind = "vids") numbered with Some v -> v | None -> failwith "AVI: no video stream" in
let vn, vs = video in
let bitmap, _ = vs.format in
let width = u32 s (bitmap + 4) and height = abs (Int32.to_int (String.get_int32_le s (bitmap + 8))) in
let codec = String.sub s (bitmap + 16) 4 in
if String.uppercase_ascii codec <> "MJPG" then failwith ("AVI: the codec " ^ String.escaped codec ^ ", not read here (only Motion JPEG)");
let rec data at size = List.concat_map (fun (n, a, sz) -> if n = "rec " then data a sz else [ (n, a, sz) ]) (chunks s at (at + size)) in
let data = data (fst movi) (snd movi) in
let of_stream n = List.filter (fun (name, _, _) -> String.length name = 4 && int_of_string_opt (String.sub name 0 2) = Some n) data in
let frames = Array.of_list (List.filter (fun (name, _, sz) -> (String.sub name 2 2 = "dc" || String.sub name 2 2 = "db") && sz > 0) (of_stream vn)) in
if frames = [||] then failwith "AVI: no frames";
let rate = if vs.rate > 0 && vs.scale > 0 then (vs.rate, vs.scale) else (1_000_000, max 1 (match find "avih" hdrl with Some (a, _) -> u32 s a | None -> 40_000)) in
let period = float_of_int (snd rate) /. float_of_int (fst rate) in
let count = Array.length frames in
let last = ref (-1, Rgba_image.create ~width:0 ~height:0) in
let decode i =
if fst !last <> i then (let _, at, size = frames.(i) in last := (i, Jpeg.decode (String.sub s at size)));
snd !last
in
let movie : Movie.t =
{
width;
height;
times = Array.init count (fun i -> float_of_int i *. period);
duration = float_of_int count *. period;
frame = decode;
}
in
let sound =
match List.find_opt (fun (_, st) -> st.kind = "auds") numbered with
| None -> None
| Some (an, st) ->
let fmt, _ = st.format in
let tag = u16 s fmt and channels = u16 s (fmt + 2) and rate = u32 s (fmt + 4) and bits = u16 s (fmt + 14) in
if tag <> 1 || (bits <> 8 && bits <> 16) || (channels <> 1 && channels <> 2) then failwith "AVI: sound not 8 or 16-bit PCM, mono or stereo";
let bytes = String.concat "" (List.map (fun (_, at, size) -> String.sub s at size) (of_stream an)) in
let width = bits / 8 * channels in
let sample i c =
let at = (i * width) + (c * bits / 8) in
if bits = 16 then float_of_int (String.get_int16_le bytes at) /. 32767. else float_of_int (Char.code bytes.[at] - 128) /. 127.
in
let mono = Array.init (String.length bytes / width) (fun i -> if channels = 1 then sample i 0 else (sample i 0 +. sample i 1) /. 2.) in
Some (Resample.to_rate Linear rate mono)
in
({ width; height; rate; codec }, movie, sound)
let chunk (b : Buffer.t) (id : string) (body : string) : unit =
Buffer.add_string b id;
Buffer.add_int32_le b (Int32.of_int (String.length body));
Buffer.add_string b body;
if String.length body land 1 = 1 then Buffer.add_char b '\000'
let list (b : Buffer.t) (typ : string) (body : string) : unit = chunk b "LIST" (typ ^ body)
let le (fields : [ `U16 of int | `U32 of int | `S of string ] list) : string =
let b = Buffer.create 64 in
List.iter (function `U16 v -> Buffer.add_uint16_le b v | `U32 v -> Buffer.add_int32_le b (Int32.of_int v) | `S s -> Buffer.add_string b s) fields;
Buffer.contents b
let to_string ?(quality = 75) ?(sound : Signal.t option) ~(rate : int * int) (frames : Rgba_image.t list) : string =
let first = match frames with [] -> invalid_arg "Avi.to_string: no frames" | f :: _ -> f in
let w = first.width and h = first.height and num, den = rate in
let n = List.length frames in
let movi = Buffer.create (n * 8192) and index = ref [] in
let add id body =
index := (id, 4 + Buffer.length movi, String.length body) :: !index;
chunk movi id body
in
let sample_at i = i * Signal.rate * den / num in
List.iteri
(fun i (img : Rgba_image.t) ->
if img.width <> w || img.height <> h then invalid_arg "Avi.to_string: frames of different sizes";
add "00dc" (Jpeg_encode.encode ~quality img);
Option.iter
(fun (samples : Signal.t) ->
let a = min (Array.length samples) (sample_at i) and z = if i = n - 1 then Array.length samples else min (Array.length samples) (sample_at (i + 1)) in
if z > a then add "01wb" (String.init (2 * (z - a)) (fun k -> let v = Signal.to_int16 samples.(a + (k / 2)) land 0xFFFF in Char.chr (if k land 1 = 0 then v land 0xFF else v lsr 8))))
sound)
frames;
let largest = List.fold_left (fun m (_, _, size) -> max m size) 0 !index in
let streams = if sound = None then 1 else 2 in
let avih =
le [ `U32 (1_000_000 * den / num); `U32 (largest * num / den); `U32 0; `U32 0x110 ; `U32 n; `U32 0; `U32 streams; `U32 largest; `U32 w; `U32 h; `S (String.make 16 '\000') ]
in
let video_strl =
let b = Buffer.create 128 in
chunk b "strh" (le [ `S "vidsMJPG"; `U32 0; `U16 0; `U16 0; `U32 0; `U32 den; `U32 num; `U32 0; `U32 n; `U32 largest; `U32 0xFFFFFFFF; `U32 0; `U16 0; `U16 0; `U16 w; `U16 h ]);
chunk b "strf" (le [ `U32 40; `U32 w; `U32 h; `U16 1; `U16 24; `S "MJPG"; `U32 (w * h * 3); `U32 0; `U32 0; `U32 0; `U32 0 ]);
Buffer.contents b
in
let audio_strl (samples : Signal.t) =
let b = Buffer.create 128 in
chunk b "strh" (le [ `S "auds"; `U32 0; `U32 0; `U16 0; `U16 0; `U32 0; `U32 2; `U32 (2 * Signal.rate); `U32 0; `U32 (Array.length samples); `U32 largest; `U32 0xFFFFFFFF; `U32 2; `U16 0; `U16 0; `U16 0; `U16 0 ]);
chunk b "strf" (le [ `U16 1; `U16 1; `U32 Signal.rate; `U32 (2 * Signal.rate); `U16 2; `U16 16 ]);
Buffer.contents b
in
let hdrl = Buffer.create 512 in
chunk hdrl "avih" avih;
list hdrl "strl" video_strl;
Option.iter (fun samples -> list hdrl "strl" (audio_strl samples)) sound;
let idx1 = Buffer.create (16 * List.length !index) in
List.iter (fun (id, offset, size) -> Buffer.add_string idx1 (le [ `S id; `U32 0x10 ; `U32 offset; `U32 size ])) (List.rev !index);
let body = Buffer.create (Buffer.length movi + 1024) in
list body "hdrl" (Buffer.contents hdrl);
list body "movi" (Buffer.contents movi);
chunk body "idx1" (Buffer.contents idx1);
let file = Buffer.create (Buffer.length body + 12) in
chunk file "RIFF" ("AVI " ^ Buffer.contents body);
Buffer.contents file