auto-editor 25.2.0__tar.gz → 25.3.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.
- {auto_editor-25.2.0 → auto_editor-25.3.0}/PKG-INFO +1 -1
- auto_editor-25.3.0/auto_editor/__init__.py +1 -0
- {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/__main__.py +5 -0
- {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/analyze.py +17 -16
- {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/edit.py +16 -6
- {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/ffwrapper.py +26 -1
- {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/formats/fcp11.py +24 -27
- {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/formats/utils.py +0 -18
- {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/output.py +6 -6
- {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/preview.py +2 -2
- {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/render/audio.py +70 -38
- {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/subcommands/levels.py +2 -2
- {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/subcommands/repl.py +2 -3
- {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/subcommands/test.py +2 -1
- {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/timeline.py +45 -0
- {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/utils/bar.py +56 -49
- {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/utils/types.py +1 -0
- {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/wavfile.py +25 -16
- {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor.egg-info/PKG-INFO +1 -1
- auto_editor-25.2.0/auto_editor/__init__.py +0 -1
- {auto_editor-25.2.0 → auto_editor-25.3.0}/LICENSE +0 -0
- {auto_editor-25.2.0 → auto_editor-25.3.0}/README.md +0 -0
- {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/formats/__init__.py +0 -0
- {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/formats/fcp7.py +0 -0
- {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/formats/json.py +0 -0
- {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/formats/shotcut.py +0 -0
- {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/help.py +0 -0
- {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/lang/__init__.py +0 -0
- {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/lang/json.py +0 -0
- {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/lang/libintrospection.py +0 -0
- {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/lang/libmath.py +0 -0
- {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/lang/palet.py +0 -0
- {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/lang/stdenv.py +0 -0
- {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/lib/__init__.py +0 -0
- {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/lib/contracts.py +0 -0
- {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/lib/data_structs.py +0 -0
- {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/lib/err.py +0 -0
- {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/make_layers.py +0 -0
- {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/render/__init__.py +0 -0
- {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/render/subtitle.py +0 -0
- {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/render/video.py +0 -0
- {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/subcommands/__init__.py +0 -0
- {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/subcommands/desc.py +0 -0
- {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/subcommands/info.py +0 -0
- {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/subcommands/palet.py +0 -0
- {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/subcommands/subdump.py +0 -0
- {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/utils/__init__.py +0 -0
- {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/utils/chunks.py +0 -0
- {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/utils/cmdkw.py +0 -0
- {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/utils/container.py +0 -0
- {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/utils/encoder.py +0 -0
- {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/utils/func.py +0 -0
- {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/utils/log.py +0 -0
- {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/utils/subtitle_tools.py +0 -0
- {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/validate_input.py +0 -0
- {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor/vanparse.py +0 -0
- {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor.egg-info/SOURCES.txt +0 -0
- {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor.egg-info/dependency_links.txt +0 -0
- {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor.egg-info/entry_points.txt +0 -0
- {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor.egg-info/requires.txt +0 -0
- {auto_editor-25.2.0 → auto_editor-25.3.0}/auto_editor.egg-info/top_level.txt +0 -0
- {auto_editor-25.2.0 → auto_editor-25.3.0}/docs/build.py +0 -0
- {auto_editor-25.2.0 → auto_editor-25.3.0}/pyproject.toml +0 -0
- {auto_editor-25.2.0 → auto_editor-25.3.0}/setup.cfg +0 -0
@@ -0,0 +1 @@
|
|
1
|
+
__version__ = "25.3.0"
|
@@ -254,6 +254,11 @@ def main_options(parser: ArgumentParser) -> ArgumentParser:
|
|
254
254
|
flag=True,
|
255
255
|
help="Disable the inclusion of subtitle streams in the output file",
|
256
256
|
)
|
257
|
+
parser.add_argument(
|
258
|
+
"-dn",
|
259
|
+
flag=True,
|
260
|
+
help="Disable the inclusion of data streams in the output file",
|
261
|
+
)
|
257
262
|
parser.add_argument(
|
258
263
|
"--extras",
|
259
264
|
metavar="CMD",
|
@@ -19,6 +19,7 @@ from auto_editor.utils.subtitle_tools import convert_ass_to_text
|
|
19
19
|
if TYPE_CHECKING:
|
20
20
|
from collections.abc import Iterator
|
21
21
|
from fractions import Fraction
|
22
|
+
from pathlib import Path
|
22
23
|
from typing import Any
|
23
24
|
|
24
25
|
from numpy.typing import NDArray
|
@@ -70,15 +71,6 @@ def mut_remove_large(
|
|
70
71
|
active = False
|
71
72
|
|
72
73
|
|
73
|
-
def obj_tag(tag: str, tb: Fraction, obj: dict[str, Any]) -> str:
|
74
|
-
key = f"{tag}:{tb}:"
|
75
|
-
for k, v in obj.items():
|
76
|
-
key += f"{k}={v},"
|
77
|
-
|
78
|
-
key = key[:-1] # remove unnecessary char
|
79
|
-
return key
|
80
|
-
|
81
|
-
|
82
74
|
def iter_audio(src, tb: Fraction, stream: int = 0) -> Iterator[np.float32]:
|
83
75
|
fifo = AudioFifo()
|
84
76
|
try:
|
@@ -122,7 +114,7 @@ def iter_motion(src, tb, stream: int, blur: int, width: int) -> Iterator[np.floa
|
|
122
114
|
|
123
115
|
prev_frame = None
|
124
116
|
current_frame = None
|
125
|
-
total_pixels =
|
117
|
+
total_pixels = None
|
126
118
|
index = 0
|
127
119
|
prev_index = -1
|
128
120
|
|
@@ -140,10 +132,13 @@ def iter_motion(src, tb, stream: int, blur: int, width: int) -> Iterator[np.floa
|
|
140
132
|
continue
|
141
133
|
|
142
134
|
graph.push(unframe)
|
143
|
-
frame = graph.
|
135
|
+
frame = graph.vpull()
|
144
136
|
assert frame.time is not None
|
145
137
|
index = round(frame.time * tb)
|
146
138
|
|
139
|
+
if total_pixels is None:
|
140
|
+
total_pixels = frame.width * frame.height
|
141
|
+
|
147
142
|
current_frame = frame.to_ndarray()
|
148
143
|
if prev_frame is None:
|
149
144
|
value = np.float32(0.0)
|
@@ -161,6 +156,12 @@ def iter_motion(src, tb, stream: int, blur: int, width: int) -> Iterator[np.floa
|
|
161
156
|
container.close()
|
162
157
|
|
163
158
|
|
159
|
+
def obj_tag(path: Path, kind: str, tb: Fraction, obj: dict[str, Any]) -> str:
|
160
|
+
mod_time = int(path.stat().st_mtime)
|
161
|
+
key = f"{path.name}:{mod_time:x}:{kind}:{tb}:"
|
162
|
+
return key + ",".join(f"{v}" for v in obj.values())
|
163
|
+
|
164
|
+
|
164
165
|
@dataclass(slots=True)
|
165
166
|
class Levels:
|
166
167
|
src: FileInfo
|
@@ -201,7 +202,7 @@ class Levels:
|
|
201
202
|
def all(self) -> NDArray[np.bool_]:
|
202
203
|
return np.zeros(self.media_length, dtype=np.bool_)
|
203
204
|
|
204
|
-
def read_cache(self,
|
205
|
+
def read_cache(self, kind: str, obj: dict[str, Any]) -> None | np.ndarray:
|
205
206
|
if self.no_cache:
|
206
207
|
return None
|
207
208
|
|
@@ -213,14 +214,14 @@ class Levels:
|
|
213
214
|
self.log.debug(e)
|
214
215
|
return None
|
215
216
|
|
216
|
-
key =
|
217
|
+
key = obj_tag(self.src.path, kind, self.tb, obj)
|
217
218
|
if key not in npzfile.files:
|
218
219
|
return None
|
219
220
|
|
220
221
|
self.log.debug("Using cache")
|
221
222
|
return npzfile[key]
|
222
223
|
|
223
|
-
def cache(self, arr: np.ndarray,
|
224
|
+
def cache(self, arr: np.ndarray, kind: str, obj: dict[str, Any]) -> np.ndarray:
|
224
225
|
if self.no_cache:
|
225
226
|
return arr
|
226
227
|
|
@@ -228,8 +229,8 @@ class Levels:
|
|
228
229
|
if not os.path.exists(workdur):
|
229
230
|
os.mkdir(workdur)
|
230
231
|
|
231
|
-
|
232
|
-
np.savez(os.path.join(workdur, "cache.npz"), **{
|
232
|
+
key = obj_tag(self.src.path, kind, self.tb, obj)
|
233
|
+
np.savez(os.path.join(workdur, "cache.npz"), **{key: arr})
|
233
234
|
|
234
235
|
return arr
|
235
236
|
|
@@ -11,7 +11,7 @@ from auto_editor.render.audio import make_new_audio
|
|
11
11
|
from auto_editor.render.subtitle import make_new_subtitles
|
12
12
|
from auto_editor.render.video import render_av
|
13
13
|
from auto_editor.timeline import v1, v3
|
14
|
-
from auto_editor.utils.bar import
|
14
|
+
from auto_editor.utils.bar import initBar
|
15
15
|
from auto_editor.utils.chunks import Chunk, Chunks
|
16
16
|
from auto_editor.utils.cmdkw import ParserError, parse_with_palet, pAttr, pAttrs
|
17
17
|
from auto_editor.utils.container import Container, container_constructor
|
@@ -125,7 +125,9 @@ def parse_export(export: str, log: Log) -> dict[str, Any]:
|
|
125
125
|
"default": pAttrs("default"),
|
126
126
|
"premiere": pAttrs("premiere", name_attr),
|
127
127
|
"resolve-fcp7": pAttrs("resolve-fcp7", name_attr),
|
128
|
-
"final-cut-pro": pAttrs(
|
128
|
+
"final-cut-pro": pAttrs(
|
129
|
+
"final-cut-pro", name_attr, pAttr("version", 11, is_int)
|
130
|
+
),
|
129
131
|
"resolve": pAttrs("resolve", name_attr),
|
130
132
|
"shotcut": pAttrs("shotcut"),
|
131
133
|
"json": pAttrs("json", pAttr("api", 3, is_int)),
|
@@ -146,7 +148,7 @@ def parse_export(export: str, log: Log) -> dict[str, Any]:
|
|
146
148
|
|
147
149
|
|
148
150
|
def edit_media(paths: list[str], ffmpeg: FFmpeg, args: Args, log: Log) -> None:
|
149
|
-
bar =
|
151
|
+
bar = initBar(args.progress)
|
150
152
|
tl = None
|
151
153
|
|
152
154
|
if paths:
|
@@ -232,11 +234,19 @@ def edit_media(paths: list[str], ffmpeg: FFmpeg, args: Args, log: Log) -> None:
|
|
232
234
|
fcp7_write_xml(export_ops["name"], output, is_resolve, tl, log)
|
233
235
|
return
|
234
236
|
|
235
|
-
if export
|
237
|
+
if export == "final-cut-pro":
|
236
238
|
from auto_editor.formats.fcp11 import fcp11_write_xml
|
237
239
|
|
238
|
-
|
239
|
-
fcp11_write_xml(export_ops["name"],
|
240
|
+
ver = export_ops["version"]
|
241
|
+
fcp11_write_xml(export_ops["name"], ver, output, False, tl, log)
|
242
|
+
return
|
243
|
+
|
244
|
+
if export == "resolve":
|
245
|
+
from auto_editor.formats.fcp11 import fcp11_write_xml
|
246
|
+
from auto_editor.timeline import set_stream_to_0
|
247
|
+
|
248
|
+
set_stream_to_0(tl, log)
|
249
|
+
fcp11_write_xml(export_ops["name"], 10, output, True, tl, log)
|
240
250
|
return
|
241
251
|
|
242
252
|
if export == "shotcut":
|
@@ -127,6 +127,31 @@ class FFmpeg:
|
|
127
127
|
return output
|
128
128
|
|
129
129
|
|
130
|
+
def mux(input: Path, output: Path, stream: int, codec: str | None = None) -> None:
|
131
|
+
input_container = av.open(input, "r")
|
132
|
+
output_container = av.open(output, "w")
|
133
|
+
|
134
|
+
input_audio_stream = input_container.streams.audio[stream]
|
135
|
+
|
136
|
+
if codec is None:
|
137
|
+
codec = "pcm_s16le"
|
138
|
+
|
139
|
+
output_audio_stream = output_container.add_stream(codec)
|
140
|
+
assert isinstance(output_audio_stream, av.audio.AudioStream)
|
141
|
+
|
142
|
+
for frame in input_container.decode(input_audio_stream):
|
143
|
+
packet = output_audio_stream.encode(frame)
|
144
|
+
if packet:
|
145
|
+
output_container.mux(packet)
|
146
|
+
|
147
|
+
packet = output_audio_stream.encode(None)
|
148
|
+
if packet:
|
149
|
+
output_container.mux(packet)
|
150
|
+
|
151
|
+
output_container.close()
|
152
|
+
input_container.close()
|
153
|
+
|
154
|
+
|
130
155
|
@dataclass(slots=True, frozen=True)
|
131
156
|
class VideoStream:
|
132
157
|
width: int
|
@@ -269,7 +294,7 @@ def initFileInfo(path: str, log: Log) -> FileInfo:
|
|
269
294
|
|
270
295
|
desc = cont.metadata.get("description", None)
|
271
296
|
bitrate = 0 if cont.bit_rate is None else cont.bit_rate
|
272
|
-
dur = 0 if cont.duration is None else cont.duration /
|
297
|
+
dur = 0 if cont.duration is None else cont.duration / av.time_base
|
273
298
|
|
274
299
|
cont.close()
|
275
300
|
|
@@ -3,17 +3,15 @@ from __future__ import annotations
|
|
3
3
|
from typing import TYPE_CHECKING, Any, cast
|
4
4
|
from xml.etree.ElementTree import Element, ElementTree, SubElement, indent
|
5
5
|
|
6
|
-
from auto_editor.ffwrapper import FFmpeg, FileInfo, initFileInfo
|
7
|
-
|
8
|
-
from .utils import make_tracks_dir
|
9
|
-
|
10
6
|
if TYPE_CHECKING:
|
11
7
|
from collections.abc import Sequence
|
12
8
|
from fractions import Fraction
|
13
9
|
|
10
|
+
from auto_editor.ffwrapper import FileInfo
|
14
11
|
from auto_editor.timeline import TlAudio, TlVideo, v3
|
15
12
|
from auto_editor.utils.log import Log
|
16
13
|
|
14
|
+
|
17
15
|
"""
|
18
16
|
Export a FCPXML 11 file readable with Final Cut Pro 10.6.8 or later.
|
19
17
|
|
@@ -54,7 +52,7 @@ def make_name(src: FileInfo, tb: Fraction) -> str:
|
|
54
52
|
|
55
53
|
|
56
54
|
def fcp11_write_xml(
|
57
|
-
group_name: str,
|
55
|
+
group_name: str, version: int, output: str, resolve: bool, tl: v3, log: Log
|
58
56
|
) -> None:
|
59
57
|
def fraction(val: int) -> str:
|
60
58
|
if val == 0:
|
@@ -68,23 +66,17 @@ def fcp11_write_xml(
|
|
68
66
|
src_dur = int(src.duration * tl.tb)
|
69
67
|
tl_dur = src_dur if resolve else tl.out_len()
|
70
68
|
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
newtrack = fold / f"{i}.wav"
|
78
|
-
ffmpeg.run(
|
79
|
-
["-i", f"{src.path.resolve()}", "-map", f"0:a:{i}", f"{newtrack}"]
|
80
|
-
)
|
81
|
-
all_srcs.append(initFileInfo(f"{newtrack}", log))
|
82
|
-
all_refs.append(f"r{(i + 1) * 2}")
|
69
|
+
if version == 11:
|
70
|
+
ver_str = "1.11"
|
71
|
+
elif version == 10:
|
72
|
+
ver_str = "1.10"
|
73
|
+
else:
|
74
|
+
log.error(f"Unknown final cut pro version: {version}")
|
83
75
|
|
84
|
-
fcpxml = Element("fcpxml", version=
|
76
|
+
fcpxml = Element("fcpxml", version=ver_str)
|
85
77
|
resources = SubElement(fcpxml, "resources")
|
86
78
|
|
87
|
-
for i, one_src in enumerate(
|
79
|
+
for i, one_src in enumerate(tl.unique_sources()):
|
88
80
|
SubElement(
|
89
81
|
resources,
|
90
82
|
"format",
|
@@ -126,13 +118,6 @@ def fcp11_write_xml(
|
|
126
118
|
)
|
127
119
|
spine = SubElement(sequence, "spine")
|
128
120
|
|
129
|
-
if tl.v and tl.v[0]:
|
130
|
-
clips: Sequence[TlVideo | TlAudio] = cast(Any, tl.v[0])
|
131
|
-
elif tl.a and tl.a[0]:
|
132
|
-
clips = tl.a[0]
|
133
|
-
else:
|
134
|
-
clips = []
|
135
|
-
|
136
121
|
def make_clip(ref: str, clip: TlVideo | TlAudio) -> None:
|
137
122
|
clip_properties = {
|
138
123
|
"name": proj_name,
|
@@ -157,7 +142,19 @@ def fcp11_write_xml(
|
|
157
142
|
interp="smooth2",
|
158
143
|
)
|
159
144
|
|
160
|
-
|
145
|
+
if tl.v and tl.v[0]:
|
146
|
+
clips: Sequence[TlVideo | TlAudio] = cast(Any, tl.v[0])
|
147
|
+
elif tl.a and tl.a[0]:
|
148
|
+
clips = tl.a[0]
|
149
|
+
else:
|
150
|
+
clips = []
|
151
|
+
|
152
|
+
all_refs: list[str] = ["r2"]
|
153
|
+
if resolve:
|
154
|
+
for i in range(1, len(tl.a)):
|
155
|
+
all_refs.append(f"r{(i + 1) * 2}")
|
156
|
+
|
157
|
+
for my_ref in reversed(all_refs):
|
161
158
|
for clip in clips:
|
162
159
|
make_clip(my_ref, clip)
|
163
160
|
|
@@ -4,9 +4,6 @@ from typing import TYPE_CHECKING
|
|
4
4
|
from xml.etree.ElementTree import Element
|
5
5
|
|
6
6
|
if TYPE_CHECKING:
|
7
|
-
from pathlib import Path
|
8
|
-
|
9
|
-
from auto_editor.ffwrapper import FileInfo
|
10
7
|
from auto_editor.utils.log import Log
|
11
8
|
|
12
9
|
|
@@ -19,21 +16,6 @@ def show(ele: Element, limit: int, depth: int = 0) -> None:
|
|
19
16
|
show(child, limit, depth + 1)
|
20
17
|
|
21
18
|
|
22
|
-
def make_tracks_dir(src: FileInfo) -> Path:
|
23
|
-
from os import mkdir
|
24
|
-
from shutil import rmtree
|
25
|
-
|
26
|
-
fold = src.path.parent / f"{src.path.stem}_tracks"
|
27
|
-
|
28
|
-
try:
|
29
|
-
mkdir(fold)
|
30
|
-
except OSError:
|
31
|
-
rmtree(fold)
|
32
|
-
mkdir(fold)
|
33
|
-
|
34
|
-
return fold
|
35
|
-
|
36
|
-
|
37
19
|
class Validator:
|
38
20
|
def __init__(self, log: Log):
|
39
21
|
self.log = log
|
@@ -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 =
|
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(
|
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
|
-
|
241
|
-
|
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)
|
@@ -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
|
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,
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
@@ -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
|
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 =
|
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
|
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,
|
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], ["-
|
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"])
|
@@ -3,6 +3,7 @@ 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
9
|
from auto_editor.utils.types import color, natural, number, threshold
|
@@ -10,10 +11,12 @@ from auto_editor.utils.types import color, natural, number, threshold
|
|
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)
|
@@ -241,6 +244,13 @@ video\n"""
|
|
241
244
|
for a in aclips:
|
242
245
|
yield a.src
|
243
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
|
+
|
244
254
|
def _duration(self, layer: Any) -> int:
|
245
255
|
total_dur = 0
|
246
256
|
for clips in layer:
|
@@ -276,3 +286,38 @@ video\n"""
|
|
276
286
|
"v": v,
|
277
287
|
"a": a,
|
278
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
|
@@ -1,6 +1,7 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
3
|
import sys
|
4
|
+
from dataclasses import dataclass
|
4
5
|
from math import floor
|
5
6
|
from shutil import get_terminal_size
|
6
7
|
from time import localtime, time
|
@@ -8,39 +9,50 @@ from time import localtime, time
|
|
8
9
|
from .func import get_stdout_bytes
|
9
10
|
|
10
11
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
12
|
+
def initBar(bar_type: str) -> Bar:
|
13
|
+
icon = "⏳"
|
14
|
+
chars: tuple[str, ...] = (" ", "▏", "▎", "▍", "▌", "▋", "▊", "▉", "█")
|
15
|
+
brackets = ("|", "|")
|
16
|
+
machine = hide = False
|
17
|
+
|
18
|
+
if bar_type == "classic":
|
19
|
+
icon = "⏳"
|
20
|
+
chars = ("░", "█")
|
21
|
+
brackets = ("[", "]")
|
22
|
+
if bar_type == "ascii":
|
23
|
+
icon = "& "
|
24
|
+
chars = ("-", "#")
|
25
|
+
brackets = ("[", "]")
|
26
|
+
if bar_type == "machine":
|
27
|
+
machine = True
|
28
|
+
if bar_type == "none":
|
29
|
+
hide = True
|
30
|
+
|
31
|
+
part_width = len(chars) - 1
|
32
|
+
|
33
|
+
ampm = True
|
34
|
+
if sys.platform == "darwin" and bar_type in ("modern", "classic", "ascii"):
|
35
|
+
try:
|
36
|
+
date_format = get_stdout_bytes(
|
37
|
+
["defaults", "read", "com.apple.menuextra.clock", "Show24Hour"]
|
38
|
+
)
|
39
|
+
ampm = date_format == b"0\n"
|
40
|
+
except FileNotFoundError:
|
41
|
+
pass
|
15
42
|
|
16
|
-
|
17
|
-
self.chars: tuple[str, ...] = (" ", "▏", "▎", "▍", "▌", "▋", "▊", "▉", "█")
|
18
|
-
self.brackets = ("|", "|")
|
43
|
+
return Bar(icon, chars, brackets, machine, hide, part_width, ampm, [])
|
19
44
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
self.hide = True
|
32
|
-
|
33
|
-
self.part_width = len(self.chars) - 1
|
34
|
-
|
35
|
-
self.ampm = True
|
36
|
-
if sys.platform == "darwin" and bar_type in ("modern", "classic", "ascii"):
|
37
|
-
try:
|
38
|
-
date_format = get_stdout_bytes(
|
39
|
-
["defaults", "read", "com.apple.menuextra.clock", "Show24Hour"]
|
40
|
-
)
|
41
|
-
self.ampm = date_format == b"0\n"
|
42
|
-
except FileNotFoundError:
|
43
|
-
pass
|
45
|
+
|
46
|
+
@dataclass(slots=True)
|
47
|
+
class Bar:
|
48
|
+
icon: str
|
49
|
+
chars: tuple[str, ...]
|
50
|
+
brackets: tuple[str, str]
|
51
|
+
machine: bool
|
52
|
+
hide: bool
|
53
|
+
part_width: int
|
54
|
+
ampm: bool
|
55
|
+
stack: list[tuple[str, int, float, float]]
|
44
56
|
|
45
57
|
@staticmethod
|
46
58
|
def pretty_time(my_time: float, ampm: bool) -> str:
|
@@ -62,28 +74,25 @@ class Bar:
|
|
62
74
|
if self.hide:
|
63
75
|
return
|
64
76
|
|
65
|
-
|
66
|
-
|
77
|
+
title, len_title, total, begin = self.stack[-1]
|
78
|
+
progress = 0.0 if total == 0 else min(1, max(0, index / total))
|
79
|
+
rate = 0.0 if progress == 0 else (time() - begin) / progress
|
67
80
|
|
68
81
|
if self.machine:
|
69
|
-
index = min(index,
|
70
|
-
secs_til_eta = round(
|
71
|
-
print(
|
72
|
-
f"{self.title}~{index}~{self.total}~{secs_til_eta}",
|
73
|
-
end="\r",
|
74
|
-
flush=True,
|
75
|
-
)
|
82
|
+
index = min(index, total)
|
83
|
+
secs_til_eta = round(begin + rate - time(), 2)
|
84
|
+
print(f"{title}~{index}~{total}~{secs_til_eta}", end="\r", flush=True)
|
76
85
|
return
|
77
86
|
|
78
|
-
new_time = self.pretty_time(
|
87
|
+
new_time = self.pretty_time(begin + rate, self.ampm)
|
79
88
|
|
80
89
|
percent = round(progress * 100, 1)
|
81
90
|
p_pad = " " * (4 - len(str(percent)))
|
82
91
|
columns = get_terminal_size().columns
|
83
|
-
bar_len = max(1, columns - (
|
92
|
+
bar_len = max(1, columns - (len_title + 32))
|
84
93
|
bar_str = self._bar_str(progress, bar_len)
|
85
94
|
|
86
|
-
bar = f" {self.icon}{
|
95
|
+
bar = f" {self.icon}{title} {bar_str} {p_pad}{percent}% ETA {new_time}"
|
87
96
|
|
88
97
|
if len(bar) > columns - 2:
|
89
98
|
bar = bar[: columns - 2]
|
@@ -93,10 +102,7 @@ class Bar:
|
|
93
102
|
sys.stdout.write(bar + "\r")
|
94
103
|
|
95
104
|
def start(self, total: float, title: str = "Please wait") -> None:
|
96
|
-
self.title
|
97
|
-
self.len_title = len(title)
|
98
|
-
self.total = total
|
99
|
-
self.begin_time = time()
|
105
|
+
self.stack.append((title, len(title), total, time()))
|
100
106
|
|
101
107
|
try:
|
102
108
|
self.tick(0)
|
@@ -124,6 +130,7 @@ class Bar:
|
|
124
130
|
)
|
125
131
|
return line
|
126
132
|
|
127
|
-
|
128
|
-
def end() -> None:
|
133
|
+
def end(self) -> None:
|
129
134
|
sys.stdout.write(" " * (get_terminal_size().columns - 2) + "\r")
|
135
|
+
if self.stack:
|
136
|
+
self.stack.pop()
|
@@ -224,6 +224,7 @@ class Args:
|
|
224
224
|
scale: float = 1.0
|
225
225
|
extras: str | None = None
|
226
226
|
sn: bool = False
|
227
|
+
dn: bool = False
|
227
228
|
no_seek: bool = False
|
228
229
|
cut_out: list[tuple[str, str]] = field(default_factory=list)
|
229
230
|
add_in: list[tuple[str, str]] = field(default_factory=list)
|
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|
3
3
|
import io
|
4
4
|
import struct
|
5
5
|
import sys
|
6
|
-
from typing import Literal
|
6
|
+
from typing import TYPE_CHECKING, Literal
|
7
7
|
|
8
8
|
import numpy as np
|
9
9
|
|
@@ -15,13 +15,17 @@ AudioData = np.memmap | np.ndarray
|
|
15
15
|
Endian = Literal[">", "<"] # Big Endian, Little Endian
|
16
16
|
ByteOrd = Literal["big", "little"]
|
17
17
|
|
18
|
+
if TYPE_CHECKING:
|
19
|
+
Reader = io.BufferedReader | io.BytesIO
|
20
|
+
Writer = io.BufferedWriter | io.BytesIO
|
21
|
+
|
18
22
|
|
19
23
|
class WavError(Exception):
|
20
24
|
pass
|
21
25
|
|
22
26
|
|
23
27
|
def _read_fmt_chunk(
|
24
|
-
fid:
|
28
|
+
fid: Reader, bytes_order: ByteOrd
|
25
29
|
) -> tuple[int, int, int, int, int]:
|
26
30
|
size = int.from_bytes(fid.read(4), bytes_order)
|
27
31
|
|
@@ -69,7 +73,7 @@ def _read_fmt_chunk(
|
|
69
73
|
|
70
74
|
|
71
75
|
def _read_data_chunk(
|
72
|
-
fid:
|
76
|
+
fid: Reader,
|
73
77
|
format_tag: int,
|
74
78
|
channels: int,
|
75
79
|
bit_depth: int,
|
@@ -114,16 +118,22 @@ def _read_data_chunk(
|
|
114
118
|
else:
|
115
119
|
n_samples = (size - 1) // block_align
|
116
120
|
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
+
if isinstance(fid, io.BufferedReader):
|
122
|
+
data: AudioData = np.memmap(
|
123
|
+
fid, dtype=dtype, mode="c", offset=fid.tell(), shape=(n_samples, channels)
|
124
|
+
)
|
125
|
+
fid.seek(size, 1)
|
126
|
+
else:
|
127
|
+
bytes_per_sample = np.dtype(dtype).itemsize
|
128
|
+
buffer = fid.read(n_samples * channels * bytes_per_sample)
|
129
|
+
data = np.frombuffer(buffer, dtype=dtype).reshape((n_samples, channels))
|
130
|
+
|
121
131
|
_handle_pad_byte(fid, size)
|
122
132
|
|
123
133
|
return data
|
124
134
|
|
125
135
|
|
126
|
-
def _skip_unknown_chunk(fid:
|
136
|
+
def _skip_unknown_chunk(fid: Reader, en: Endian) -> None:
|
127
137
|
data = fid.read(4)
|
128
138
|
|
129
139
|
if len(data) == 4:
|
@@ -140,7 +150,7 @@ def _skip_unknown_chunk(fid: io.BufferedReader, en: Endian) -> None:
|
|
140
150
|
)
|
141
151
|
|
142
152
|
|
143
|
-
def _read_rf64_chunk(fid:
|
153
|
+
def _read_rf64_chunk(fid: Reader) -> tuple[int, int, Endian]:
|
144
154
|
# https://tech.ebu.ch/docs/tech/tech3306v1_0.pdf
|
145
155
|
# https://www.itu.int/dms_pubrec/itu-r/rec/bs/R-REC-BS.2088-1-201910-I!!PDF-E.pdf
|
146
156
|
|
@@ -171,7 +181,7 @@ def _read_rf64_chunk(fid: io.BufferedReader) -> tuple[int, int, Endian]:
|
|
171
181
|
return data_size, file_size, en
|
172
182
|
|
173
183
|
|
174
|
-
def _read_riff_chunk(sig: bytes, fid:
|
184
|
+
def _read_riff_chunk(sig: bytes, fid: Reader) -> tuple[None, int, Endian]:
|
175
185
|
en: Endian = "<" if sig == b"RIFF" else ">"
|
176
186
|
bytes_order: ByteOrd = "big" if en == ">" else "little"
|
177
187
|
|
@@ -184,14 +194,12 @@ def _read_riff_chunk(sig: bytes, fid: io.BufferedReader) -> tuple[None, int, End
|
|
184
194
|
return None, file_size, en
|
185
195
|
|
186
196
|
|
187
|
-
def _handle_pad_byte(fid:
|
197
|
+
def _handle_pad_byte(fid: Reader, size: int) -> None:
|
188
198
|
if size % 2 == 1:
|
189
199
|
fid.seek(1, 1)
|
190
200
|
|
191
201
|
|
192
|
-
def read(
|
193
|
-
fid = open(filename, "rb")
|
194
|
-
|
202
|
+
def read(fid: Reader) -> tuple[int, AudioData]:
|
195
203
|
file_sig = fid.read(4)
|
196
204
|
if file_sig in (b"RIFF", b"RIFX"):
|
197
205
|
data_size, file_size, en = _read_riff_chunk(file_sig, fid)
|
@@ -241,7 +249,7 @@ def read(filename: str) -> tuple[int, AudioData]:
|
|
241
249
|
raise WavError("Found no data")
|
242
250
|
|
243
251
|
|
244
|
-
def write(fid:
|
252
|
+
def write(fid: Writer, sr: int, arr: np.ndarray) -> None:
|
245
253
|
channels = 1 if arr.ndim == 1 else arr.shape[1]
|
246
254
|
bit_depth = arr.dtype.itemsize * 8
|
247
255
|
block_align = channels * (bit_depth // 8)
|
@@ -290,7 +298,8 @@ def main() -> None:
|
|
290
298
|
with open("test.wav", "wb") as file:
|
291
299
|
write(file, 48_000, data)
|
292
300
|
|
293
|
-
|
301
|
+
with open("test.wav", "rb") as file:
|
302
|
+
read_sr, read_data = read(file)
|
294
303
|
|
295
304
|
assert read_sr == 48_000
|
296
305
|
assert np.array_equal(data, read_data)
|
@@ -1 +0,0 @@
|
|
1
|
-
__version__ = "25.2.0"
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|