auto-editor 27.0.0__tar.gz → 27.1.0__tar.gz

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.
Files changed (63) hide show
  1. {auto_editor-27.0.0 → auto_editor-27.1.0}/PKG-INFO +2 -2
  2. auto_editor-27.1.0/auto_editor/__init__.py +1 -0
  3. {auto_editor-27.0.0 → auto_editor-27.1.0}/auto_editor/__main__.py +8 -0
  4. {auto_editor-27.0.0 → auto_editor-27.1.0}/auto_editor/cmds/desc.py +2 -2
  5. {auto_editor-27.0.0 → auto_editor-27.1.0}/auto_editor/cmds/info.py +2 -2
  6. {auto_editor-27.0.0 → auto_editor-27.1.0}/auto_editor/cmds/levels.py +2 -2
  7. {auto_editor-27.0.0 → auto_editor-27.1.0}/auto_editor/cmds/repl.py +3 -8
  8. {auto_editor-27.0.0 → auto_editor-27.1.0}/auto_editor/cmds/test.py +36 -2
  9. {auto_editor-27.0.0 → auto_editor-27.1.0}/auto_editor/edit.py +37 -79
  10. auto_editor-27.1.0/auto_editor/ffwrapper.py +178 -0
  11. {auto_editor-27.0.0 → auto_editor-27.1.0}/auto_editor/formats/fcp11.py +10 -8
  12. {auto_editor-27.0.0 → auto_editor-27.1.0}/auto_editor/formats/fcp7.py +11 -12
  13. {auto_editor-27.0.0 → auto_editor-27.1.0}/auto_editor/formats/json.py +8 -9
  14. {auto_editor-27.0.0 → auto_editor-27.1.0}/auto_editor/lang/stdenv.py +1 -0
  15. {auto_editor-27.0.0 → auto_editor-27.1.0}/auto_editor/make_layers.py +18 -8
  16. {auto_editor-27.0.0 → auto_editor-27.1.0}/auto_editor/render/audio.py +219 -84
  17. {auto_editor-27.0.0 → auto_editor-27.1.0}/auto_editor/render/video.py +1 -2
  18. {auto_editor-27.0.0 → auto_editor-27.1.0}/auto_editor/timeline.py +60 -10
  19. {auto_editor-27.0.0 → auto_editor-27.1.0}/auto_editor/utils/container.py +19 -12
  20. {auto_editor-27.0.0 → auto_editor-27.1.0}/auto_editor/utils/func.py +21 -0
  21. {auto_editor-27.0.0 → auto_editor-27.1.0}/auto_editor.egg-info/PKG-INFO +2 -2
  22. {auto_editor-27.0.0 → auto_editor-27.1.0}/auto_editor.egg-info/SOURCES.txt +0 -2
  23. {auto_editor-27.0.0 → auto_editor-27.1.0}/pyproject.toml +2 -2
  24. auto_editor-27.0.0/auto_editor/__init__.py +0 -1
  25. auto_editor-27.0.0/auto_editor/ffwrapper.py +0 -174
  26. auto_editor-27.0.0/auto_editor/output.py +0 -86
  27. auto_editor-27.0.0/auto_editor/wavfile.py +0 -310
  28. {auto_editor-27.0.0 → auto_editor-27.1.0}/LICENSE +0 -0
  29. {auto_editor-27.0.0 → auto_editor-27.1.0}/README.md +0 -0
  30. {auto_editor-27.0.0 → auto_editor-27.1.0}/auto_editor/analyze.py +0 -0
  31. {auto_editor-27.0.0 → auto_editor-27.1.0}/auto_editor/cmds/__init__.py +0 -0
  32. {auto_editor-27.0.0 → auto_editor-27.1.0}/auto_editor/cmds/cache.py +0 -0
  33. {auto_editor-27.0.0 → auto_editor-27.1.0}/auto_editor/cmds/palet.py +0 -0
  34. {auto_editor-27.0.0 → auto_editor-27.1.0}/auto_editor/cmds/subdump.py +0 -0
  35. {auto_editor-27.0.0 → auto_editor-27.1.0}/auto_editor/formats/__init__.py +0 -0
  36. {auto_editor-27.0.0 → auto_editor-27.1.0}/auto_editor/formats/shotcut.py +0 -0
  37. {auto_editor-27.0.0 → auto_editor-27.1.0}/auto_editor/formats/utils.py +0 -0
  38. {auto_editor-27.0.0 → auto_editor-27.1.0}/auto_editor/help.py +0 -0
  39. {auto_editor-27.0.0 → auto_editor-27.1.0}/auto_editor/json.py +0 -0
  40. {auto_editor-27.0.0 → auto_editor-27.1.0}/auto_editor/lang/__init__.py +0 -0
  41. {auto_editor-27.0.0 → auto_editor-27.1.0}/auto_editor/lang/libintrospection.py +0 -0
  42. {auto_editor-27.0.0 → auto_editor-27.1.0}/auto_editor/lang/libmath.py +0 -0
  43. {auto_editor-27.0.0 → auto_editor-27.1.0}/auto_editor/lang/palet.py +0 -0
  44. {auto_editor-27.0.0 → auto_editor-27.1.0}/auto_editor/lib/__init__.py +0 -0
  45. {auto_editor-27.0.0 → auto_editor-27.1.0}/auto_editor/lib/contracts.py +0 -0
  46. {auto_editor-27.0.0 → auto_editor-27.1.0}/auto_editor/lib/data_structs.py +0 -0
  47. {auto_editor-27.0.0 → auto_editor-27.1.0}/auto_editor/lib/err.py +0 -0
  48. {auto_editor-27.0.0 → auto_editor-27.1.0}/auto_editor/preview.py +0 -0
  49. {auto_editor-27.0.0 → auto_editor-27.1.0}/auto_editor/render/__init__.py +0 -0
  50. {auto_editor-27.0.0 → auto_editor-27.1.0}/auto_editor/render/subtitle.py +0 -0
  51. {auto_editor-27.0.0 → auto_editor-27.1.0}/auto_editor/utils/__init__.py +0 -0
  52. {auto_editor-27.0.0 → auto_editor-27.1.0}/auto_editor/utils/bar.py +0 -0
  53. {auto_editor-27.0.0 → auto_editor-27.1.0}/auto_editor/utils/chunks.py +0 -0
  54. {auto_editor-27.0.0 → auto_editor-27.1.0}/auto_editor/utils/cmdkw.py +0 -0
  55. {auto_editor-27.0.0 → auto_editor-27.1.0}/auto_editor/utils/log.py +0 -0
  56. {auto_editor-27.0.0 → auto_editor-27.1.0}/auto_editor/utils/types.py +0 -0
  57. {auto_editor-27.0.0 → auto_editor-27.1.0}/auto_editor/vanparse.py +0 -0
  58. {auto_editor-27.0.0 → auto_editor-27.1.0}/auto_editor.egg-info/dependency_links.txt +0 -0
  59. {auto_editor-27.0.0 → auto_editor-27.1.0}/auto_editor.egg-info/entry_points.txt +0 -0
  60. {auto_editor-27.0.0 → auto_editor-27.1.0}/auto_editor.egg-info/requires.txt +0 -0
  61. {auto_editor-27.0.0 → auto_editor-27.1.0}/auto_editor.egg-info/top_level.txt +0 -0
  62. {auto_editor-27.0.0 → auto_editor-27.1.0}/docs/build.py +0 -0
  63. {auto_editor-27.0.0 → auto_editor-27.1.0}/setup.cfg +0 -0
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: auto-editor
3
- Version: 27.0.0
3
+ Version: 27.1.0
4
4
  Summary: Auto-Editor: Effort free video editing!
