auto-editor 27.0.0__py3-none-any.whl → 27.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 +8 -0
- auto_editor/cmds/desc.py +2 -2
- auto_editor/cmds/info.py +2 -2
- auto_editor/cmds/levels.py +2 -2
- auto_editor/cmds/repl.py +3 -8
- auto_editor/cmds/test.py +36 -2
- auto_editor/edit.py +37 -79
- auto_editor/ffwrapper.py +88 -84
- auto_editor/formats/fcp11.py +10 -8
- auto_editor/formats/fcp7.py +11 -12
- auto_editor/formats/json.py +8 -9
- auto_editor/lang/stdenv.py +1 -0
- auto_editor/make_layers.py +18 -8
- auto_editor/render/audio.py +219 -84
- auto_editor/render/video.py +1 -2
- auto_editor/timeline.py +60 -10
- auto_editor/utils/container.py +19 -12
- auto_editor/utils/func.py +21 -0
- {auto_editor-27.0.0.dist-info → auto_editor-27.1.0.dist-info}/METADATA +2 -2
- {auto_editor-27.0.0.dist-info → auto_editor-27.1.0.dist-info}/RECORD +25 -27
- {auto_editor-27.0.0.dist-info → auto_editor-27.1.0.dist-info}/WHEEL +1 -1
- auto_editor/output.py +0 -86
- auto_editor/wavfile.py +0 -310
- {auto_editor-27.0.0.dist-info → auto_editor-27.1.0.dist-info}/entry_points.txt +0 -0
- {auto_editor-27.0.0.dist-info → auto_editor-27.1.0.dist-info}/licenses/LICENSE +0 -0
- {auto_editor-27.0.0.dist-info → auto_editor-27.1.0.dist-info}/top_level.txt +0 -0
auto_editor/formats/fcp7.py
CHANGED
@@ -7,8 +7,8 @@ from math import ceil
|
|
7
7
|
from typing import TYPE_CHECKING
|
8
8
|
from xml.etree.ElementTree import Element
|
9
9
|
|
10
|
-
from auto_editor.ffwrapper import FileInfo
|
11
|
-
from auto_editor.timeline import ASpace, TlAudio, TlVideo, VSpace, v3
|
10
|
+
from auto_editor.ffwrapper import FileInfo
|
11
|
+
from auto_editor.timeline import ASpace, Template, TlAudio, TlVideo, VSpace, v3
|
12
12
|
|
13
13
|
from .utils import Validator, show
|
14
14
|
|
@@ -282,7 +282,7 @@ def fcp7_read_xml(path: str, log: Log) -> v3:
|
|
282
282
|
fileobj = valid.parse(clipitem["file"], {"pathurl": str})
|
283
283
|
|
284
284
|
if "pathurl" in fileobj:
|
285
|
-
sources[file_id] =
|
285
|
+
sources[file_id] = FileInfo.init(
|
286
286
|
uri_to_path(fileobj["pathurl"]),
|
287
287
|
log,
|
288
288
|
)
|
@@ -317,7 +317,7 @@ def fcp7_read_xml(path: str, log: Log) -> v3:
|
|
317
317
|
file_id = clipitem["file"].attrib["id"]
|
318
318
|
if file_id not in sources:
|
319
319
|
fileobj = valid.parse(clipitem["file"], {"pathurl": str})
|
320
|
-
sources[file_id] =
|
320
|
+
sources[file_id] = FileInfo.init(
|
321
321
|
uri_to_path(fileobj["pathurl"]), log
|
322
322
|
)
|
323
323
|
|
@@ -336,10 +336,8 @@ def fcp7_read_xml(path: str, log: Log) -> v3:
|
|
336
336
|
)
|
337
337
|
)
|
338
338
|
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
return v3(primary_src, tb, sr, res, "#000", vobjs, aobjs, v1=None)
|
339
|
+
T = Template.init(sources[next(iter(sources))], sr, res=res)
|
340
|
+
return v3(tb, "#000", T, vobjs, aobjs, v1=None)
|
343
341
|
|
344
342
|
|
345
343
|
def media_def(
|
@@ -424,13 +422,14 @@ def resolve_write_audio(audio: Element, make_filedef, tl: v3) -> None:
|
|
424
422
|
clipitem.append(speedup(aclip.speed * 100))
|
425
423
|
|
426
424
|
|
427
|
-
def premiere_write_audio(audio: Element, make_filedef,
|
425
|
+
def premiere_write_audio(audio: Element, make_filedef, tl: v3) -> None:
|
428
426
|
ET.SubElement(audio, "numOutputChannels").text = "2"
|
429
427
|
aformat = ET.SubElement(audio, "format")
|
430
428
|
aschar = ET.SubElement(aformat, "samplecharacteristics")
|
431
429
|
ET.SubElement(aschar, "depth").text = DEPTH
|
432
430
|
ET.SubElement(aschar, "samplerate").text = f"{tl.sr}"
|
433
431
|
|
432
|
+
has_video = tl.v and tl.v[0]
|
434
433
|
t = 0
|
435
434
|
for aclips in tl.a:
|
436
435
|
for channelcount in range(0, 2): # Because "stereo" is hardcoded.
|
@@ -442,7 +441,7 @@ def premiere_write_audio(audio: Element, make_filedef, src: FileInfo, tl: v3) ->
|
|
442
441
|
premiereTrackType="Stereo",
|
443
442
|
)
|
444
443
|
|
445
|
-
if
|
444
|
+
if has_video:
|
446
445
|
ET.SubElement(track, "outputchannelindex").text = f"{channelcount + 1}"
|
447
446
|
|
448
447
|
for j, aclip in enumerate(aclips):
|
@@ -453,7 +452,7 @@ def premiere_write_audio(audio: Element, make_filedef, src: FileInfo, tl: v3) ->
|
|
453
452
|
_in = f"{aclip.offset}"
|
454
453
|
_out = f"{aclip.offset + aclip.dur}"
|
455
454
|
|
456
|
-
if not
|
455
|
+
if not has_video:
|
457
456
|
clip_item_num = j + 1
|
458
457
|
else:
|
459
458
|
clip_item_num = len(aclips) + 1 + j + (t * len(aclips))
|
@@ -579,7 +578,7 @@ def fcp7_write_xml(name: str, output: str, resolve: bool, tl: v3) -> None:
|
|
579
578
|
if resolve:
|
580
579
|
resolve_write_audio(audio, make_filedef, tl)
|
581
580
|
else:
|
582
|
-
premiere_write_audio(audio, make_filedef,
|
581
|
+
premiere_write_audio(audio, make_filedef, tl)
|
583
582
|
|
584
583
|
tree = ET.ElementTree(xmeml)
|
585
584
|
ET.indent(tree, space=" ", level=0)
|
auto_editor/formats/json.py
CHANGED
@@ -6,11 +6,12 @@ from difflib import get_close_matches
|
|
6
6
|
from fractions import Fraction
|
7
7
|
from typing import Any
|
8
8
|
|
9
|
-
from auto_editor.ffwrapper import FileInfo
|
9
|
+
from auto_editor.ffwrapper import FileInfo
|
10
10
|
from auto_editor.json import dump, load
|
11
11
|
from auto_editor.lib.err import MyError
|
12
12
|
from auto_editor.timeline import (
|
13
13
|
ASpace,
|
14
|
+
Template,
|
14
15
|
TlAudio,
|
15
16
|
TlVideo,
|
16
17
|
VSpace,
|
@@ -59,7 +60,7 @@ def read_v3(tl: Any, log: Log) -> v3:
|
|
59
60
|
def make_src(v: str) -> FileInfo:
|
60
61
|
if v in srcs:
|
61
62
|
return srcs[v]
|
62
|
-
temp =
|
63
|
+
temp = FileInfo.init(v, log)
|
63
64
|
srcs[v] = temp
|
64
65
|
return temp
|
65
66
|
|
@@ -151,11 +152,11 @@ def read_v3(tl: Any, log: Log) -> v3:
|
|
151
152
|
a.append(a_out)
|
152
153
|
|
153
154
|
try:
|
154
|
-
|
155
|
+
T = Template.init(srcs[next(iter(srcs))])
|
155
156
|
except StopIteration:
|
156
|
-
|
157
|
+
T = Template(sr, "stereo", res, [], [])
|
157
158
|
|
158
|
-
return v3(
|
159
|
+
return v3(tb, bg, T, v, a, v1=None)
|
159
160
|
|
160
161
|
|
161
162
|
def read_v1(tl: Any, log: Log) -> v3:
|
@@ -168,7 +169,7 @@ def read_v1(tl: Any, log: Log) -> v3:
|
|
168
169
|
|
169
170
|
check_file(path, log)
|
170
171
|
|
171
|
-
src =
|
172
|
+
src = FileInfo.init(path, log)
|
172
173
|
|
173
174
|
vtl: VSpace = []
|
174
175
|
atl: ASpace = [[] for _ in range(len(src.audios))]
|
@@ -207,11 +208,9 @@ def read_v1(tl: Any, log: Log) -> v3:
|
|
207
208
|
atl[a].append(TlAudio(c.start, c.dur, c.src, c.offset, c.speed, 1, a))
|
208
209
|
|
209
210
|
return v3(
|
210
|
-
src,
|
211
211
|
src.get_fps(),
|
212
|
-
src.get_sr(),
|
213
|
-
src.get_res(),
|
214
212
|
"#000",
|
213
|
+
Template.init(src),
|
215
214
|
vtl,
|
216
215
|
atl,
|
217
216
|
v1(src, chunks),
|
auto_editor/lang/stdenv.py
CHANGED
auto_editor/make_layers.py
CHANGED
@@ -11,7 +11,7 @@ from auto_editor.ffwrapper import FileInfo
|
|
11
11
|
from auto_editor.lang.palet import Lexer, Parser, env, interpret, is_boolean_array
|
12
12
|
from auto_editor.lib.data_structs import print_str
|
13
13
|
from auto_editor.lib.err import MyError
|
14
|
-
from auto_editor.timeline import ASpace, TlAudio, TlVideo, VSpace, v1, v3
|
14
|
+
from auto_editor.timeline import ASpace, Template, TlAudio, TlVideo, VSpace, v1, v3
|
15
15
|
from auto_editor.utils.func import mut_margin
|
16
16
|
from auto_editor.utils.types import CoerceError, time
|
17
17
|
|
@@ -99,16 +99,17 @@ def parse_time(val: str, arr: NDArray, tb: Fraction) -> int: # raises: `CoerceE
|
|
99
99
|
|
100
100
|
|
101
101
|
def make_timeline(
|
102
|
-
sources: list[FileInfo],
|
103
|
-
args: Args,
|
104
|
-
sr: int,
|
105
|
-
bar: Bar,
|
106
|
-
log: Log,
|
102
|
+
sources: list[FileInfo], args: Args, sr: int, bar: Bar, log: Log
|
107
103
|
) -> v3:
|
108
104
|
inp = None if not sources else sources[0]
|
109
105
|
|
110
106
|
if inp is None:
|
111
|
-
tb
|
107
|
+
tb = (
|
108
|
+
Fraction(30)
|
109
|
+
if args.frame_rate is None
|
110
|
+
else make_sane_timebase(args.frame_rate)
|
111
|
+
)
|
112
|
+
res = (1920, 1080) if args.resolution is None else args.resolution
|
112
113
|
else:
|
113
114
|
tb = make_sane_timebase(
|
114
115
|
inp.get_fps() if args.frame_rate is None else args.frame_rate
|
@@ -302,4 +303,13 @@ def make_timeline(
|
|
302
303
|
else:
|
303
304
|
v1_compatiable = None
|
304
305
|
|
305
|
-
|
306
|
+
if len(vtl) == 0 and len(atl) == 0:
|
307
|
+
log.error("Timeline is empty, nothing to do.")
|
308
|
+
|
309
|
+
if inp is None:
|
310
|
+
layout = "stereo" if args.audio_layout is None else args.audio_layout
|
311
|
+
template = Template(sr, layout, res, [], [])
|
312
|
+
else:
|
313
|
+
template = Template.init(inp, sr, args.audio_layout, res)
|
314
|
+
|
315
|
+
return v3(tb, args.background, template, vtl, atl, v1_compatiable)
|
auto_editor/render/audio.py
CHANGED
@@ -1,11 +1,13 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
-
import
|
3
|
+
from fractions import Fraction
|
4
|
+
from io import BytesIO
|
4
5
|
from pathlib import Path
|
5
6
|
from typing import TYPE_CHECKING
|
6
7
|
|
7
8
|
import bv
|
8
9
|
import numpy as np
|
10
|
+
from bv import AudioFrame
|
9
11
|
from bv.filter.loudnorm import stats
|
10
12
|
|
11
13
|
from auto_editor.ffwrapper import FileInfo
|
@@ -13,17 +15,18 @@ from auto_editor.json import load
|
|
13
15
|
from auto_editor.lang.palet import env
|
14
16
|
from auto_editor.lib.contracts import andc, between_c, is_int_or_float
|
15
17
|
from auto_editor.lib.err import MyError
|
16
|
-
from auto_editor.output import Ensure
|
17
18
|
from auto_editor.timeline import TlAudio, v3
|
18
|
-
from auto_editor.utils.bar import Bar
|
19
19
|
from auto_editor.utils.cmdkw import ParserError, parse_with_palet, pAttr, pAttrs
|
20
|
-
from auto_editor.utils.
|
20
|
+
from auto_editor.utils.func import parse_bitrate
|
21
21
|
from auto_editor.utils.log import Log
|
22
|
-
from auto_editor.wavfile import AudioData, read, write
|
23
22
|
|
24
23
|
if TYPE_CHECKING:
|
24
|
+
from collections.abc import Iterator
|
25
|
+
from typing import Any
|
26
|
+
|
25
27
|
from auto_editor.__main__ import Args
|
26
28
|
|
29
|
+
|
27
30
|
norm_types = {
|
28
31
|
"ebu": pAttrs(
|
29
32
|
"ebu",
|
@@ -106,7 +109,7 @@ def apply_audio_normalization(
|
|
106
109
|
else:
|
107
110
|
assert "t" in norm
|
108
111
|
|
109
|
-
def get_peak_level(frame:
|
112
|
+
def get_peak_level(frame: AudioFrame) -> float:
|
110
113
|
# Calculate peak level in dB
|
111
114
|
# Should be equivalent to: -af astats=measure_overall=Peak_level:measure_perchannel=0
|
112
115
|
max_amplitude = np.abs(frame.to_ndarray()).max()
|
@@ -143,7 +146,7 @@ def apply_audio_normalization(
|
|
143
146
|
while True:
|
144
147
|
try:
|
145
148
|
aframe = graph.pull()
|
146
|
-
assert isinstance(aframe,
|
149
|
+
assert isinstance(aframe, AudioFrame)
|
147
150
|
output_file.mux(output_stream.encode(aframe))
|
148
151
|
except (bv.BlockingIOError, bv.EOFError):
|
149
152
|
break
|
@@ -152,20 +155,27 @@ def apply_audio_normalization(
|
|
152
155
|
output_file.close()
|
153
156
|
|
154
157
|
|
155
|
-
def process_audio_clip(
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
158
|
+
def process_audio_clip(clip: TlAudio, data: np.ndarray, sr: int) -> np.ndarray:
|
159
|
+
to_s16 = bv.AudioResampler(format="s16", layout="stereo", rate=sr)
|
160
|
+
input_buffer = BytesIO()
|
161
|
+
|
162
|
+
with bv.open(input_buffer, "w", format="wav") as container:
|
163
|
+
output_stream = container.add_stream(
|
164
|
+
"pcm_s16le", sample_rate=sr, format="s16", layout="stereo"
|
165
|
+
)
|
166
|
+
|
167
|
+
frame = AudioFrame.from_ndarray(data, format="s16p", layout="stereo")
|
168
|
+
frame.rate = sr
|
169
|
+
|
170
|
+
for reframe in to_s16.resample(frame):
|
171
|
+
container.mux(output_stream.encode(reframe))
|
172
|
+
container.mux(output_stream.encode(None))
|
173
|
+
|
160
174
|
input_buffer.seek(0)
|
161
175
|
|
162
176
|
input_file = bv.open(input_buffer, "r")
|
163
177
|
input_stream = input_file.streams.audio[0]
|
164
178
|
|
165
|
-
output_bytes = io.BytesIO()
|
166
|
-
output_file = bv.open(output_bytes, mode="w", format="wav")
|
167
|
-
output_stream = output_file.add_stream("pcm_s16le", rate=sr)
|
168
|
-
|
169
179
|
graph = bv.filter.Graph()
|
170
180
|
args = [graph.add_abuffer(template=input_stream)]
|
171
181
|
|
@@ -191,29 +201,23 @@ def process_audio_clip(
|
|
191
201
|
args.append(graph.add("abuffersink"))
|
192
202
|
graph.link_nodes(*args).configure()
|
193
203
|
|
204
|
+
all_frames = []
|
205
|
+
resampler = bv.AudioResampler(format="s16p", layout="stereo", rate=sr)
|
206
|
+
|
194
207
|
for frame in input_file.decode(input_stream):
|
195
208
|
graph.push(frame)
|
196
209
|
while True:
|
197
210
|
try:
|
198
211
|
aframe = graph.pull()
|
199
|
-
assert isinstance(aframe,
|
200
|
-
output_file.mux(output_stream.encode(aframe))
|
201
|
-
except (bv.BlockingIOError, bv.EOFError):
|
202
|
-
break
|
212
|
+
assert isinstance(aframe, AudioFrame)
|
203
213
|
|
204
|
-
|
205
|
-
|
214
|
+
for resampled_frame in resampler.resample(aframe):
|
215
|
+
all_frames.append(resampled_frame.to_ndarray())
|
206
216
|
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
output_bytes.seek(0)
|
211
|
-
has_filesig = output_bytes.read(4)
|
212
|
-
output_bytes.seek(0)
|
213
|
-
if not has_filesig: # Can rarely happen when clip is extremely small
|
214
|
-
return np.empty((0, 2), dtype=np.int16)
|
217
|
+
except (bv.BlockingIOError, bv.EOFError):
|
218
|
+
break
|
215
219
|
|
216
|
-
return
|
220
|
+
return np.concatenate(all_frames, axis=1)
|
217
221
|
|
218
222
|
|
219
223
|
def mix_audio_files(sr: int, audio_paths: list[str], output_path: str) -> None:
|
@@ -278,7 +282,7 @@ def mix_audio_files(sr: int, audio_paths: list[str], output_path: str) -> None:
|
|
278
282
|
# Shape becomes (1, samples) for mono
|
279
283
|
chunk = np.array([mixed_audio[i : i + chunk_size]])
|
280
284
|
|
281
|
-
frame =
|
285
|
+
frame = AudioFrame.from_ndarray(chunk, format="s16", layout="mono")
|
282
286
|
frame.rate = sr
|
283
287
|
frame.pts = i # Set presentation timestamp
|
284
288
|
|
@@ -288,92 +292,223 @@ def mix_audio_files(sr: int, audio_paths: list[str], output_path: str) -> None:
|
|
288
292
|
output_container.close()
|
289
293
|
|
290
294
|
|
295
|
+
def file_to_ndarray(src: FileInfo, stream: int, sr: int) -> np.ndarray:
|
296
|
+
all_frames = []
|
297
|
+
|
298
|
+
resampler = bv.AudioResampler(format="s16p", layout="stereo", rate=sr)
|
299
|
+
|
300
|
+
with bv.open(src.path) as container:
|
301
|
+
for frame in container.decode(audio=stream):
|
302
|
+
for resampled_frame in resampler.resample(frame):
|
303
|
+
all_frames.append(resampled_frame.to_ndarray())
|
304
|
+
|
305
|
+
return np.concatenate(all_frames, axis=1)
|
306
|
+
|
307
|
+
|
308
|
+
def ndarray_to_file(audio_data: np.ndarray, rate: int, out: str | Path) -> None:
|
309
|
+
layout = "stereo"
|
310
|
+
|
311
|
+
with bv.open(out, mode="w") as output:
|
312
|
+
stream = output.add_stream("pcm_s16le", rate=rate, format="s16", layout=layout)
|
313
|
+
|
314
|
+
frame = bv.AudioFrame.from_ndarray(audio_data, format="s16p", layout=layout)
|
315
|
+
frame.rate = rate
|
316
|
+
|
317
|
+
output.mux(stream.encode(frame))
|
318
|
+
output.mux(stream.encode(None))
|
319
|
+
|
320
|
+
|
321
|
+
def ndarray_to_iter(
|
322
|
+
audio_data: np.ndarray, fmt: bv.AudioFormat, layout: str, rate: int
|
323
|
+
) -> Iterator[AudioFrame]:
|
324
|
+
chunk_size = rate // 4 # Process 0.25 seconds at a time
|
325
|
+
|
326
|
+
resampler = bv.AudioResampler(rate=rate, format=fmt, layout=layout)
|
327
|
+
for i in range(0, audio_data.shape[1], chunk_size):
|
328
|
+
chunk = audio_data[:, i : i + chunk_size]
|
329
|
+
|
330
|
+
frame = AudioFrame.from_ndarray(chunk, format="s16p", layout="stereo")
|
331
|
+
frame.rate = rate
|
332
|
+
frame.pts = i
|
333
|
+
|
334
|
+
yield from resampler.resample(frame)
|
335
|
+
|
336
|
+
|
291
337
|
def make_new_audio(
|
292
|
-
|
293
|
-
|
338
|
+
output: bv.container.OutputContainer,
|
339
|
+
audio_format: bv.AudioFormat,
|
340
|
+
tl: v3,
|
341
|
+
args: Args,
|
342
|
+
log: Log,
|
343
|
+
) -> tuple[list[bv.AudioStream], list[Iterator[AudioFrame]]]:
|
344
|
+
audio_inputs = []
|
345
|
+
audio_gen_frames = []
|
346
|
+
audio_streams: list[bv.AudioStream] = []
|
347
|
+
audio_paths = _make_new_audio(tl, audio_format, args, log)
|
348
|
+
|
349
|
+
for i, audio_path in enumerate(audio_paths):
|
350
|
+
audio_stream = output.add_stream(
|
351
|
+
args.audio_codec,
|
352
|
+
rate=tl.sr,
|
353
|
+
format=audio_format,
|
354
|
+
layout=tl.T.layout,
|
355
|
+
time_base=Fraction(1, tl.sr),
|
356
|
+
)
|
357
|
+
if not isinstance(audio_stream, bv.AudioStream):
|
358
|
+
log.error(f"Not a known audio codec: {args.audio_codec}")
|
359
|
+
|
360
|
+
if args.audio_bitrate != "auto":
|
361
|
+
audio_stream.bit_rate = parse_bitrate(args.audio_bitrate, log)
|
362
|
+
log.debug(f"audio bitrate: {audio_stream.bit_rate}")
|
363
|
+
else:
|
364
|
+
log.debug(f"[auto] audio bitrate: {audio_stream.bit_rate}")
|
365
|
+
|
366
|
+
if i < len(tl.T.audios) and (lang := tl.T.audios[i].lang) is not None:
|
367
|
+
audio_stream.metadata["language"] = lang
|
368
|
+
|
369
|
+
audio_streams.append(audio_stream)
|
370
|
+
|
371
|
+
if isinstance(audio_path, str):
|
372
|
+
audio_input = bv.open(audio_path)
|
373
|
+
audio_inputs.append(audio_input)
|
374
|
+
audio_gen_frames.append(audio_input.decode(audio=0))
|
375
|
+
else:
|
376
|
+
audio_gen_frames.append(audio_path)
|
377
|
+
|
378
|
+
return audio_streams, audio_gen_frames
|
379
|
+
|
380
|
+
|
381
|
+
class Getter:
|
382
|
+
__slots__ = ("container", "stream", "rate")
|
383
|
+
|
384
|
+
def __init__(self, path: Path, stream: int, rate: int):
|
385
|
+
self.container = bv.open(path)
|
386
|
+
self.stream = self.container.streams.audio[0]
|
387
|
+
self.rate = rate
|
388
|
+
|
389
|
+
def get(self, start: int, end: int) -> np.ndarray:
|
390
|
+
# start/end is in samples
|
391
|
+
|
392
|
+
container = self.container
|
393
|
+
stream = self.stream
|
394
|
+
resampler = bv.AudioResampler(format="s16p", layout="stereo", rate=self.rate)
|
395
|
+
|
396
|
+
time_base = stream.time_base
|
397
|
+
assert time_base is not None
|
398
|
+
start_pts = int(start / self.rate / time_base)
|
399
|
+
|
400
|
+
# Seek to the approximate position
|
401
|
+
container.seek(start_pts, stream=stream)
|
402
|
+
|
403
|
+
all_frames = []
|
404
|
+
total_samples = 0
|
405
|
+
target_samples = end - start
|
406
|
+
|
407
|
+
# Decode frames until we have enough samples
|
408
|
+
for frame in container.decode(stream):
|
409
|
+
for resampled_frame in resampler.resample(frame):
|
410
|
+
frame_array = resampled_frame.to_ndarray()
|
411
|
+
all_frames.append(frame_array)
|
412
|
+
total_samples += frame_array.shape[1]
|
413
|
+
|
414
|
+
if total_samples >= target_samples:
|
415
|
+
break
|
416
|
+
|
417
|
+
if total_samples >= target_samples:
|
418
|
+
break
|
419
|
+
|
420
|
+
result = np.concatenate(all_frames, axis=1)
|
421
|
+
|
422
|
+
# Trim to exact size
|
423
|
+
if result.shape[1] > target_samples:
|
424
|
+
result = result[:, :target_samples]
|
425
|
+
elif result.shape[1] < target_samples:
|
426
|
+
# Pad with zeros if we don't have enough samples
|
427
|
+
padding = np.zeros(
|
428
|
+
(result.shape[0], target_samples - result.shape[1]), dtype=result.dtype
|
429
|
+
)
|
430
|
+
result = np.concatenate([result, padding], axis=1)
|
431
|
+
|
432
|
+
assert result.shape[1] == end - start
|
433
|
+
return result # Return NumPy array with shape (channels, samples)
|
434
|
+
|
435
|
+
|
436
|
+
def _make_new_audio(tl: v3, fmt: bv.AudioFormat, args: Args, log: Log) -> list[Any]:
|
294
437
|
sr = tl.sr
|
295
438
|
tb = tl.tb
|
296
|
-
output: list[
|
297
|
-
samples: dict[tuple[FileInfo, int],
|
439
|
+
output: list[Any] = []
|
440
|
+
samples: dict[tuple[FileInfo, int], Getter] = {}
|
298
441
|
|
299
442
|
norm = parse_norm(args.audio_normalize, log)
|
300
443
|
|
301
|
-
|
302
|
-
|
303
|
-
if not tl.a or not tl.a[0]:
|
444
|
+
if not tl.a[0]:
|
304
445
|
log.error("Trying to render empty audio timeline")
|
305
446
|
|
306
|
-
|
307
|
-
|
447
|
+
layout = tl.T.layout
|
448
|
+
try:
|
449
|
+
bv.AudioLayout(layout)
|
450
|
+
except ValueError:
|
451
|
+
log.error(f"Invalid audio layout: {layout}")
|
308
452
|
|
309
|
-
|
310
|
-
|
311
|
-
|
453
|
+
for i, layer in enumerate(tl.a):
|
454
|
+
arr: np.ndarray | None = None
|
455
|
+
use_iter = False
|
312
456
|
|
313
457
|
for c, clip in enumerate(layer):
|
314
458
|
if (clip.src, clip.stream) not in samples:
|
315
|
-
|
316
|
-
|
317
|
-
|
459
|
+
samples[(clip.src, clip.stream)] = Getter(
|
460
|
+
clip.src.path, clip.stream, sr
|
461
|
+
)
|
318
462
|
|
463
|
+
log.conwrite("Creating audio")
|
319
464
|
if arr is None:
|
320
465
|
leng = max(round((layer[-1].start + layer[-1].dur) * sr / tb), sr // tb)
|
321
|
-
|
322
|
-
|
323
|
-
dtype = _samp_arr.dtype
|
324
|
-
break
|
466
|
+
map_path = Path(log.temp, f"{i}.map")
|
467
|
+
arr = np.memmap(map_path, mode="w+", dtype=np.int16, shape=(2, leng))
|
325
468
|
|
326
|
-
arr = np.memmap(
|
327
|
-
Path(temp, "asdf.map"),
|
328
|
-
mode="w+",
|
329
|
-
dtype=dtype,
|
330
|
-
shape=(leng, 2),
|
331
|
-
)
|
332
|
-
del leng
|
333
|
-
|
334
|
-
samp_list = samples[(clip.src, clip.stream)]
|
335
469
|
samp_start = round(clip.offset * clip.speed * sr / tb)
|
336
470
|
samp_end = round((clip.offset + clip.dur) * clip.speed * sr / tb)
|
337
|
-
|
338
|
-
|
471
|
+
|
472
|
+
getter = samples[(clip.src, clip.stream)]
|
339
473
|
|
340
474
|
if clip.speed != 1 or clip.volume != 1:
|
341
|
-
clip_arr = process_audio_clip(
|
475
|
+
clip_arr = process_audio_clip(
|
476
|
+
clip, getter.get(samp_start, samp_end), sr
|
477
|
+
)
|
342
478
|
else:
|
343
|
-
clip_arr =
|
479
|
+
clip_arr = getter.get(samp_start, samp_end)
|
344
480
|
|
345
481
|
# Mix numpy arrays
|
346
482
|
start = clip.start * sr // tb
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
arr[start:] += clip_arr[: len(arr) - start]
|
483
|
+
clip_samples = clip_arr.shape[1]
|
484
|
+
if start + clip_samples > arr.shape[1]:
|
485
|
+
# Shorten `clip_arr` if bigger than expected.
|
486
|
+
arr[:, start:] += clip_arr[:, : arr.shape[1] - start]
|
352
487
|
else:
|
353
|
-
arr[start : start +
|
354
|
-
|
355
|
-
bar.tick(c)
|
488
|
+
arr[:, start : start + clip_samples] += clip_arr
|
356
489
|
|
357
490
|
if arr is not None:
|
358
491
|
if norm is None:
|
359
|
-
|
360
|
-
|
492
|
+
if args.mix_audio_streams:
|
493
|
+
path = Path(log.temp, f"new{i}.wav")
|
494
|
+
ndarray_to_file(arr, sr, path)
|
495
|
+
output.append(f"{path}")
|
496
|
+
else:
|
497
|
+
use_iter = True
|
361
498
|
else:
|
362
|
-
|
363
|
-
|
364
|
-
write(fid, sr, arr)
|
499
|
+
path = Path(log.temp, f"new{i}.wav")
|
500
|
+
pre_master = Path(log.temp, "premaster.wav")
|
365
501
|
|
502
|
+
ndarray_to_file(arr, sr, pre_master)
|
366
503
|
apply_audio_normalization(norm, pre_master, path, log)
|
504
|
+
output.append(f"{path}")
|
367
505
|
|
368
|
-
|
369
|
-
|
370
|
-
try:
|
371
|
-
Path(temp, "asdf.map").unlink(missing_ok=True)
|
372
|
-
except PermissionError:
|
373
|
-
pass
|
506
|
+
if use_iter and arr is not None:
|
507
|
+
output.append(ndarray_to_iter(arr, fmt, layout, sr))
|
374
508
|
|
375
509
|
if args.mix_audio_streams and len(output) > 1:
|
376
|
-
new_a_file = f"{Path(temp, 'new_audio.wav')}"
|
510
|
+
new_a_file = f"{Path(log.temp, 'new_audio.wav')}"
|
377
511
|
mix_audio_files(sr, output, new_a_file)
|
378
512
|
return [new_a_file]
|
513
|
+
|
379
514
|
return output
|
auto_editor/render/video.py
CHANGED
@@ -6,8 +6,8 @@ from typing import TYPE_CHECKING
|
|
6
6
|
import bv
|
7
7
|
import numpy as np
|
8
8
|
|
9
|
-
from auto_editor.output import parse_bitrate
|
10
9
|
from auto_editor.timeline import TlImage, TlRect, TlVideo
|
10
|
+
from auto_editor.utils.func import parse_bitrate
|
11
11
|
|
12
12
|
if TYPE_CHECKING:
|
13
13
|
from collections.abc import Iterator
|
@@ -61,7 +61,6 @@ def render_av(
|
|
61
61
|
) -> Iterator[tuple[int, bv.VideoFrame]]:
|
62
62
|
from_ndarray = bv.VideoFrame.from_ndarray
|
63
63
|
|
64
|
-
src = tl.src
|
65
64
|
cns: dict[FileInfo, bv.container.InputContainer] = {}
|
66
65
|
decoders: dict[FileInfo, Iterator[bv.VideoFrame]] = {}
|
67
66
|
seek_cost: dict[FileInfo, int] = {}
|