auto-editor 25.1.0__py3-none-any.whl → 25.3.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.
@@ -20,6 +20,7 @@ if TYPE_CHECKING:
20
20
 
21
21
 
22
22
  def make_standard_env() -> dict[str, Any]:
23
+ import os.path
23
24
  from cmath import sqrt as complex_sqrt
24
25
  from functools import reduce
25
26
  from operator import add, ge, gt, is_, le, lt, mod, mul
@@ -135,6 +136,22 @@ def make_standard_env() -> dict[str, Any]:
135
136
  Sym("float64"): np.float64,
136
137
  }
137
138
 
139
+ @dataclass(slots=True)
140
+ class InputPort:
141
+ name: str
142
+ port: Any
143
+ closed: bool
144
+
145
+ def close(self) -> None:
146
+ if not self.closed:
147
+ self.closed = True
148
+ self.port.close()
149
+
150
+ def __str__(self) -> str:
151
+ return f"#<input-port:{self.name}>"
152
+
153
+ __repr__ = __str__
154
+
138
155
  @dataclass(slots=True)
139
156
  class OutputPort:
140
157
  name: str
@@ -152,6 +169,13 @@ def make_standard_env() -> dict[str, Any]:
152
169
 
153
170
  __repr__ = __str__
154
171
 
172
+ def initInPort(name: str) -> InputPort | Literal[False]:
173
+ try:
174
+ port = open(name, encoding="utf-8")
175
+ except Exception:
176
+ return False
177
+ return InputPort(name, port, False)
178
+
155
179
  def initOutPort(name: str) -> OutputPort | Literal[False]:
156
180
  try:
157
181
  port = open(name, "w", encoding="utf-8")
@@ -198,14 +222,20 @@ def make_standard_env() -> dict[str, Any]:
198
222
  parms: list[str] = []
199
223
  for item in node[1]:
200
224
  if type(item) is not Sym:
201
- raise MyError(f"{node[0]}: must be an identifier")
225
+ raise MyError(f"{node[0]}: must be an identifier, got: {item} {type(item)}")
202
226
 
203
227
  parms.append(f"{item}")
204
228
 
205
229
  return UserProc(env, "", parms, (), node[2:])
206
230
 
207
231
  def syn_define(env: Env, node: Node) -> None:
232
+ if len(node) < 2:
233
+ raise MyError(f"{node[0]}: too few terms")
208
234
  if len(node) < 3:
235
+ if type(node[1]) is Sym:
236
+ raise MyError(f"{node[0]}: what should `{node[1]}` be defined as?")
237
+ elif type(node[1]) is tuple and len(node[1]) > 0:
238
+ raise MyError(f"{node[0]}: function `{node[1][0]}` needs a body.")
209
239
  raise MyError(f"{node[0]}: too few terms")
210
240
 
211
241
  if type(node[1]) is tuple:
@@ -437,6 +467,11 @@ def make_standard_env() -> dict[str, Any]:
437
467
  raise MyError(f"{node[0]}: Expected string? got: {print_str(num)}")
438
468
  env[name] += num
439
469
 
470
+ def syn_while(env: Env, node: Node) -> None:
471
+ while my_eval(env, node[1]) == True:
472
+ for c in node[2:]:
473
+ my_eval(env, c)
474
+
440
475
  def syn_for(env: Env, node: Node) -> None:
441
476
  var, my_iter = check_for_syntax(env, node)
442
477
 
@@ -808,6 +843,12 @@ def make_standard_env() -> dict[str, Any]:
808
843
  return f"{val.real}{join}{val.imag}i"
809
844
  return f"{val}"
810
845
 
846
+ def string_to_number(val) -> float:
847
+ try:
848
+ return float(val)
849
+ except Exception:
850
+ raise MyError(f"failed to convert {val} to number")
851
+
811
852
  def palet_join(v: Any, s: str) -> str:
812
853
  try:
813
854
  return s.join(v)
@@ -943,6 +984,7 @@ def make_standard_env() -> dict[str, Any]:
943
984
  # loops
944
985
  "for": Syntax(syn_for),
945
986
  "for-items": Syntax(syn_for_items),
987
+ "while": Syntax(syn_while),
946
988
  # contracts
947
989
  "number?": is_num,
948
990
  "real?": is_real,
@@ -1102,10 +1144,20 @@ def make_standard_env() -> dict[str, Any]:
1102
1144
  ),
1103
1145
  ),
1104
1146
  # i/o