5
5
  Author-email: WyattBlue <wyattblue@auto-editor.com>
6
- License: Unlicense
6
+ License-Expression: Unlicense
7
7
  Project-URL: Bug Tracker, https://github.com/WyattBlue/auto-editor/issues
8
8
  Project-URL: Source Code, https://github.com/WyattBlue/auto-editor
9
9
  Project-URL: homepage, https://auto-editor.com
@@ -0,0 +1 @@
1
+ __version__ = "27.1.0"
@@ -75,6 +75,7 @@ class Args:
75
75
 
76
76
  # Audio Rendering
77
77
  audio_codec: str = "auto"
78
+ audio_layout: str | None = None
78
79
  audio_bitrate: str = "auto"
79
80
  mix_audio_streams: bool = False
80
81
  keep_tracks_separate: bool = False
@@ -336,6 +337,13 @@ def main_options(parser: ArgumentParser) -> ArgumentParser:
336
337
  metavar="ENCODER",
337
338
  help="Set audio codec for output media",
338
339
  )
340
+ parser.add_argument(
341
+ "--audio-layout",
342
+ "-channel-layout",
343
+ "-layout",
344
+ metavar="LAYOUT",
345
+ help="Set the audio layout for the output media/timeline",
346
+ )
339
347
  parser.add_argument(
340
348
  "--audio-bitrate",
341
349
  "-b:a",
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
  import sys
4
4
  from dataclasses import dataclass, field
5
5
 
6
- from auto_editor.ffwrapper import initFileInfo
6
+ from auto_editor.ffwrapper import FileInfo
7
7
  from auto_editor.utils.log import Log
8
8
  from auto_editor.vanparse import ArgumentParser
9
9
 
@@ -22,7 +22,7 @@ def desc_options(parser: ArgumentParser) -> ArgumentParser:
22
22
  def main(sys_args: list[str] = sys.argv[1:]) -> None:
23
23
  args = desc_options(ArgumentParser("desc")).parse_args(DescArgs, sys_args)
24
24
  for path in args.input:
25
- src = initFileInfo(path, Log())
25
+ src = FileInfo.init(path, Log())
26
26
  if src.description is not None:
27
27
  sys.stdout.write(f"\n{src.description}\n\n")
28
28
  else:
@@ -5,7 +5,7 @@ import sys
5
5
  from dataclasses import dataclass, field
6
6
  from typing import Any, Literal, TypedDict
7
7
 
8
- from auto_editor.ffwrapper import initFileInfo
8
+ from auto_editor.ffwrapper import FileInfo
9
9
  from auto_editor.json import dump
10
10
  from auto_editor.make_layers import make_sane_timebase
11
11
  from auto_editor.timeline import v3
@@ -102,7 +102,7 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
102
102
  file_info[file] = {"type": "timeline"}
103
103
  continue
104
104
 
105
- src = initFileInfo(file, log)
105
+ src = FileInfo.init(file, log)
106
106
 
107
107
  if len(src.videos) + len(src.audios) + len(src.subtitles) == 0:
108
108
  file_info[file] = {"type": "unknown"}
@@ -9,7 +9,7 @@ import bv
9
9
  import numpy as np
10
10
 
11
11
  from auto_editor.analyze import *
12
- from auto_editor.ffwrapper import initFileInfo
12
+ from auto_editor.ffwrapper import FileInfo
13
13
  from auto_editor.lang.palet import env
14
14
  from auto_editor.lib.contracts import is_bool, is_nat, is_nat1, is_str, is_void, orc
15
15
  from auto_editor.utils.bar import initBar
@@ -87,7 +87,7 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
87
87
  bar = initBar("none")
88
88
  log = Log(quiet=True)
89
89
 
90
- sources = [initFileInfo(path, log) for path in args.input]
90
+ sources = [FileInfo.init(path, log) for path in args.input]
91
91
  if len(sources) < 1:
92
92
  log.error("levels needs at least one input file")
93
93
 
@@ -7,7 +7,7 @@ from os import environ
7
7
 
8
8
  import auto_editor
9
9
  from auto_editor.analyze import initLevels
10
- from auto_editor.ffwrapper import initFileInfo
10
+ from auto_editor.ffwrapper import FileInfo
11
11
  from auto_editor.lang.palet import ClosingError, Lexer, Parser, env, interpret
12
12
  from auto_editor.lang.stdenv import make_standard_env
13
13
  from auto_editor.lib.data_structs import print_str
@@ -48,11 +48,6 @@ def repl_options(parser: ArgumentParser) -> ArgumentParser:
48
48
  type=frame_rate,
49
49
  help="Set custom timebase",
50
50
  )
