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.
@@ -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, initFileInfo
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] = initFileInfo(
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] = initFileInfo(
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
- primary_src = sources[next(iter(sources))]
340
- assert type(primary_src) is FileInfo
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, src: FileInfo, tl: v3) -> None:
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 src.videos:
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 src.videos:
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, src, tl)
581
+ premiere_write_audio(audio, make_filedef, tl)
583
582
 
584
583
  tree = ET.ElementTree(xmeml)
585
584
  ET.indent(tree, space=" ", level=0)
@@ -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, initFileInfo
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 = initFileInfo(v, log)
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
- src = srcs[next(iter(srcs))]
155
+ T = Template.init(srcs[next(iter(srcs))])
155
156
  except StopIteration:
156
- src = None
157
+ T = Template(sr, "stereo", res, [], [])
157
158
 
158
- return v3(src, tb, sr, res, bg, v, a, v1=None)
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 = initFileInfo(path, log)
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),
@@ -13,6 +13,7 @@ from .palet import Syntax, env, is_boolarr, is_iterable, my_eval, p_slice, raise
13
13
  if TYPE_CHECKING:
14
14
  from typing import Any, Literal
15
15
 
16
+ import numpy as np
16
17
  from numpy.typing import NDArray
17
18
 
18
19
  Number = int | float | complex | Fraction
@@ -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, res = Fraction(30), (1920, 1080)
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
- return v3(inp, tb, sr, res, args.background, vtl, atl, v1_compatiable)
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)
@@ -1,11 +1,13 @@
1
1
  from __future__ import annotations
2
2
 
3
- import io
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.container import Container
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: bv.AudioFrame) -> float:
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, bv.AudioFrame)
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
- clip: TlAudio, samp_list: AudioData, samp_start: int, samp_end: int, sr: int
157
- ) -> AudioData:
158
- input_buffer = io.BytesIO()
159
- write(input_buffer, sr, samp_list[samp_start:samp_end])
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, bv.AudioFrame)
200
- output_file.mux(output_stream.encode(aframe))
201
- except (bv.BlockingIOError, bv.EOFError):
202
- break
212
+ assert isinstance(aframe, AudioFrame)
203
213
 
204
- # Flush the stream
205
- output_file.mux(output_stream.encode(None))
214
+ for resampled_frame in resampler.resample(aframe):
215
+ all_frames.append(resampled_frame.to_ndarray())
206
216
 
207
- input_file.close()
208
- output_file.close()
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 read(output_bytes)[1]
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 = bv.AudioFrame.from_ndarray(chunk, format="s16", layout="mono")
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
- tl: v3, ctr: Container, ensure: Ensure, args: Args, bar: Bar, log: Log
293
- ) -> list[str]:
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[str] = []
297
- samples: dict[tuple[FileInfo, int], AudioData] = {}
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
- temp = log.temp
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
- for i, layer in enumerate(tl.a):
307
- bar.start(len(layer), "Creating new audio")
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
- path = Path(temp, f"new{i}.wav")
310
- output.append(f"{path}")
311
- arr: AudioData | None = None
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
- audio_path = ensure.audio(clip.src, clip.stream)
316
- with open(audio_path, "rb") as file:
317
- samples[(clip.src, clip.stream)] = read(file)[1]
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
- dtype = np.int32
322
- for _samp_arr in samples.values():
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
- if samp_end > len(samp_list):
338
- samp_end = len(samp_list)
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(clip, samp_list, samp_start, samp_end, sr)
475
+ clip_arr = process_audio_clip(
476
+ clip, getter.get(samp_start, samp_end), sr
477
+ )
342
478
  else:
343
- clip_arr = samp_list[samp_start:samp_end]
479
+ clip_arr = getter.get(samp_start, samp_end)
344
480
 
345
481
  # Mix numpy arrays
346
482
  start = clip.start * sr // tb
347
- car_len = clip_arr.shape[0]
348
-
349
- if start + car_len > len(arr):
350
- # Clip 'clip_arr' if bigger than expected.
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 + car_len] += clip_arr
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
- with open(path, "wb") as fid:
360
- write(fid, sr, arr)
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
- pre_master = Path(temp, "premaster.wav")
363
- with open(pre_master, "wb") as fid:
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
- bar.end()
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
@@ -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] = {}