1147
+ "file-exists?": Proc("file-exists", os.path.isfile, (1, 1), is_str),
1148
+ "open-input-file": Proc("open-input-file", initInPort, (1, 1), is_str),
1149
+ "input-port?": (ip := Contract("input-port?", lambda v: type(v) is InputPort)),
1150
+ "read-line": Proc("read-line",
1151
+ lambda f: (r := f.port.readline(), None if r == "" else r.rstrip())[-1],
1152
+ (1, 1), ip),
1105
1153
  "open-output-file": Proc("open-output-file", initOutPort, (1, 1), is_str),
1106
1154
  "output-port?": (op := Contract("output-port?", lambda v: type(v) is OutputPort)),
1107
- "close-port": Proc("close-port", OutputPort.close, (1, 1), op),
1108
- "closed?": Proc("closed?", lambda o: o.closed, (1, 1), op),
1155
+ "port?": (port := Contract("port?", orc(ip, op))),
1156
+ "close-port": Proc("close-port", lambda p: p.close, (1, 1), port),
1157
+ "closed?": Proc("closed?", lambda o: o.closed, (1, 1), port),
1158
+ # subprocess
1159
+ "system": Proc("system", palet_system, (1, 1), is_str),
1160
+ "system*": Proc("system*", palet_system_star, (1, None), is_str),
1109
1161
  # printing
1110
1162
  "display": Proc("display",
1111
1163
  lambda v, f=None: print(display_str(v), end="", file=f), (1, 2), any_p, op),
@@ -1119,10 +1171,9 @@ def make_standard_env() -> dict[str, Any]:
1119
1171
  "assert": Proc("assert", palet_assert, (1, 2), any_p, orc(is_str, False)),
1120
1172
  "error": Proc("error", raise_, (1, 1), is_str),
1121
1173
  "sleep": Proc("sleep", sleep, (1, 1), is_int_or_float),
1122
- "system": Proc("system", palet_system, (1, 1), is_str),
1123
- "system*": Proc("system*", palet_system_star, (1, None), is_str),
1124
1174
  # conversions
1125
1175
  "number->string": Proc("number->string", number_to_string, (1, 1), is_num),
1176
+ "string->number": Proc("string->number", string_to_number, (1, 1), is_str),
1126
1177
  "string->vector": Proc(
1127
1178
  "string->vector", lambda s: [Char(c) for c in s], (1, 1), is_str
1128
1179
  ),
@@ -118,6 +118,10 @@ class Proc:
118
118
 
119
119
  if kws is not None:
120
120
  for key, val in kwargs.items():
121
+ if key not in kws:
122
+ raise MyError(
123
+ f"{self.name} got an unexpected keyword argument: {key}"
124
+ )
121
125
  check = cont[-1] if kws[key] >= len(cont) else cont[kws[key]]
122
126
  if not check_contract(check, val):
123
127
  exp = f"{check}" if callable(check) else print_str(check)
@@ -54,12 +54,14 @@ class Env:
54
54
 
55
55
 
56
56
  class Sym:
57
- __slots__ = ("val", "hash")
57
+ __slots__ = ("val", "hash", "lineno", "column")
58
58
 
59
- def __init__(self, val: str):
59
+ def __init__(self, val: str, lineno: int = -1, column: int = -1):
60
60
  assert isinstance(val, str)
61
61
  self.val = val
62
62
  self.hash = hash(val)
63
+ self.lineno = lineno
64
+ self.column = column
63
65
 
64
66
  def __str__(self) -> str:
65
67
  return self.val
auto_editor/output.py CHANGED
@@ -46,9 +46,9 @@ class Ensure:
46
46
  astream = in_container.streams.audio[stream]
47
47
 
48
48
  if astream.duration is None or astream.time_base is None:
49
- dur = 1
49
+ dur = 1.0
50
50
  else:
51
- dur = int(astream.duration * astream.time_base)
51
+ dur = float(astream.duration * astream.time_base)
52
52
 
53
53
  bar.start(dur, "Extracting audio")
54
54
 
@@ -58,8 +58,8 @@ class Ensure:
58
58
 
59
59
  resampler = AudioResampler(format="s16", layout="stereo", rate=sample_rate)
60
60
  for i, frame in enumerate(in_container.decode(astream)):
61
- if i % 1500 == 0:
62
- bar.tick(0 if frame.time is None else frame.time)
61
+ if i % 1500 == 0 and frame.time is not None:
62
+ bar.tick(frame.time)
63
63
 