51
- parser.add_argument(
52
- "--temp-dir",
53
- metavar="PATH",
54
- help="Set where the temporary directory is located",
55
- )
56
51
  return parser
57
52
 
58
53
 
@@ -60,8 +55,8 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
60
55
  args = repl_options(ArgumentParser(None)).parse_args(REPL_Args, sys_args)
61
56
 
62
57
  if args.input:
63
- log = Log(quiet=True, temp_dir=args.temp_dir)
64
- sources = [initFileInfo(path, log) for path in args.input]
58
+ log = Log(quiet=True)
59
+ sources = [FileInfo.init(path, log) for path in args.input]
65
60
  src = sources[0]
66
61
  tb = src.get_fps() if args.timebase is None else args.timebase
67
62
  env["timebase"] = tb
@@ -14,7 +14,7 @@ from time import perf_counter
14
14
  import bv
15
15
  import numpy as np
16
16
 
17
- from auto_editor.ffwrapper import FileInfo, initFileInfo
17
+ from auto_editor.ffwrapper import FileInfo
18
18
  from auto_editor.lang.palet import Lexer, Parser, env, interpret
19
19
  from auto_editor.lang.stdenv import make_standard_env
20
20
  from auto_editor.lib.data_structs import Char
@@ -62,7 +62,7 @@ log = Log()
62
62
 
