auto-editor 28.0.2__py3-none-any.whl → 28.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- auto_editor/__init__.py +1 -1
- auto_editor/__main__.py +4 -3
- auto_editor/analyze.py +13 -13
- auto_editor/cmds/desc.py +2 -2
- auto_editor/cmds/levels.py +3 -3
- auto_editor/cmds/subdump.py +4 -4
- auto_editor/cmds/test.py +31 -27
- auto_editor/edit.py +35 -23
- auto_editor/exports/kdenlive.py +322 -0
- auto_editor/ffwrapper.py +8 -8
- auto_editor/help.py +1 -0
- auto_editor/lang/stdenv.py +0 -5
- auto_editor/make_layers.py +3 -3
- auto_editor/render/audio.py +42 -42
- auto_editor/render/subtitle.py +5 -5
- auto_editor/render/video.py +28 -33
- auto_editor/utils/container.py +2 -3
- auto_editor/utils/log.py +3 -1
- {auto_editor-28.0.2.dist-info → auto_editor-28.1.0.dist-info}/METADATA +2 -2
- {auto_editor-28.0.2.dist-info → auto_editor-28.1.0.dist-info}/RECORD +24 -23
- {auto_editor-28.0.2.dist-info → auto_editor-28.1.0.dist-info}/WHEEL +0 -0
- {auto_editor-28.0.2.dist-info → auto_editor-28.1.0.dist-info}/entry_points.txt +0 -0
- {auto_editor-28.0.2.dist-info → auto_editor-28.1.0.dist-info}/licenses/LICENSE +0 -0
- {auto_editor-28.0.2.dist-info → auto_editor-28.1.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,322 @@
|
|
1
|
+
import json
|
2
|
+
import xml.etree.ElementTree as ET
|
3
|
+
from os import getcwd
|
4
|
+
from uuid import uuid4
|
5
|
+
|
6
|
+
from auto_editor.timeline import Clip, v3
|
7
|
+
from auto_editor.utils.func import aspect_ratio, to_timecode
|
8
|
+
|
9
|
+
"""
|
10
|
+
kdenlive uses the MLT timeline format
|
11
|
+
|
12
|
+
See docs here:
|
13
|
+
https://mltframework.org/docs/mltxml/
|
14
|
+
|
15
|
+
kdenlive specifics:
|
16
|
+
https://github.com/KDE/kdenlive/blob/master/dev-docs/fileformat.md
|
17
|
+
"""
|
18
|
+
|
19
|
+
|
20
|
+
def kdenlive_write(output: str, tl: v3) -> None:
|
21
|
+
mlt = ET.Element(
|
22
|
+
"mlt",
|
23
|
+
attrib={
|
24
|
+
"LC_NUMERIC": "C",
|
25
|
+
"version": "7.22.0",
|
26
|
+
"producer": "main_bin",
|
27
|
+
"root": f"{getcwd()}",
|
28
|
+
},
|
29
|
+
)
|
30
|
+
|
31
|
+
width, height = tl.res
|
32
|
+
num, den = aspect_ratio(width, height)
|
33
|
+
tb = tl.tb
|
34
|
+
seq_uuid = uuid4()
|
35
|
+
|
36
|
+
ET.SubElement(
|
37
|
+
mlt,
|
38
|
+
"profile",
|
39
|
+
attrib={
|
40
|
+
"description": "automatic",
|
41
|
+
"width": f"{width}",
|
42
|
+
"height": f"{height}",
|
43
|
+
"progressive": "1",
|
44
|
+
"sample_aspect_num": "1",
|
45
|
+
"sample_aspect_den": "1",
|
46
|
+
"display_aspect_num": f"{num}",
|
47
|
+
"display_aspect_den": f"{den}",
|
48
|
+
"frame_rate_num": f"{tb.numerator}",
|
49
|
+
"frame_rate_den": f"{tb.denominator}",
|
50
|
+
"colorspace": "709",
|
51
|
+
},
|
52
|
+
)
|
53
|
+
|
54
|
+
# Reserved producer0
|
55
|
+
global_out = to_timecode(len(tl) / tb, "standard")
|
56
|
+
producer = ET.SubElement(mlt, "producer", id="producer0")
|
57
|
+
ET.SubElement(producer, "property", name="length").text = global_out
|
58
|
+
ET.SubElement(producer, "property", name="eof").text = "continue"
|
59
|
+
ET.SubElement(producer, "property", name="resource").text = "black"
|
60
|
+
ET.SubElement(producer, "property", name="mlt_service").text = "color"
|
61
|
+
ET.SubElement(producer, "property", name="kdenlive:playlistid").text = "black_track"
|
62
|
+
ET.SubElement(producer, "property", name="mlt_image_format").text = "rgba"
|
63
|
+
ET.SubElement(producer, "property", name="aspect_ratio").text = "1"
|
64
|
+
|
65
|
+
# Get all clips
|
66
|
+
if tl.v:
|
67
|
+
clips = [clip for clip in tl.v[0] if isinstance(clip, Clip)]
|
68
|
+
elif tl.a:
|
69
|
+
clips = tl.a[0]
|
70
|
+
else:
|
71
|
+
clips = []
|
72
|
+
|
73
|
+
source_ids = {}
|
74
|
+
source_id = 4
|
75
|
+
clip_playlists = []
|
76
|
+
chains = 0
|
77
|
+
playlists = 0
|
78
|
+
producers = 1
|
79
|
+
a_channels = len(tl.a)
|
80
|
+
v_channels = len(tl.v)
|
81
|
+
warped_clips = [i for i, clip in enumerate(clips) if clip.speed != 1]
|
82
|
+
|
83
|
+
# create all producers for warped clips
|
84
|
+
for clip_idx in warped_clips:
|
85
|
+
for i in range(a_channels + v_channels):
|
86
|
+
clip = clips[clip_idx]
|
87
|
+
path = str(clip.src.path)
|
88
|
+
|
89
|
+
if path not in source_ids:
|
90
|
+
source_ids[path] = str(source_id)
|
91
|
+
source_id += 1
|
92
|
+
|
93
|
+
prod = ET.SubElement(
|
94
|
+
mlt,
|
95
|
+
"producer",
|
96
|
+
attrib={
|
97
|
+
"id": f"producer{producers}",
|
98
|
+
"in": "00:00:00.000",
|
99
|
+
"out": global_out,
|
100
|
+
},
|
101
|
+
)
|
102
|
+
ET.SubElement(
|
103
|
+
prod, "property", name="resource"
|
104
|
+
).text = f"{clip.speed}:{path}"
|
105
|
+
ET.SubElement(prod, "property", name="warp_speed").text = str(clip.speed)
|
106
|
+
ET.SubElement(prod, "property", name="warp_resource").text = path
|
107
|
+
ET.SubElement(prod, "property", name="warp_pitch").text = "0"
|
108
|
+
ET.SubElement(prod, "property", name="mlt_service").text = "timewarp"
|
109
|
+
ET.SubElement(prod, "property", name="kdenlive:id").text = source_ids[path]
|
110
|
+
|
111
|
+
if i < a_channels:
|
112
|
+
ET.SubElement(prod, "property", name="vstream").text = "0"
|
113
|
+
ET.SubElement(prod, "property", name="astream").text = str(
|
114
|
+
a_channels - 1 - i
|
115
|
+
)
|
116
|
+
ET.SubElement(prod, "property", name="set.test_audio").text = "0"
|
117
|
+
ET.SubElement(prod, "property", name="set.test_video").text = "1"
|
118
|
+
else:
|
119
|
+
ET.SubElement(prod, "property", name="vstream").text = str(
|
120
|
+
v_channels - 1 - (i - a_channels)
|
121
|
+
)
|
122
|
+
ET.SubElement(prod, "property", name="astream").text = "0"
|
123
|
+
ET.SubElement(prod, "property", name="set.test_audio").text = "1"
|
124
|
+
ET.SubElement(prod, "property", name="set.test_video").text = "0"
|
125
|
+
|
126
|
+
producers += 1
|
127
|
+
|
128
|
+
# create chains, playlists and tractors for audio channels
|
129
|
+
for i, audio in enumerate(tl.a):
|
130
|
+
path = str(audio[0].src.path)
|
131
|
+
|
132
|
+
if path not in source_ids:
|
133
|
+
source_ids[path] = str(source_id)
|
134
|
+
source_id += 1
|
135
|
+
|
136
|
+
chain = ET.SubElement(mlt, "chain", attrib={"id": f"chain{chains}"})
|
137
|
+
ET.SubElement(chain, "property", name="resource").text = path
|
138
|
+
ET.SubElement(
|
139
|
+
chain, "property", name="mlt_service"
|
140
|
+
).text = "avformat-novalidate"
|
141
|
+
ET.SubElement(chain, "property", name="vstream").text = "0"
|
142
|
+
ET.SubElement(chain, "property", name="astream").text = str(a_channels - 1 - i)
|
143
|
+
ET.SubElement(chain, "property", name="set.test_audio").text = "0"
|
144
|
+
ET.SubElement(chain, "property", name="set.test_video").text = "1"
|
145
|
+
ET.SubElement(chain, "property", name="kdenlive:id").text = source_ids[path]
|
146
|
+
|
147
|
+
for _i in range(2):
|
148
|
+
playlist = ET.SubElement(mlt, "playlist", id=f"playlist{playlists}")
|
149
|
+
clip_playlists.append(playlist)
|
150
|
+
ET.SubElement(playlist, "property", name="kdenlive:audio_track").text = "1"
|
151
|
+
playlists += 1
|
152
|
+
|
153
|
+
tractor = ET.SubElement(
|
154
|
+
mlt,
|
155
|
+
"tractor",
|
156
|
+
attrib={"id": f"tractor{chains}", "in": "00:00:00.000", "out": global_out},
|
157
|
+
)
|
158
|
+
ET.SubElement(tractor, "property", name="kdenlive:audio_track").text = "1"
|
159
|
+
ET.SubElement(tractor, "property", name="kdenlive:timeline_active").text = "1"
|
160
|
+
ET.SubElement(tractor, "property", name="kdenlive:audio_rec")
|
161
|
+
ET.SubElement(
|
162
|
+
tractor,
|
163
|
+
"track",
|
164
|
+
attrib={"hide": "video", "producer": f"playlist{playlists - 2}"},
|
165
|
+
)
|
166
|
+
ET.SubElement(
|
167
|
+
tractor,
|
168
|
+
"track",
|
169
|
+
attrib={"hide": "video", "producer": f"playlist{playlists - 1}"},
|
170
|
+
)
|
171
|
+
chains += 1
|
172
|
+
|
173
|
+
# create chains, playlists and tractors for video channels
|
174
|
+
for i, video in enumerate(tl.v):
|
175
|
+
path = f"{video[0].src.path}" # type: ignore
|
176
|
+
|
177
|
+
if path not in source_ids:
|
178
|
+
source_ids[path] = str(source_id)
|
179
|
+
source_id += 1
|
180
|
+
|
181
|
+
chain = ET.SubElement(mlt, "chain", attrib={"id": f"chain{chains}"})
|
182
|
+
ET.SubElement(chain, "property", name="resource").text = path
|
183
|
+
ET.SubElement(
|
184
|
+
chain, "property", name="mlt_service"
|
185
|
+
).text = "avformat-novalidate"
|
186
|
+
ET.SubElement(chain, "property", name="vstream").text = str(v_channels - 1 - i)
|
187
|
+
ET.SubElement(chain, "property", name="astream").text = "0"
|
188
|
+
ET.SubElement(chain, "property", name="set.test_audio").text = "1"
|
189
|
+
ET.SubElement(chain, "property", name="set.test_video").text = "0"
|
190
|
+
ET.SubElement(chain, "property", name="kdenlive:id").text = source_ids[path]
|
191
|
+
|
192
|
+
for _i in range(2):
|
193
|
+
playlist = ET.SubElement(mlt, "playlist", id=f"playlist{playlists}")
|
194
|
+
clip_playlists.append(playlist)
|
195
|
+
playlists += 1
|
196
|
+
|
197
|
+
tractor = ET.SubElement(
|
198
|
+
mlt,
|
199
|
+
"tractor",
|
200
|
+
attrib={"id": f"tractor{chains}", "in": "00:00:00.000", "out": global_out},
|
201
|
+
)
|
202
|
+
ET.SubElement(tractor, "property", name="kdenlive:timeline_active").text = "1"
|
203
|
+
ET.SubElement(
|
204
|
+
tractor,
|
205
|
+
"track",
|
206
|
+
attrib={"hide": "audio", "producer": f"playlist{playlists - 2}"},
|
207
|
+
)
|
208
|
+
ET.SubElement(
|
209
|
+
tractor,
|
210
|
+
"track",
|
211
|
+
attrib={"hide": "audio", "producer": f"playlist{playlists - 1}"},
|
212
|
+
)
|
213
|
+
chains += 1
|
214
|
+
|
215
|
+
# final chain for the project bin
|
216
|
+
path = str(clips[0].src.path)
|
217
|
+
chain = ET.SubElement(mlt, "chain", attrib={"id": f"chain{chains}"})
|
218
|
+
ET.SubElement(chain, "property", name="resource").text = path
|
219
|
+
ET.SubElement(chain, "property", name="mlt_service").text = "avformat-novalidate"
|
220
|
+
ET.SubElement(chain, "property", name="audio_index").text = "1"
|
221
|
+
ET.SubElement(chain, "property", name="video_index").text = "0"
|
222
|
+
ET.SubElement(chain, "property", name="vstream").text = "0"
|
223
|
+
ET.SubElement(chain, "property", name="astream").text = "0"
|
224
|
+
ET.SubElement(chain, "property", name="kdenlive:id").text = source_ids[path]
|
225
|
+
|
226
|
+
groups = []
|
227
|
+
group_counter = 0
|
228
|
+
producers = 1
|
229
|
+
|
230
|
+
for clip in clips:
|
231
|
+
group_children: list[object] = []
|
232
|
+
_in = to_timecode(clip.offset / tb, "standard")
|
233
|
+
_out = to_timecode((clip.offset + clip.dur) / tb, "standard")
|
234
|
+
path = str(clip.src.path)
|
235
|
+
|
236
|
+
for i, playlist in enumerate(clip_playlists[::2]):
|
237
|
+
# adding 1 extra frame for each previous group to the start time works but feels hacky?
|
238
|
+
group_children.append(
|
239
|
+
{
|
240
|
+
"data": f"{i}:{clip.start + group_counter}",
|
241
|
+
"leaf": "clip",
|
242
|
+
"type": "Leaf",
|
243
|
+
}
|
244
|
+
)
|
245
|
+
clip_prod = ""
|
246
|
+
|
247
|
+
if clip.speed == 1:
|
248
|
+
clip_prod = f"chain{i}"
|
249
|
+
else:
|
250
|
+
clip_prod = f"producer{producers}"
|
251
|
+
producers += 1
|
252
|
+
|
253
|
+
entry = ET.SubElement(
|
254
|
+
playlist,
|
255
|
+
"entry",
|
256
|
+
attrib={"producer": f"{clip_prod}", "in": _in, "out": _out},
|
257
|
+
)
|
258
|
+
ET.SubElement(entry, "property", name="kdenlive:id").text = source_ids[path]
|
259
|
+
|
260
|
+
groups.append({"children": group_children[:], "type": "Normal"})
|
261
|
+
group_counter += 1
|
262
|
+
|
263
|
+
# default sequence tractor
|
264
|
+
sequence = ET.SubElement(
|
265
|
+
mlt,
|
266
|
+
"tractor",
|
267
|
+
attrib={"id": f"{{{seq_uuid}}}", "in": "00:00:00.000", "out": "00:00:00.000"},
|
268
|
+
)
|
269
|
+
ET.SubElement(sequence, "property", name="kdenlive:uuid").text = f"{{{seq_uuid}}}"
|
270
|
+
ET.SubElement(sequence, "property", name="kdenlive:clipname").text = "Sequence 1"
|
271
|
+
ET.SubElement(
|
272
|
+
sequence, "property", name="kdenlive:sequenceproperties.groups"
|
273
|
+
).text = json.dumps(groups, indent=4)
|
274
|
+
ET.SubElement(sequence, "track", producer="producer0")
|
275
|
+
|
276
|
+
for i in range(chains):
|
277
|
+
ET.SubElement(sequence, "track", producer=f"tractor{i}")
|
278
|
+
|
279
|
+
# main bin
|
280
|
+
playlist_bin = ET.SubElement(mlt, "playlist", id="main_bin")
|
281
|
+
ET.SubElement(
|
282
|
+
playlist_bin, "property", name="kdenlive:docproperties.uuid"
|
283
|
+
).text = f"{{{seq_uuid}}}"
|
284
|
+
ET.SubElement(
|
285
|
+
playlist_bin, "property", name="kdenlive:docproperties.version"
|
286
|
+
).text = "1.1"
|
287
|
+
ET.SubElement(playlist_bin, "property", name="xml_retain").text = "1"
|
288
|
+
ET.SubElement(
|
289
|
+
playlist_bin,
|
290
|
+
"entry",
|
291
|
+
attrib={
|
292
|
+
"producer": f"{{{seq_uuid}}}",
|
293
|
+
"in": "00:00:00.000",
|
294
|
+
"out": "00:00:00.000",
|
295
|
+
},
|
296
|
+
)
|
297
|
+
ET.SubElement(
|
298
|
+
playlist_bin,
|
299
|
+
"entry",
|
300
|
+
attrib={"producer": f"chain{chains}", "in": "00:00:00.000"},
|
301
|
+
)
|
302
|
+
|
303
|
+
# reserved last tractor for project
|
304
|
+
tractor = ET.SubElement(
|
305
|
+
mlt,
|
306
|
+
"tractor",
|
307
|
+
attrib={"id": f"tractor{chains}", "in": "00:00:00.000", "out": global_out},
|
308
|
+
)
|
309
|
+
ET.SubElement(tractor, "property", name="kdenlive:projectTractor").text = "1"
|
310
|
+
ET.SubElement(
|
311
|
+
tractor,
|
312
|
+
"track",
|
313
|
+
attrib={"producer": f"{{{seq_uuid}}}", "in": "00:00:00.000", "out": global_out},
|
314
|
+
)
|
315
|
+
tree = ET.ElementTree(mlt)
|
316
|
+
|
317
|
+
ET.indent(tree, space="\t", level=0)
|
318
|
+
|
319
|
+
if output == "-":
|
320
|
+
print(ET.tostring(mlt, encoding="unicode"))
|
321
|
+
else:
|
322
|
+
tree.write(output, xml_declaration=True, encoding="utf-8")
|
auto_editor/ffwrapper.py
CHANGED
@@ -4,14 +4,14 @@ from dataclasses import dataclass
|
|
4
4
|
from fractions import Fraction
|
5
5
|
from pathlib import Path
|
6
6
|
|
7
|
-
import
|
7
|
+
import av
|
8
8
|
|
9
9
|
from auto_editor.utils.log import Log
|
10
10
|
|
11
11
|
|
12
12
|
def mux(input: Path, output: Path, stream: int) -> None:
|
13
|
-
input_container =
|
14
|
-
output_container =
|
13
|
+
input_container = av.open(input, "r")
|
14
|
+
output_container = av.open(output, "w")
|
15
15
|
|
16
16
|
input_audio_stream = input_container.streams.audio[stream]
|
17
17
|
output_audio_stream = output_container.add_stream("pcm_s16le")
|
@@ -89,12 +89,12 @@ class FileInfo:
|
|
89
89
|
@classmethod
|
90
90
|
def init(self, path: str, log: Log) -> FileInfo:
|
91
91
|
try:
|
92
|
-
cont =
|
93
|
-
except
|
92
|
+
cont = av.open(path, "r")
|
93
|
+
except av.error.FileNotFoundError:
|
94
94
|
log.error(f"Input file doesn't exist: {path}")
|
95
|
-
except
|
95
|
+
except av.error.IsADirectoryError:
|
96
96
|
log.error(f"Expected a media file, but got a directory: {path}")
|
97
|
-
except
|
97
|
+
except av.error.InvalidDataError:
|
98
98
|
log.error(f"Invalid data when processing: {path}")
|
99
99
|
|
100
100
|
videos: tuple[VideoStream, ...] = ()
|
@@ -177,7 +177,7 @@ class FileInfo:
|
|
177
177
|
|
178
178
|
timecode = get_timecode()
|
179
179
|
bitrate = 0 if cont.bit_rate is None else cont.bit_rate
|
180
|
-
dur = 0 if cont.duration is None else cont.duration /
|
180
|
+
dur = 0 if cont.duration is None else cont.duration / av.time_base
|
181
181
|
|
182
182
|
cont.close()
|
183
183
|
|
auto_editor/help.py
CHANGED
@@ -78,6 +78,7 @@ Export Methods:
|
|
78
78
|
- final-cut-pro ; Export as an XML timeline file for Final Cut Pro
|
79
79
|
- name : "Auto-Editor Media Group"
|
80
80
|
- shotcut ; Export as an XML timeline file for Shotcut
|
81
|
+
- kdenlive ; Export as an XML timeline file for kdenlive
|
81
82
|
- v3 ; Export as an auto-editor v3 timeline file
|
82
83
|
- v1 ; Export as an auto-editor v1 timeline file
|
83
84
|
- clip-sequence ; Export as multiple numbered media files
|
auto_editor/lang/stdenv.py
CHANGED
@@ -3,8 +3,6 @@ from __future__ import annotations
|
|
3
3
|
from dataclasses import dataclass
|
4
4
|
from typing import TYPE_CHECKING
|
5
5
|
|
6
|
-
import bv
|
7
|
-
|
8
6
|
from auto_editor.analyze import mut_remove_large, mut_remove_small
|
9
7
|
from auto_editor.lib.contracts import *
|
10
8
|
from auto_editor.lib.data_structs import *
|
@@ -1169,9 +1167,6 @@ def make_standard_env() -> dict[str, Any]:
|
|
1169
1167
|
"string->vector", lambda s: [Char(c) for c in s], (1, 1), is_str
|
1170
1168
|
),
|
1171
1169
|
"range->vector": Proc("range->vector", list, (1, 1), is_range),
|
1172
|
-
# av
|
1173
|
-
"encoder": Proc("encoder", lambda x: bv.Codec(x, "w"), (1, 1), is_str),
|
1174
|
-
"decoder": Proc("decoder", lambda x: bv.Codec(x), (1, 1), is_str),
|
1175
1170
|
# reflexion
|
1176
1171
|
"var-exists?": Proc("var-exists?", lambda sym: sym.val in env, (1, 1), is_symbol),
|
1177
1172
|
"rename": Syntax(syn_rename),
|
auto_editor/make_layers.py
CHANGED
@@ -299,9 +299,9 @@ def make_timeline(
|
|
299
299
|
|
300
300
|
if len(sources) == 1 and inp is not None:
|
301
301
|
chunks = chunkify(speed_index, speed_hash)
|
302
|
-
|
302
|
+
v1_compatible = v1(inp, chunks)
|
303
303
|
else:
|
304
|
-
|
304
|
+
v1_compatible = None
|
305
305
|
|
306
306
|
if len(vtl) == 0 and len(atl) == 0:
|
307
307
|
log.error("Timeline is empty, nothing to do.")
|
@@ -312,4 +312,4 @@ def make_timeline(
|
|
312
312
|
else:
|
313
313
|
template = Template.init(inp, sr, args.audio_layout, res)
|
314
314
|
|
315
|
-
return v3(tb, args.background, template, vtl, atl,
|
315
|
+
return v3(tb, args.background, template, vtl, atl, v1_compatible)
|
auto_editor/render/audio.py
CHANGED
@@ -3,12 +3,12 @@ from __future__ import annotations
|
|
3
3
|
from fractions import Fraction
|
4
4
|
from io import BytesIO
|
5
5
|
from pathlib import Path
|
6
|
-
from typing import TYPE_CHECKING
|
6
|
+
from typing import TYPE_CHECKING, cast
|
7
7
|
|
8
|
-
import
|
8
|
+
import av
|
9
9
|
import numpy as np
|
10
|
-
from
|
11
|
-
from
|
10
|
+
from av import AudioFrame
|
11
|
+
from av.filter.loudnorm import stats
|
12
12
|
|
13
13
|
from auto_editor.ffwrapper import FileInfo
|
14
14
|
from auto_editor.json import load
|
@@ -22,7 +22,6 @@ from auto_editor.utils.log import Log
|
|
22
22
|
|
23
23
|
if TYPE_CHECKING:
|
24
24
|
from collections.abc import Iterator
|
25
|
-
from typing import Any
|
26
25
|
|
27
26
|
from auto_editor.__main__ import Args
|
28
27
|
|
@@ -102,7 +101,7 @@ def apply_audio_normalization(
|
|
102
101
|
f"i={norm['i']}:lra={norm['lra']}:tp={norm['tp']}:offset={norm['gain']}"
|
103
102
|
)
|
104
103
|
log.debug(f"audio norm first pass: {first_pass}")
|
105
|
-
with
|
104
|
+
with av.open(f"{pre_master}") as container:
|
106
105
|
stats_ = stats(first_pass, container.streams.audio[0])
|
107
106
|
|
108
107
|
name, filter_args = parse_ebu_bytes(norm, stats_, log)
|
@@ -117,7 +116,7 @@ def apply_audio_normalization(
|
|
117
116
|
return -20.0 * np.log10(max_amplitude)
|
118
117
|
return -99.0
|
119
118
|
|
120
|
-
with
|
119
|
+
with av.open(pre_master) as container:
|
121
120
|
max_peak_level = -99.0
|
122
121
|
assert len(container.streams.video) == 0
|
123
122
|
for frame in container.decode(audio=0):
|
@@ -129,13 +128,13 @@ def apply_audio_normalization(
|
|
129
128
|
log.print(f"peak adjustment: {adjustment:.3f}dB")
|
130
129
|
name, filter_args = "volume", f"{adjustment}"
|
131
130
|
|
132
|
-
with
|
131
|
+
with av.open(pre_master) as container:
|
133
132
|
input_stream = container.streams.audio[0]
|
134
133
|
|
135
|
-
output_file =
|
134
|
+
output_file = av.open(path, mode="w")
|
136
135
|
output_stream = output_file.add_stream("pcm_s16le", rate=input_stream.rate)
|
137
136
|
|
138
|
-
graph =
|
137
|
+
graph = av.filter.Graph()
|
139
138
|
graph.link_nodes(
|
140
139
|
graph.add_abuffer(template=input_stream),
|
141
140
|
graph.add(name, filter_args),
|
@@ -148,7 +147,7 @@ def apply_audio_normalization(
|
|
148
147
|
aframe = graph.pull()
|
149
148
|
assert isinstance(aframe, AudioFrame)
|
150
149
|
output_file.mux(output_stream.encode(aframe))
|
151
|
-
except (
|
150
|
+
except (av.BlockingIOError, av.EOFError):
|
152
151
|
break
|
153
152
|
|
154
153
|
output_file.mux(output_stream.encode(None))
|
@@ -156,10 +155,10 @@ def apply_audio_normalization(
|
|
156
155
|
|
157
156
|
|
158
157
|
def process_audio_clip(clip: Clip, data: np.ndarray, sr: int, log: Log) -> np.ndarray:
|
159
|
-
to_s16 =
|
158
|
+
to_s16 = av.AudioResampler(format="s16", layout="stereo", rate=sr)
|
160
159
|
input_buffer = BytesIO()
|
161
160
|
|
162
|
-
with
|
161
|
+
with av.open(input_buffer, "w", format="wav") as container:
|
163
162
|
output_stream = container.add_stream(
|
164
163
|
"pcm_s16le", sample_rate=sr, format="s16", layout="stereo"
|
165
164
|
)
|
@@ -173,10 +172,10 @@ def process_audio_clip(clip: Clip, data: np.ndarray, sr: int, log: Log) -> np.nd
|
|
173
172
|
|
174
173
|
input_buffer.seek(0)
|
175
174
|
|
176
|
-
input_file =
|
175
|
+
input_file = av.open(input_buffer, "r")
|
177
176
|
input_stream = input_file.streams.audio[0]
|
178
177
|
|
179
|
-
graph =
|
178
|
+
graph = av.filter.Graph()
|
180
179
|
args = [graph.add_abuffer(template=input_stream)]
|
181
180
|
|
182
181
|
if clip.speed != 1:
|
@@ -202,7 +201,7 @@ def process_audio_clip(clip: Clip, data: np.ndarray, sr: int, log: Log) -> np.nd
|
|
202
201
|
graph.link_nodes(*args).configure()
|
203
202
|
|
204
203
|
all_frames = []
|
205
|
-
resampler =
|
204
|
+
resampler = av.AudioResampler(format="s16p", layout="stereo", rate=sr)
|
206
205
|
|
207
206
|
for frame in input_file.decode(input_stream):
|
208
207
|
graph.push(frame)
|
@@ -214,7 +213,7 @@ def process_audio_clip(clip: Clip, data: np.ndarray, sr: int, log: Log) -> np.nd
|
|
214
213
|
for resampled_frame in resampler.resample(aframe):
|
215
214
|
all_frames.append(resampled_frame.to_ndarray())
|
216
215
|
|
217
|
-
except (
|
216
|
+
except (av.BlockingIOError, av.EOFError):
|
218
217
|
break
|
219
218
|
|
220
219
|
if not all_frames:
|
@@ -229,7 +228,7 @@ def mix_audio_files(sr: int, audio_paths: list[str], output_path: str) -> None:
|
|
229
228
|
|
230
229
|
# First pass: determine the maximum length
|
231
230
|
for path in audio_paths:
|
232
|
-
container =
|
231
|
+
container = av.open(path)
|
233
232
|
stream = container.streams.audio[0]
|
234
233
|
|
235
234
|
# Calculate duration in samples
|
@@ -241,14 +240,11 @@ def mix_audio_files(sr: int, audio_paths: list[str], output_path: str) -> None:
|
|
241
240
|
|
242
241
|
# Second pass: read and mix audio
|
243
242
|
for path in audio_paths:
|
244
|
-
container =
|
243
|
+
container = av.open(path)
|
245
244
|
stream = container.streams.audio[0]
|
246
245
|
|
247
|
-
resampler = bv.audio.resampler.AudioResampler(
|
248
|
-
format="s16", layout="mono", rate=sr
|
249
|
-
)
|
250
|
-
|
251
246
|
audio_array: list[np.ndarray] = []
|
247
|
+
resampler = av.AudioResampler(format="s16", layout="mono", rate=sr)
|
252
248
|
for frame in container.decode(audio=0):
|
253
249
|
frame.pts = None
|
254
250
|
resampled = resampler.resample(frame)[0]
|
@@ -277,7 +273,7 @@ def mix_audio_files(sr: int, audio_paths: list[str], output_path: str) -> None:
|
|
277
273
|
mixed_audio = mixed_audio * (32767 / max_val)
|
278
274
|
mixed_audio = mixed_audio.astype(np.int16)
|
279
275
|
|
280
|
-
output_container =
|
276
|
+
output_container = av.open(output_path, mode="w")
|
281
277
|
output_stream = output_container.add_stream("pcm_s16le", rate=sr)
|
282
278
|
|
283
279
|
chunk_size = sr # Process 1 second at a time
|
@@ -298,9 +294,9 @@ def mix_audio_files(sr: int, audio_paths: list[str], output_path: str) -> None:
|
|
298
294
|
def file_to_ndarray(src: FileInfo, stream: int, sr: int) -> np.ndarray:
|
299
295
|
all_frames = []
|
300
296
|
|
301
|
-
resampler =
|
297
|
+
resampler = av.AudioResampler(format="s16p", layout="stereo", rate=sr)
|
302
298
|
|
303
|
-
with
|
299
|
+
with av.open(src.path) as container:
|
304
300
|
for frame in container.decode(audio=stream):
|
305
301
|
for resampled_frame in resampler.resample(frame):
|
306
302
|
all_frames.append(resampled_frame.to_ndarray())
|
@@ -311,10 +307,10 @@ def file_to_ndarray(src: FileInfo, stream: int, sr: int) -> np.ndarray:
|
|
311
307
|
def ndarray_to_file(audio_data: np.ndarray, rate: int, out: str | Path) -> None:
|
312
308
|
layout = "stereo"
|
313
309
|
|
314
|
-
with
|
310
|
+
with av.open(out, mode="w") as output:
|
315
311
|
stream = output.add_stream("pcm_s16le", rate=rate, format="s16", layout=layout)
|
316
312
|
|
317
|
-
frame =
|
313
|
+
frame = AudioFrame.from_ndarray(audio_data, format="s16p", layout=layout)
|
318
314
|
frame.rate = rate
|
319
315
|
|
320
316
|
output.mux(stream.encode(frame))
|
@@ -322,11 +318,11 @@ def ndarray_to_file(audio_data: np.ndarray, rate: int, out: str | Path) -> None:
|
|
322
318
|
|
323
319
|
|
324
320
|
def ndarray_to_iter(
|
325
|
-
audio_data: np.ndarray, fmt:
|
321
|
+
audio_data: np.ndarray, fmt: av.AudioFormat, layout: str, rate: int
|
326
322
|
) -> Iterator[AudioFrame]:
|
327
323
|
chunk_size = rate // 4 # Process 0.25 seconds at a time
|
328
324
|
|
329
|
-
resampler =
|
325
|
+
resampler = av.AudioResampler(rate=rate, format=fmt, layout=layout)
|
330
326
|
for i in range(0, audio_data.shape[1], chunk_size):
|
331
327
|
chunk = audio_data[:, i : i + chunk_size]
|
332
328
|
|
@@ -338,15 +334,15 @@ def ndarray_to_iter(
|
|
338
334
|
|
339
335
|
|
340
336
|
def make_new_audio(
|
341
|
-
output:
|
342
|
-
audio_format:
|
337
|
+
output: av.container.OutputContainer,
|
338
|
+
audio_format: av.AudioFormat,
|
343
339
|
tl: v3,
|
344
340
|
args: Args,
|
345
341
|
log: Log,
|
346
|
-
) -> tuple[list[
|
342
|
+
) -> tuple[list[av.AudioStream], list[Iterator[AudioFrame]]]:
|
347
343
|
audio_inputs = []
|
348
344
|
audio_gen_frames = []
|
349
|
-
audio_streams: list[
|
345
|
+
audio_streams: list[av.AudioStream] = []
|
350
346
|
audio_paths = _make_new_audio(tl, audio_format, args, log)
|
351
347
|
|
352
348
|
for i, audio_path in enumerate(audio_paths):
|
@@ -357,7 +353,7 @@ def make_new_audio(
|
|
357
353
|
layout=tl.T.layout,
|
358
354
|
time_base=Fraction(1, tl.sr),
|
359
355
|
)
|
360
|
-
if not isinstance(audio_stream,
|
356
|
+
if not isinstance(audio_stream, av.AudioStream):
|
361
357
|
log.error(f"Not a known audio codec: {args.audio_codec}")
|
362
358
|
|
363
359
|
if args.audio_bitrate != "auto":
|
@@ -372,7 +368,7 @@ def make_new_audio(
|
|
372
368
|
audio_streams.append(audio_stream)
|
373
369
|
|
374
370
|
if isinstance(audio_path, str):
|
375
|
-
audio_input =
|
371
|
+
audio_input = av.open(audio_path)
|
376
372
|
audio_inputs.append(audio_input)
|
377
373
|
audio_gen_frames.append(audio_input.decode(audio=0))
|
378
374
|
else:
|
@@ -385,7 +381,7 @@ class Getter:
|
|
385
381
|
__slots__ = ("container", "stream", "rate")
|
386
382
|
|
387
383
|
def __init__(self, path: Path, stream: int, rate: int):
|
388
|
-
self.container =
|
384
|
+
self.container = av.open(path)
|
389
385
|
self.stream = self.container.streams.audio[stream]
|
390
386
|
self.rate = rate
|
391
387
|
|
@@ -394,7 +390,7 @@ class Getter:
|
|
394
390
|
|
395
391
|
container = self.container
|
396
392
|
stream = self.stream
|
397
|
-
resampler =
|
393
|
+
resampler = av.AudioResampler(format="s16p", layout="stereo", rate=self.rate)
|
398
394
|
|
399
395
|
time_base = stream.time_base
|
400
396
|
assert time_base is not None
|
@@ -436,10 +432,12 @@ class Getter:
|
|
436
432
|
return result # Return NumPy array with shape (channels, samples)
|
437
433
|
|
438
434
|
|
439
|
-
def _make_new_audio(
|
435
|
+
def _make_new_audio(
|
436
|
+
tl: v3, fmt: av.AudioFormat, args: Args, log: Log
|
437
|
+
) -> list[str | Iterator[AudioFrame]]:
|
440
438
|
sr = tl.sr
|
441
439
|
tb = tl.tb
|
442
|
-
output: list[
|
440
|
+
output: list[str | Iterator[AudioFrame]] = []
|
443
441
|
samples: dict[tuple[FileInfo, int], Getter] = {}
|
444
442
|
|
445
443
|
norm = parse_norm(args.audio_normalize, log)
|
@@ -449,7 +447,7 @@ def _make_new_audio(tl: v3, fmt: bv.AudioFormat, args: Args, log: Log) -> list[A
|
|
449
447
|
|
450
448
|
layout = tl.T.layout
|
451
449
|
try:
|
452
|
-
|
450
|
+
av.AudioLayout(layout)
|
453
451
|
except ValueError:
|
454
452
|
log.error(f"Invalid audio layout: {layout}")
|
455
453
|
|
@@ -511,7 +509,9 @@ def _make_new_audio(tl: v3, fmt: bv.AudioFormat, args: Args, log: Log) -> list[A
|
|
511
509
|
|
512
510
|
if args.mix_audio_streams and len(output) > 1:
|
513
511
|
new_a_file = f"{Path(log.temp, 'new_audio.wav')}"
|
514
|
-
|
512
|
+
# When mix_audio_streams is True, output only contains strings
|
513
|
+
audio_paths = cast(list[str], output)
|
514
|
+
mix_audio_files(sr, audio_paths, new_a_file)
|
515
515
|
return [new_a_file]
|
516
516
|
|
517
517
|
return output
|