64
64
  for new_frame in resampler.resample(frame):
65
65
  for packet in output_astream.encode(new_frame):
@@ -237,8 +237,8 @@ def mux_quality_media(
237
237
  if s_tracks > 0:
238
238
  cmd.extend(["-map", "0:t?"]) # Add input attachments to output.
239
239
 
240
- # This was causing a crash for 'example.mp4 multi-track.mov'
241
- # cmd.extend(["-map", "0:d?"])
240
+ if not args.dn:
241
+ cmd.extend(["-map", "0:d?"])
242
242
 
243
243
  cmd.append(output_path)
244
244
  ffmpeg.run_check_errors(cmd, log, path=output_path)
auto_editor/preview.py CHANGED
@@ -7,7 +7,7 @@ from typing import TextIO
7
7
 
8
8
  from auto_editor.analyze import Levels
9
9
  from auto_editor.timeline import v3
10
- from auto_editor.utils.bar import Bar
10
+ from auto_editor.utils.bar import initBar
11
11
  from auto_editor.utils.func import to_timecode
12
12
  from auto_editor.utils.log import Log
13
13
 
@@ -65,7 +65,7 @@ def preview(tl: v3, log: Log) -> None:
65
65
 
66
66
  in_len = 0
67
67
  for src in all_sources:
68
- in_len += Levels(src, tb, Bar("none"), False, log, False).media_length
68
+ in_len += Levels(src, tb, initBar("none"), False, log, False).media_length
69
69
 
70
70
  out_len = tl.out_len()
71
71
 
@@ -1,9 +1,11 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import io
3
4
  from pathlib import Path
4
5
  from platform import system
5
6
  from subprocess import PIPE
6
7
 
8
+ import av
7
9
  import numpy as np
8
10
 
9
11
  from auto_editor.ffwrapper import FFmpeg, FileInfo
@@ -12,7 +14,7 @@ from auto_editor.lang.palet import env
12
14
  from auto_editor.lib.contracts import andc, between_c, is_int_or_float
13
15
  from auto_editor.lib.err import MyError
14
16
  from auto_editor.output import Ensure
15
- from auto_editor.timeline import v3
17
+ from auto_editor.timeline import TlAudio, v3
16
18
  from auto_editor.utils.bar import Bar
17
19
  from auto_editor.utils.cmdkw import ParserError, parse_with_palet, pAttr, pAttrs
18
20
  from auto_editor.utils.log import Log
@@ -165,6 +167,68 @@ def apply_audio_normalization(
165
167
  ffmpeg.run(["-i", f"{pre_master}"] + cmd + [f"{path}"])
166
168
 
167
169
 
170
+ def process_audio_clip(
171
+ clip: TlAudio, samp_list: AudioData, samp_start: int, samp_end: int, sr: int
172
+ ) -> AudioData:
173
+ input_buffer = io.BytesIO()
174
+ write(input_buffer, sr, samp_list[samp_start:samp_end])
175
+ input_buffer.seek(0)
176
+
177
+ input_file = av.open(input_buffer, "r")
178
+ input_stream = input_file.streams.audio[0]
179
+
180
+ output_bytes = io.BytesIO()
181
+ output_file = av.open(output_bytes, mode="w", format="wav")
182
+ output_stream = output_file.add_stream("pcm_s16le", rate=sr)
183
+ assert isinstance(output_stream, av.audio.AudioStream)
184
+
185
+ graph = av.filter.Graph()
186
+ args = [graph.add_abuffer(template=input_stream)]
187
+
188
+ if clip.speed != 1:
189
+ if clip.speed > 10_000:
190
+ for _ in range(3):
191
+ args.append(graph.add("atempo", f"{clip.speed ** (1/3)}"))
192
+ elif clip.speed > 100:
193
+ for _ in range(2):
194
+ args.append(graph.add("atempo", f"{clip.speed ** 0.5}"))
195
+ elif clip.speed >= 0.5:
196
+ args.append(graph.add("atempo", f"{clip.speed}"))
197
+ else:
198
+ start = 0.5
199
+ while start * 0.5 > clip.speed:
200
+ start *= 0.5
201
+ args.append(graph.add("atempo", "0.5"))
202
+ args.append(graph.add("atempo", f"{clip.speed / start}"))
203
+
204
+ if clip.volume != 1:
205
+ args.append(graph.add("volume", f"{clip.volume}"))
206
+
207
+ args.append(graph.add("abuffersink"))
208
+ graph.link_nodes(*args).configure()
209
+
210
+ for frame in input_file.decode(input_stream):
211
+ graph.push(frame)
212
+ while True:
213
+ try:
214
+ aframe = graph.pull()
215
+ assert isinstance(aframe, av.audio.AudioFrame)
216
+ for packet in output_stream.encode(aframe):
217
+ output_file.mux(packet)
218
+ except (av.BlockingIOError, av.EOFError):
219
+ break
220
+
221
+ # Flush the stream
222
+ for packet in output_stream.encode(None):
223
+ output_file.mux(packet)
224
+
225
+ input_file.close()
226
+ output_file.close()
227
+
228
+ output_bytes.seek(0)
229
+ return read(output_bytes)[1]
230
+
231
+
168
232
  def make_new_audio(
169
233
  tl: v3, ensure: Ensure, args: Args, ffmpeg: FFmpeg, bar: Bar, log: Log
170
234
  ) -> list[str]:
@@ -175,7 +239,6 @@ def make_new_audio(
175
239
 
176
240
  norm = parse_norm(args.audio_normalize, log)
177
241
 
178
- af_tick = 0
179
242
  temp = log.temp
180
243
 
181
244
  if not tl.a or not tl.a[0]:
@@ -191,7 +254,8 @@ def make_new_audio(
191
254
  for c, clip in enumerate(layer):
192
255
  if (clip.src, clip.stream) not in samples:
193
256
  audio_path = ensure.audio(clip.src, clip.stream)
194
- samples[(clip.src, clip.stream)] = read(audio_path)[1]
257
+ with open(audio_path, "rb") as file:
258
+ samples[(clip.src, clip.stream)] = read(file)[1]
195
259
 
196
260
  if arr is None:
197
261
  leng = max(round((layer[-1].start + layer[-1].dur) * sr / tb), sr // tb)
@@ -214,42 +278,10 @@ def make_new_audio(
214
278
  if samp_end > len(samp_list):
215
279
  samp_end = len(samp_list)
216
280
 
217
- filters: list[str] = []
218
-
219
- if clip.speed != 1:
220
- if clip.speed > 10_000:
221
- filters.extend([f"atempo={clip.speed}^.33333"] * 3)
222
- elif clip.speed > 100:
223
- filters.extend(
224
- [f"atempo=sqrt({clip.speed})", f"atempo=sqrt({clip.speed})"]
225
- )
226
- elif clip.speed >= 0.5:
227
- filters.append(f"atempo={clip.speed}")
228
- else:
229
- start = 0.5
230
- while start * 0.5 > clip.speed:
231
- start *= 0.5
232
- filters.append("atempo=0.5")
233
- filters.append(f"atempo={clip.speed / start}")
234
-
235
- if clip.volume != 1:
236
- filters.append(f"volume={clip.volume}")
237
-
238
- if not filters:
239
- clip_arr = samp_list[samp_start:samp_end]
281
+ if clip.speed != 1 or clip.volume != 1:
282
+ clip_arr = process_audio_clip(clip, samp_list, samp_start, samp_end, sr)
240
283
  else:
241
- af = Path(temp, f"af{af_tick}.wav")
242
- af_out = Path(temp, f"af{af_tick}_out.wav")
243
-
244
- # Windows can't replace a file that's already in use, so we have to
245
- # cycle through file names.
246
- af_tick = (af_tick + 1) % 3
247
-
248
- with open(af, "wb") as fid:
249
- write(fid, sr, samp_list[samp_start:samp_end])
250
-
251
- ffmpeg.run(["-i", f"{af}", "-af", ",".join(filters), f"{af_out}"])
252
- clip_arr = read(f"{af_out}")[1]
284
+ clip_arr = samp_list[samp_start:samp_end]
253
285
 
254
286
  # Mix numpy arrays
255
287
  start = clip.start * sr // tb
@@ -57,21 +57,6 @@ allowed_pix_fmt = {
57
57
  }
58
58
 
59
59
 
60
- def apply_anchor(x: int, y: int, w: int, h: int, anchor: str) -> tuple[int, int]:
61
- if anchor == "ce":
62
- x = (x * 2 - w) // 2
63
- y = (y * 2 - h) // 2
64
- if anchor == "tr":
65
- x -= w
66
- if anchor == "bl":
67
- y -= h
68
- if anchor == "br":
69
- x -= w
70
- y -= h
71
- # Use 'tl' by default
72
- return x, y
73
-
74
-
75
60
  def make_solid(width: int, height: int, pix_fmt: str, bg: str) -> av.VideoFrame:
76
61
  hex_color = bg.lstrip("#").upper()
77
62
  rgb_color = tuple(int(hex_color[i : i + 2], 16) for i in (0, 2, 4))
@@ -292,7 +277,7 @@ def render_av(
292
277
  frame = graph.vpull()
293
278
  elif isinstance(obj, TlRect):
294
279
  graph = av.filter.Graph()
295
- x, y = apply_anchor(obj.x, obj.y, obj.width, obj.height, obj.anchor)
280
+ x, y = obj.x, obj.y
296
281
  graph.link_nodes(
297
282
  graph.add_buffer(template=my_stream),
298
283
  graph.add(
@@ -307,9 +292,7 @@ def render_av(
307
292
  array = frame.to_ndarray(format="rgb24")
308
293
 
309
294
  overlay_h, overlay_w, _ = img.shape
310
- x_pos, y_pos = apply_anchor(
311
- obj.x, obj.y, overlay_w, overlay_h, obj.anchor
312
- )
295
+ x_pos, y_pos = obj.x, obj.y
313
296
 
314
297
  x_start = max(x_pos, 0)
315
298
  y_start = max(y_pos, 0)
@@ -11,7 +11,7 @@ from auto_editor.analyze import LevelError, Levels, iter_audio, iter_motion
11
11
  from auto_editor.ffwrapper import initFileInfo
12
12
  from auto_editor.lang.palet import env
13
13
  from auto_editor.lib.contracts import is_bool, is_nat, is_nat1, is_str, is_void, orc
14
- from auto_editor.utils.bar import Bar
14
+ from auto_editor.utils.bar import initBar
15
15
  from auto_editor.utils.cmdkw import (
16
16
  ParserError,
17
17
  Required,
@@ -83,7 +83,7 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
83
83
  parser = levels_options(ArgumentParser("levels"))
84
84
  args = parser.parse_args(LevelArgs, sys_args)
85
85
 
86
- bar = Bar("none")
86
+ bar = initBar("none")
87
87
  log = Log(quiet=True)
88
88
 
89
89
  sources = [initFileInfo(path, log) for path in args.input]
@@ -11,7 +11,7 @@ from auto_editor.lang.palet import ClosingError, Lexer, Parser, env, interpret
11
11
  from auto_editor.lang.stdenv import make_standard_env
12
12
  from auto_editor.lib.data_structs import print_str
13
13
  from auto_editor.lib.err import MyError
14
- from auto_editor.utils.bar import Bar
14
+ from auto_editor.utils.bar import initBar
15
15
  from auto_editor.utils.log import Log
16
16
  from auto_editor.utils.types import frame_rate
17
17
  from auto_editor.vanparse import ArgumentParser
@@ -64,9 +64,8 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
64
64
  sources = [initFileInfo(path, log) for path in args.input]
65
65
  src = sources[0]
66
66
  tb = src.get_fps() if args.timebase is None else args.timebase
67
- bar = Bar("modern")
68
67
  env["timebase"] = tb
69
- env["@levels"] = Levels(src, tb, bar, False, log, strict)
68
+ env["@levels"] = Levels(src, tb, initBar("modern"), False, log, strict)
70
69
 
71
70
  env.update(make_standard_env())
72
71
  print(f"Auto-Editor {auto_editor.__version__}")
@@ -405,7 +405,8 @@ def main(sys_args: list[str] | None = None):
405
405
  test_file = f"resources/{test_name}"
406
406
  results.add(run.main([test_file], []))
407
407
  run.main([test_file], ["--edit", "none"])
408
- results.add(run.main([test_file], ["-exf"]))
408
+ results.add(run.main([test_file], ["--export", "final-cut-pro:version=10"]))
409
+ results.add(run.main([test_file], ["--export", "final-cut-pro:version=11"]))
409
410
  results.add(run.main([test_file], ["-exs"]))
410
411
  results.add(run.main([test_file], ["--export_as_clip_sequence"]))
411
412
  run.main([test_file], ["--stats"])
auto_editor/timeline.py CHANGED
@@ -3,17 +3,20 @@ from __future__ import annotations
3
3
  from dataclasses import dataclass
4
4
  from typing import TYPE_CHECKING
5
5
 
6
+ from auto_editor.ffwrapper import initFileInfo, mux
6
7
  from auto_editor.lib.contracts import *
7
8
  from auto_editor.utils.cmdkw import Required, pAttr, pAttrs
8
- from auto_editor.utils.types import anchor, color, natural, number, threshold
9
+ from auto_editor.utils.types import color, natural, number, threshold
9
10
 
10
11
  if TYPE_CHECKING:
11
12
  from collections.abc import Iterator
12
13
  from fractions import Fraction
14
+ from pathlib import Path
13
15
  from typing import Any
14
16
 
15
17
  from auto_editor.ffwrapper import FileInfo
16
18
  from auto_editor.utils.chunks import Chunks
19
+ from auto_editor.utils.log import Log
17
20
 
18
21
 
19
22
  @dataclass(slots=True)
@@ -88,7 +91,6 @@ class TlImage:
88
91
  y: int
89
92
  width: int
90
93
  opacity: float
91
- anchor: str
92
94
 
93
95
  def as_dict(self) -> dict:
94
96
  return {
@@ -100,7 +102,6 @@ class TlImage:
100
102
  "y": self.y,
101
103
  "width": self.width,
102
104
  "opacity": self.opacity,
103
- "anchor": self.anchor,
104
105
  }
105
106
 
106
107
 
@@ -112,7 +113,6 @@ class TlRect:
112
113
  y: int
113
114
  width: int
114
115
  height: int
115
- anchor: str
116
116
  fill: str
117
117
 
118
118
  def as_dict(self) -> dict:
@@ -124,7 +124,6 @@ class TlRect:
124
124
  "y": self.y,
125
125
  "width": self.width,
126
126
  "height": self.height,
127
- "anchor": self.anchor,
128
127
  "fill": self.fill,
129
128
  }
130
129
 
@@ -157,7 +156,6 @@ img_builder = pAttrs(
157
156
  pAttr("y", Required, is_int, int),
158
157
  pAttr("width", 0, is_nat, natural),
159
158
  pAttr("opacity", 1, is_threshold, threshold),
160
- pAttr("anchor", "ce", is_str, anchor),
161
159
  )
162
160
  rect_builder = pAttrs(
163
161
  "rect",
@@ -167,7 +165,6 @@ rect_builder = pAttrs(
167
165
  pAttr("y", Required, is_int, int),
168
166
  pAttr("width", Required, is_int, int),
169
167
  pAttr("height", Required, is_int, int),
170
- pAttr("anchor", "ce", is_str, anchor),
171
168
  pAttr("fill", "#c4c4c4", is_str, color),
172
169
  )
173
170
  visual_objects = {
@@ -247,6 +244,13 @@ video\n"""
247
244
  for a in aclips:
248
245
  yield a.src
249
246
 
247
+ def unique_sources(self) -> Iterator[FileInfo]:
248
+ seen = set()
249
+ for source in self.sources:
250
+ if source.path not in seen:
251
+ seen.add(source.path)
252
+ yield source
253
+
250
254
  def _duration(self, layer: Any) -> int:
251
255
  total_dur = 0
252
256
  for clips in layer:
@@ -282,3 +286,38 @@ video\n"""
282
286
  "v": v,
283
287
  "a": a,
284
288
  }
289
+
290
+
291
+ def make_tracks_dir(path: Path) -> Path:
292
+ from os import mkdir
293
+ from shutil import rmtree
294
+
295
+ tracks_dir = path.parent / f"{path.stem}_tracks"
296
+
297
+ try:
298
+ mkdir(tracks_dir)
299
+ except OSError:
300
+ rmtree(tracks_dir)
301
+ mkdir(tracks_dir)
302
+
303
+ return tracks_dir
304
+
305
+
306
+ def set_stream_to_0(tl: v3, log: Log) -> None:
307
+ src = tl.src
308
+ assert src is not None
309
+ fold = make_tracks_dir(src.path)
310
+ cache: dict[Path, FileInfo] = {}
311
+
312
+ def make_track(i: int, path: Path) -> FileInfo:
313
+ newtrack = fold / f"{path.stem}_{i}.wav"
314
+ if newtrack not in cache:
315
+ mux(path, output=newtrack, stream=i)
316
+ cache[newtrack] = initFileInfo(f"{newtrack}", log)
317
+ return cache[newtrack]
318
+
319
+ for alayer in tl.a:
320
+ for aobj in alayer:
321
+ if aobj.stream > 0:
322
+ aobj.src = make_track(aobj.stream, aobj.src.path)
323
+ aobj.stream = 0