63
63
 
64
64
  def fileinfo(path: str) -> FileInfo:
65
- return initFileInfo(path, log)
65
+ return FileInfo.init(path, log)
66
66
 
67
67
 
68
68
  def calculate_sha256(filename: str) -> str:
@@ -186,6 +186,7 @@ class Runner:
186
186
  assert container.duration is not None
187
187
  assert container.duration > 17300000 and container.duration < 2 << 24
188
188
 
189
+ assert len(container.streams) == 2
189
190
  video = container.streams[0]
190
191
  audio = container.streams[1]
191
192
  assert isinstance(video, bv.VideoStream)
@@ -199,6 +200,29 @@ class Runner:
199
200
  assert audio.codec.name == "aac"
200
201
  assert audio.sample_rate == 48000
201
202
  assert audio.language == "eng"
203
+ assert audio.layout.name == "stereo"
204
+
205
+ def test_to_mono(self) -> None:
206
+ out = self.main(["example.mp4"], ["-layout", "mono"], output="example_mono.mp4")
207
+ with bv.open(out) as container:
208
+ assert container.duration is not None
209
+ assert container.duration > 17300000 and container.duration < 2 << 24
210
+
211
+ assert len(container.streams) == 2
212
+ video = container.streams[0]
213
+ audio = container.streams[1]
214
+ assert isinstance(video, bv.VideoStream)
215
+ assert isinstance(audio, bv.AudioStream)
216
+ assert video.base_rate == 30
217
+ assert video.average_rate is not None
218
+ assert video.average_rate == 30, video.average_rate
219
+ assert (video.width, video.height) == (1280, 720)
220
+ assert video.codec.name == "h264"
221
+ assert video.language == "eng"
222
+ assert audio.codec.name == "aac"
223
+ assert audio.sample_rate == 48000
224
+ assert audio.language == "eng"
225
+ assert audio.layout.name == "mono"
202
226
 
203
227
  # PR #260
204
228
  def test_high_speed(self):
@@ -332,8 +356,18 @@ class Runner:
332
356
  assert cn.videos[0].height == 380
333
357
  assert cn.audios[0].samplerate == 48000
334
358
 
359
+ # def test_premiere_multi(self):
360
+ # p_xml = self.main([f"resources/multi-track.mov"], ["-exp"], "multi.xml")
361
+
362
+ # cn = fileinfo(self.main([p_xml], []))
363
+ # assert len(cn.videos) == 1
364
+ # assert len(cn.audios) == 2
365
+
335
366
  def test_premiere(self):
336
367
  for test_name in all_files:
368
+ if test_name == "multi-track.mov":
369
+ continue
370
+
337
371
  p_xml = self.main([f"resources/{test_name}"], ["-exp"], "out.xml")
338
372
  self.main([p_xml], [])
339
373
 
@@ -9,12 +9,10 @@ from subprocess import run
9
9
  from typing import TYPE_CHECKING, Any
10
10
 
11
11
  import bv
12
- from bv import AudioResampler, Codec
13
12
 
14
- from auto_editor.ffwrapper import FileInfo, initFileInfo
13
+ from auto_editor.ffwrapper import FileInfo
15
14
  from auto_editor.lib.contracts import is_int, is_str
16
15
  from auto_editor.make_layers import clipify, make_av, make_timeline
17
- from auto_editor.output import Ensure, parse_bitrate
18
16
  from auto_editor.render.audio import make_new_audio
19
17
  from auto_editor.render.subtitle import make_new_subtitles
20
18
  from auto_editor.render.video import render_av
@@ -85,11 +83,11 @@ def set_video_codec(
85
83
 
86
84
  if ctr.vcodecs is not None and codec not in ctr.vcodecs:
87
85
  try:
88
- cobj = Codec(codec, "w")
86
+ cobj = bv.Codec(codec, "w")
89
87
  except bv.codec.codec.UnknownCodecError:
90
88
  log.error(f"Unknown encoder: {codec}")
91
89
  # Normalize encoder names
92
- if cobj.id not in (Codec(x, "w").id for x in ctr.vcodecs):
90
+ if cobj.id not in (bv.Codec(x, "w").id for x in ctr.vcodecs):
93
91
  log.error(codec_error.format(codec, out_ext))
94
92
 
95
93
  return codec
@@ -113,11 +111,11 @@ def set_audio_codec(
113
111
 
114
112
  if ctr.acodecs is None or codec not in ctr.acodecs:
115
113
  try:
116
- cobj = Codec(codec, "w")
114
+ cobj = bv.Codec(codec, "w")
117
115
  except bv.codec.codec.UnknownCodecError:
118
116
  log.error(f"Unknown encoder: {codec}")
119
117
  # Normalize encoder names
120
- if cobj.id not in (Codec(x, "w").id for x in ctr.acodecs):
118
+ if cobj.id not in (bv.Codec(x, "w").id for x in ctr.acodecs):
121
119
  log.error(codec_error.format(codec, out_ext))
122
120
 
123
121
  return codec
@@ -161,6 +159,7 @@ def parse_export(export: str, log: Log) -> dict[str, Any]:
161
159
  def edit_media(paths: list[str], args: Args, log: Log) -> None:
162
160
  bar = initBar(args.progress)
163
161
  tl = None
162
+ src = None
164
163
 
165
164
  if args.keep_tracks_separate:
166
165
  log.deprecated("--keep-tracks-separate is deprecated.")
@@ -172,30 +171,18 @@ def edit_media(paths: list[str], args: Args, log: Log) -> None:
172
171
  from auto_editor.formats.fcp7 import fcp7_read_xml
173
172
 
174
173
  tl = fcp7_read_xml(paths[0], log)
175
- assert tl.src is not None
176
- sources: list[FileInfo] = [tl.src]
177
- src: FileInfo | None = tl.src
178
-
179
174
  elif path_ext == ".mlt":
180
175
  from auto_editor.formats.shotcut import shotcut_read_mlt
181
176
 
182
177
  tl = shotcut_read_mlt(paths[0], log)
183
- assert tl.src is not None
184
- sources = [tl.src]
185
- src = tl.src
186
-
187
178
  elif path_ext == ".json":
188
179
  from auto_editor.formats.json import read_json
189
180
 
190
181
  tl = read_json(paths[0], log)
191
- sources = [] if tl.src is None else [tl.src]
192
- src = tl.src
193
182
  else:
194
- sources = [initFileInfo(path, log) for path in paths]
183
+ sources = [FileInfo.init(path, log) for path in paths]
195
184
  src = None if not sources else sources[0]
196
185
 
197
- del paths
198
-
199
186
  output, export_ops = set_output(args.output, args.export, src, log)
200
187
  assert "export" in export_ops
201
188
  export = export_ops["export"]
@@ -256,7 +243,8 @@ def edit_media(paths: list[str], args: Args, log: Log) -> None:
256
243
  from auto_editor.formats.fcp11 import fcp11_write_xml
257
244
  from auto_editor.timeline import set_stream_to_0
258
245
 
259
- set_stream_to_0(tl, log)
246
+ assert src is not None
247
+ set_stream_to_0(src, tl, log)
260
248
  fcp11_write_xml(export_ops["name"], 10, output, True, tl, log)
261
249
  return
262
250
 
@@ -269,7 +257,7 @@ def edit_media(paths: list[str], args: Args, log: Log) -> None:
269
257
  out_ext = splitext(output)[1].replace(".", "")
270
258
 
271
259
  # Check if export options make sense.
272
- ctr = container_constructor(out_ext.lower())
260
+ ctr = container_constructor(out_ext.lower(), log)
273
261
 
274
262
  if ctr.samplerate is not None and args.sample_rate not in ctr.samplerate:
275
263
  log.error(f"'{out_ext}' container only supports samplerates: {ctr.samplerate}")
@@ -278,8 +266,6 @@ def edit_media(paths: list[str], args: Args, log: Log) -> None:
278
266
  args.audio_codec = set_audio_codec(args.audio_codec, src, out_ext, ctr, log)
279
267
 
280
268
  def make_media(tl: v3, output_path: str) -> None:
281
- assert src is not None
282
-
283
269
  options = {}
284
270
  mov_flags = []
285
271
  if args.fragmented and not args.no_fragmented:
@@ -303,51 +289,29 @@ def edit_media(paths: list[str], args: Args, log: Log) -> None:
303
289
  output_stream, vframes = None, iter([])
304
290
 
305
291
  # Setup audio
306
- if ctr.default_aud != "none":
307
- ensure = Ensure(bar, samplerate, log)
308
- audio_paths = make_new_audio(tl, ctr, ensure, args, bar, log)
309
- else:
310
- audio_paths = []
311
-
312
- if len(audio_paths) > 1 and ctr.max_audios == 1:
313
- log.warning("Dropping extra audio streams (container only allows one)")
314
- audio_paths = audio_paths[0:1]
315
-
316
- if audio_paths:
317
- try:
318
- audio_encoder = bv.Codec(args.audio_codec, "w")
319
- except bv.FFmpegError as e:
320
- log.error(e)
321
- if audio_encoder.audio_formats is None:
322
- log.error(f"{args.audio_codec}: No known audio formats avail.")
323
- audio_format = audio_encoder.audio_formats[0]
324
- resampler = AudioResampler(format=audio_format, layout="stereo", rate=tl.sr)
292
+ try:
293
+ audio_encoder = bv.Codec(args.audio_codec, "w")
294
+ except bv.FFmpegError as e:
295
+ log.error(e)
296
+ if audio_encoder.audio_formats is None:
297
+ log.error(f"{args.audio_codec}: No known audio formats avail.")
298
+ fmt = audio_encoder.audio_formats[0]
325
299
 
326
300
  audio_streams: list[bv.AudioStream] = []
327
- audio_inputs = []
328
- audio_gen_frames = []
329
- for i, audio_path in enumerate(audio_paths):
330
- audio_stream = output.add_stream(
331
- args.audio_codec,
332
- format=audio_format,
333
- rate=tl.sr,
334
- time_base=Fraction(1, tl.sr),
335
- )
336
- if not isinstance(audio_stream, bv.AudioStream):
337
- log.error(f"Not a known audio codec: {args.audio_codec}")
338
301
 
339
- if args.audio_bitrate != "auto":
340
- audio_stream.bit_rate = parse_bitrate(args.audio_bitrate, log)
341
- log.debug(f"audio bitrate: {audio_stream.bit_rate}")
342
- else:
343
- log.debug(f"[auto] audio bitrate: {audio_stream.bit_rate}")
344
- if i < len(src.audios) and src.audios[i].lang is not None:
345
- audio_stream.metadata["language"] = src.audios[i].lang # type: ignore
302
+ if ctr.default_aud == "none":
303
+ while len(tl.a) > 0:
304
+ tl.a.pop()
305
+ elif len(tl.a) > 1 and ctr.max_audios == 1:
306
+ log.warning("Dropping extra audio streams (container only allows one)")
346
307
 
347
- audio_streams.append(audio_stream)
348
- audio_input = bv.open(audio_path)
349
- audio_inputs.append(audio_input)
350
- audio_gen_frames.append(audio_input.decode(audio=0))
308
+ while len(tl.a) > 1:
309
+ tl.a.pop()
310
+
311
+ if len(tl.a) > 0:
312
+ audio_streams, audio_gen_frames = make_new_audio(output, fmt, tl, args, log)
313
+ else:
314
+ audio_streams, audio_gen_frames = [], [iter([])]
351
315
 
352
316
  # Setup subtitles
353
317
  if ctr.default_sub != "none" and not args.sn:
@@ -365,8 +329,8 @@ def edit_media(paths: list[str], args: Args, log: Log) -> None:
365
329
  subtitle_stream = output.add_stream_from_template(
366
330
  subtitle_input.streams.subtitles[0]
367
331
  )
368
- if i < len(src.subtitles) and src.subtitles[i].lang is not None:
369
- subtitle_stream.metadata["language"] = src.subtitles[i].lang # type: ignore
332
+ if i < len(tl.T.subtitles) and (lang := tl.T.subtitles[i].lang) is not None:
333
+ subtitle_stream.metadata["language"] = lang
370
334
 
371
335
  subtitle_streams.append(subtitle_stream)
372
336
  sub_gen_frames.append(subtitle_input.demux(subtitles=0))
@@ -463,13 +427,11 @@ def edit_media(paths: list[str], args: Args, log: Log) -> None:
463
427
  break
464
428
 
465
429
  if should_get_audio:
466
- for audio_stream, audio_frame in zip(audio_streams, audio_frames):
467
- for reframe in resampler.resample(audio_frame):
468
- assert reframe.pts is not None
469
- heappush(
470
- frame_queue,
471
- Priority(reframe.pts, reframe, audio_stream),
472
- )
430
+ for audio_stream, aframe in zip(audio_streams, audio_frames):
431
+ if aframe is None:
432
+ continue
433
+ assert aframe.pts is not None
434
+ heappush(frame_queue, Priority(aframe.pts, aframe, audio_stream))
473
435
  if should_get_sub:
474
436
  for subtitle_stream, packet in zip(subtitle_streams, subtitle_frames):
475
437
  if packet and packet.pts is not None:
@@ -511,8 +473,6 @@ def edit_media(paths: list[str], args: Args, log: Log) -> None:
511
473
  bar.end()
512
474
 
513
475
  # Close resources
514
- for audio_input in audio_inputs:
515
- audio_input.close()
516
476
  for subtitle_input in subtitle_inputs:
517
477
  subtitle_input.close()
518
478
  output.close()
@@ -542,11 +502,9 @@ def edit_media(paths: list[str], args: Args, log: Log) -> None:
542
502
  tl.v1.source, [clipify(padded_chunks, tl.v1.source)]
543
503
  )
544
504
  my_timeline = v3(
545
- tl.v1.source,
546
505
  tl.tb,
547
- tl.sr,
548
- tl.res,
549
506
  "#000",
507
+ tl.template,
550
508
  vspace,
551
509
  aspace,
552
510
  v1(tl.v1.source, padded_chunks),
@@ -0,0 +1,178 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from fractions import Fraction
5
+ from pathlib import Path
6
+
7
+ import bv
8
+
9
+ from auto_editor.utils.log import Log
10
+
11
+
12
+ def mux(input: Path, output: Path, stream: int) -> None:
13
+ input_container = bv.open(input, "r")
14
+ output_container = bv.open(output, "w")
15
+
16
+ input_audio_stream = input_container.streams.audio[stream]
17
+ output_audio_stream = output_container.add_stream("pcm_s16le")
18
+
19
+ for frame in input_container.decode(input_audio_stream):
20
+ output_container.mux(output_audio_stream.encode(frame))
21
+
22
+ output_container.mux(output_audio_stream.encode(None))
23
+
24
+ output_container.close()
25
+ input_container.close()
26
+
27
+
28
+ @dataclass(slots=True, frozen=True)
29
+ class VideoStream:
30
+ width: int
31
+ height: int
32
+ codec: str
33
+ fps: Fraction
34
+ duration: float
35
+ sar: Fraction
36
+ time_base: Fraction | None
37
+ pix_fmt: str | None
38
+ color_range: int
39
+ color_space: int
40
+ color_primaries: int
41
+ color_transfer: int
42
+ bitrate: int
43
+ lang: str | None
44
+
45
+
46
+ @dataclass(slots=True, frozen=True)
47
+ class AudioStream:
48
+ codec: str
49
+ samplerate: int
50
+ layout: str
51
+ channels: int
52
+ duration: float
53
+ bitrate: int
54
+ lang: str | None
55
+
56
+
57
+ @dataclass(slots=True, frozen=True)
58
+ class SubtitleStream:
59
+ codec: str
60
+ ext: str
61
+ lang: str | None
62
+
63
+
64
+ @dataclass(slots=True, frozen=True)
65
+ class FileInfo:
66
+ path: Path
67
+ bitrate: int
68
+ duration: float
69
+ description: str | None
70
+ videos: tuple[VideoStream, ...]
71
+ audios: tuple[AudioStream, ...]
72
+ subtitles: tuple[SubtitleStream, ...]
73
+
74
+ def get_res(self) -> tuple[int, int]:
75
+ if self.videos:
76
+ return self.videos[0].width, self.videos[0].height
77
+ return 1920, 1080
78
+
79
+ def get_fps(self) -> Fraction:
80
+ if self.videos:
81
+ return self.videos[0].fps
82
+ return Fraction(30)
83
+
84
+ def get_sr(self) -> int:
85
+ if self.audios:
86
+ return self.audios[0].samplerate
87
+ return 48000
88
+
89
+ @classmethod
90
+ def init(self, path: str, log: Log) -> FileInfo:
91
+ try:
92
+ cont = bv.open(path, "r")
93
+ except bv.error.FileNotFoundError:
94
+ log.error(f"Input file doesn't exist: {path}")
95
+ except bv.error.IsADirectoryError:
96
+ log.error(f"Expected a media file, but got a directory: {path}")
97
+ except bv.error.InvalidDataError:
98
+ log.error(f"Invalid data when processing: {path}")
99
+
100
+ videos: tuple[VideoStream, ...] = ()
101
+ audios: tuple[AudioStream, ...] = ()
102
+ subtitles: tuple[SubtitleStream, ...] = ()
103
+
104
+ for v in cont.streams.video:
105
+ if v.duration is not None and v.time_base is not None:
106
+ vdur = float(v.duration * v.time_base)
107
+ else:
108
+ vdur = 0.0
109
+
110
+ fps = v.average_rate
111
+ if (fps is None or fps < 1) and v.name in {"png", "mjpeg", "webp"}:
112
+ fps = Fraction(25)
113
+ if fps is None or fps == 0:
114
+ fps = Fraction(30)
115
+
116
+ if v.sample_aspect_ratio is None:
117
+ sar = Fraction(1)
118
+ else:
119
+ sar = v.sample_aspect_ratio
120
+
121
+ cc = v.codec_context
122
+
123
+ if v.name is None:
124
+ log.error(f"Can't detect codec for video stream {v}")
125
+
126
+ videos += (
127
+ VideoStream(
128
+ v.width,
129
+ v.height,
130
+ v.codec.canonical_name,
131
+ fps,
132
+ vdur,
133
+ sar,
134
+ v.time_base,
135
+ getattr(v.format, "name", None),
136
+ cc.color_range,
137
+ cc.colorspace,
138
+ cc.color_primaries,
139
+ cc.color_trc,
140
+ 0 if v.bit_rate is None else v.bit_rate,
141
+ v.language,
142
+ ),
143
+ )
144
+
145
+ for a in cont.streams.audio:
146
+ adur = 0.0
147
+ if a.duration is not None and a.time_base is not None:
148
+ adur = float(a.duration * a.time_base)
149
+
150
+ a_cc = a.codec_context
151
+ audios += (
152
+ AudioStream(
153
+ a_cc.codec.canonical_name,
154
+ 0 if a_cc.sample_rate is None else a_cc.sample_rate,
155
+ a.layout.name,
156
+ a_cc.channels,
157
+ adur,
158
+ 0 if a_cc.bit_rate is None else a_cc.bit_rate,
159
+ a.language,
160
+ ),
161
+ )
162
+
163
+ for s in cont.streams.subtitles:
164
+ codec = s.codec_context.name
165
+ sub_exts = {"mov_text": "srt", "ass": "ass", "webvtt": "vtt"}
166
+ ext = sub_exts.get(codec, "vtt")
167
+ subtitles += (SubtitleStream(codec, ext, s.language),)
168
+
169
+ desc = cont.metadata.get("description", None)
170
+ bitrate = 0 if cont.bit_rate is None else cont.bit_rate
171
+ dur = 0 if cont.duration is None else cont.duration / bv.time_base
172
+
173
+ cont.close()
174
+
175
+ return FileInfo(Path(path), bitrate, dur, desc, videos, audios, subtitles)
176
+
177
+ def __repr__(self) -> str:
178
+ return f"@{self.path.name}"
@@ -59,13 +59,6 @@ def fcp11_write_xml(
59
59
  return "0s"
60
60
  return f"{val * tl.tb.denominator}/{tl.tb.numerator}s"
61
61
 
62
- src = tl.src
63
- assert src is not None
64
-
65
- proj_name = src.path.stem
66
- src_dur = int(src.duration * tl.tb)
67
- tl_dur = src_dur if resolve else tl.out_len()
68
-
69
62
  if version == 11:
70
63
  ver_str = "1.11"
71
64
  elif version == 10:
@@ -76,7 +69,16 @@ def fcp11_write_xml(
76
69
  fcpxml = Element("fcpxml", version=ver_str)
77
70
  resources = SubElement(fcpxml, "resources")
78
71
 
72
+ src_dur = 0
73
+ tl_dur = 0 if resolve else tl.out_len()
74
+
79
75
  for i, one_src in enumerate(tl.unique_sources()):
76
+ if i == 0:
77
+ proj_name = one_src.path.stem
78
+ src_dur = int(one_src.duration * tl.tb)
79
+ if resolve:
80
+ tl_dur = src_dur
81
+
80
82
  SubElement(
81
83
  resources,
82
84
  "format",
@@ -113,7 +115,7 @@ def fcp11_write_xml(
113
115
  format="r1",
114
116
  tcStart="0s",
115
117
  tcFormat="NDF",
116
- audioLayout="mono" if src.audios and src.audios[0].channels == 1 else "stereo",
118
+ audioLayout=tl.T.layout,
117
119
  audioRate="44.1k" if tl.sr == 44100 else "48k",
118
120
  )
119
121
  spine = SubElement(sequence, "spine")