noblebarkrr commited on
Commit
fd905fe
·
verified ·
1 Parent(s): e9b6907

Исправлена проблема с целочисленными типами данных при объединении каналов в моно в методе read

Browse files
Files changed (1) hide show
  1. mvsepless/audio.py +789 -789
mvsepless/audio.py CHANGED
@@ -1,789 +1,789 @@
1
- import os
2
- from pathlib import Path
3
- import sys
4
- import json
5
- import subprocess
6
- import numpy as np
7
- from typing import Literal
8
- from collections.abc import Callable
9
- from pathlib import Path
10
- from numpy.typing import DTypeLike
11
- import tempfile
12
- import librosa
13
-
14
- if not __package__:
15
- from namer import Namer
16
- else:
17
- from .namer import Namer
18
-
19
-
20
- class NotInputFileSpecified(Exception):
21
- pass
22
-
23
-
24
- class NotOutputFileSpecified(Exception):
25
- pass
26
-
27
-
28
- class NotSupportedDataType(Exception):
29
- pass
30
-
31
-
32
- class ErrorDecode(Exception):
33
- pass
34
-
35
-
36
- class ErrorEncode(Exception):
37
- pass
38
-
39
-
40
- class NotSupportedFormat(Exception):
41
- pass
42
-
43
-
44
- class SampleRateError(Exception):
45
- pass
46
-
47
-
48
- class FileIsNotAudio(Exception):
49
- pass
50
-
51
-
52
- class Audio(Namer):
53
- def __init__(self):
54
- super().__init__()
55
- self.ffmpeg_path = os.environ.get("MVSEPLESS_FFMPEG", "ffmpeg")
56
- self.ffprobe_path = os.environ.get("MVSEPLESS_FFPROBE", "ffprobe")
57
- self.output_formats = (
58
- "mp3",
59
- "wav",
60
- "flac",
61
- "ogg",
62
- "opus",
63
- "m4a",
64
- "aac",
65
- "ac3",
66
- "aiff",
67
- )
68
- self.input_formats = (
69
- "mp3",
70
- "wav",
71
- "flac",
72
- "ogg",
73
- "opus",
74
- "m4a",
75
- "aac",
76
- "ac3",
77
- "aiff",
78
- "mp4",
79
- "mkv",
80
- "webm",
81
- "avi",
82
- "mov",
83
- "ts",
84
- )
85
- self.supported_dtypes = ("int16", "int32", "float32", "float64")
86
- self.dtypes_dict = {
87
- "int16": "s16le",
88
- "int32": "s32le",
89
- "float32": "f32le",
90
- "float64": "f64le",
91
- np.int16: "s16le",
92
- np.int32: "s32le",
93
- np.float32: "f32le",
94
- np.float64: "f64le",
95
- }
96
- self.bitrate_limit = {
97
- "mp3": {"min": 8, "max": 320},
98
- "aac": {"min": 8, "max": 512},
99
- "m4a": {"min": 8, "max": 512},
100
- "ac3": {"min": 32, "max": 640},
101
- "ogg": {"min": 64, "max": 500},
102
- "opus": {"min": 6, "max": 512},
103
- }
104
- self.sample_rates = {
105
- "mp3": {
106
- "supported": (
107
- 44100,
108
- 48000,
109
- 32000,
110
- 22050,
111
- 24000,
112
- 16000,
113
- 11025,
114
- 12000,
115
- 8000,
116
- )
117
- },
118
- "opus": {"supported": (48000, 24000, 16000, 12000, 8000)},
119
- "m4a": {
120
- "supported": (
121
- 96000,
122
- 88200,
123
- 64000,
124
- 48000,
125
- 44100,
126
- 32000,
127
- 24000,
128
- 22050,
129
- 16000,
130
- 12000,
131
- 11025,
132
- 8000,
133
- 7350,
134
- )
135
- },
136
- "aac": {
137
- "supported": (
138
- 96000,
139
- 88200,
140
- 64000,
141
- 48000,
142
- 44100,
143
- 32000,
144
- 24000,
145
- 22050,
146
- 16000,
147
- 12000,
148
- 11025,
149
- 8000,
150
- 7350,
151
- )
152
- },
153
- "ac3": {
154
- "supported": (
155
- 48000,
156
- 44100,
157
- 32000,
158
- )
159
- },
160
- "ogg": {"min": 6, "max": 192000},
161
- "wav": {"min": 0, "max": float("inf")},
162
- "aiff": {"min": 0, "max": float("inf")},
163
- "flac": {"min": 0, "max": 192000},
164
- }
165
- self.check_ffmpeg()
166
- self.check_ffprobe()
167
-
168
- def check_ffmpeg(self):
169
- try:
170
- ffmpeg_version_output = subprocess.check_output(
171
- [self.ffmpeg_path, "-version"], text=True
172
- )
173
- except FileNotFoundError:
174
- if "PYTEST_CURRENT_TEST" not in os.environ:
175
- raise FileNotFoundError(
176
- "FFMPEG не установлен. Укажите путь к установленному FFMPEG через переменную окружения MVSEPLESS_FFMPEG"
177
- )
178
-
179
- def check_ffprobe(self):
180
- try:
181
- ffmpeg_version_output = subprocess.check_output(
182
- [self.ffprobe_path, "-version"], text=True
183
- )
184
- except FileNotFoundError:
185
- if "PYTEST_CURRENT_TEST" not in os.environ:
186
- raise FileNotFoundError(
187
- "FFPROBE не установлен. Укажите путь к установленному FFPROBE через переменную окружения MVSEPLESS_FFPROBE"
188
- )
189
-
190
- def fit_sr(
191
- self,
192
- f: (
193
- str
194
- | Literal["mp3", "wav", "flac", "ogg", "opus", "m4a", "aac", "ac3", "aiff"]
195
- ) = "mp3",
196
- sr: int = 44100,
197
- ) -> int:
198
- format_info = self.sample_rates.get(f.lower())
199
-
200
- if not format_info:
201
- return None
202
-
203
- if "supported" in format_info:
204
- supported_rates = format_info["supported"]
205
- if sr in supported_rates:
206
- return sr
207
-
208
- return min(supported_rates, key=lambda x: abs(x - sr))
209
-
210
- elif "min" in format_info and "max" in format_info:
211
- min_rate = format_info["min"]
212
- max_rate = format_info["max"]
213
-
214
- if sr < min_rate:
215
- return min_rate
216
- elif sr > max_rate:
217
- return max_rate
218
- else:
219
- return sr
220
-
221
- return None
222
-
223
- def fit_br(
224
- self,
225
- f: (
226
- str
227
- | Literal["mp3", "wav", "flac", "ogg", "opus", "m4a", "aac", "ac3", "aiff"]
228
- ) = "mp3",
229
- br: int = 320,
230
- ) -> int:
231
- if f not in self.bitrate_limit:
232
- raise NotSupportedFormat(f"Формат {f} не поддерживается")
233
-
234
- limits = self.bitrate_limit[f]
235
-
236
- if br < limits["min"]:
237
- return limits["min"]
238
- elif br > limits["max"]:
239
- return limits["max"]
240
- else:
241
- return br
242
-
243
- def get_info(
244
- self,
245
- i: str | os.PathLike | Callable | None = None,
246
- ) -> dict[int, dict[int, float]]:
247
- audio_info = {}
248
- if i:
249
- if isinstance(i, Path):
250
- i = str(i)
251
- if os.path.exists(i):
252
- cmd = [
253
- self.ffprobe_path,
254
- "-i",
255
- i,
256
- "-v",
257
- "quiet",
258
- "-hide_banner",
259
- "-show_entries",
260
- "stream=index,sample_rate,duration",
261
- "-select_streams",
262
- "a",
263
- "-of",
264
- "json",
265
- ]
266
-
267
- process = subprocess.Popen(
268
- cmd,
269
- stdin=subprocess.PIPE,
270
- stdout=subprocess.PIPE,
271
- stderr=subprocess.PIPE,
272
- )
273
-
274
- stdout, stderr = process.communicate()
275
-
276
- if process.returncode != 0:
277
- print(f"STDERR: {stderr.decode('utf-8')}")
278
- print(f"STDOUT: {stdout.decode('utf-8')}")
279
-
280
- json_output = json.loads(stdout)
281
- streams = json_output["streams"]
282
- if not streams:
283
- pass
284
-
285
- else:
286
- for a, stream in enumerate(streams):
287
- audio_info[a] = {
288
- "sample_rate": int(stream.get("sample_rate", 0)),
289
- "duration": float(stream.get("duration", 0)),
290
- }
291
-
292
- return audio_info
293
-
294
- else:
295
- raise FileExistsError("Указанного файла не существует")
296
-
297
- else:
298
- raise NotInputFileSpecified("Не указан путь к файлу")
299
-
300
- def check(self, i: str | os.PathLike | Callable | None = None) -> bool:
301
- if i:
302
- if isinstance(i, Path):
303
- i = str(i)
304
- if os.path.exists(i):
305
- info = self.get_info(i=i)
306
- if info:
307
- list_streams = list(info.keys())
308
- if len(list_streams) > 0:
309
- if info[0].get("sample_rate") > 0:
310
- return True
311
- else:
312
- return False
313
- else:
314
- return False
315
- else:
316
- return False
317
- else:
318
- raise FileExistsError("Указанного файла не существует")
319
- else:
320
- raise NotInputFileSpecified("Не указан путь к файлу")
321
-
322
- def read(
323
- self,
324
- i: str | os.PathLike | Callable | None = None,
325
- sr: int | None = None,
326
- mono: bool = False,
327
- dtype: DTypeLike = np.float32,
328
- s: int = 0,
329
- ) -> tuple[np.ndarray, int, float]:
330
- output_format = self.dtypes_dict.get(dtype, None)
331
- if not output_format:
332
- raise NotSupportedDataType(f"Этот тип данных не поддерживается {dtype}")
333
- if i:
334
- if isinstance(i, Path):
335
- i = str(i)
336
- if os.path.exists(i):
337
- audio_info = self.get_info(i=i)
338
- list_streams = list(audio_info.keys())
339
- if audio_info.get(s, False):
340
- stream = s
341
- else:
342
- if len(list_streams) > 0:
343
- stream = 0
344
- else:
345
- raise FileIsNotAudio("В входном файле нет аудио потоков")
346
-
347
- sample_rate_input = audio_info[stream]["sample_rate"]
348
- if sample_rate_input == 0:
349
- raise FileIsNotAudio("В входном файле нет аудио потоков")
350
-
351
- cmd = [
352
- self.ffmpeg_path,
353
- "-i",
354
- i,
355
- "-map",
356
- f"0:a:{stream}",
357
- "-vn",
358
- "-f",
359
- output_format,
360
- "-ac",
361
- "1" if mono else "2",
362
- ]
363
-
364
- if sr:
365
- cmd.extend(["-ar", str(sr)])
366
- else:
367
- sr = sample_rate_input
368
-
369
- cmd.append("pipe:1")
370
-
371
- process = subprocess.Popen(
372
- cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, bufsize=10**8
373
- )
374
-
375
- try:
376
-
377
- raw_audio, stderr = process.communicate(timeout=300)
378
-
379
- if process.returncode != 0:
380
- raise ErrorDecode(f"FFmpeg error: {stderr.decode()}")
381
-
382
- except subprocess.TimeoutExpired:
383
- process.kill()
384
- raise ErrorDecode("FFmpeg timeout при чтении файла")
385
-
386
- audio_array = np.frombuffer(raw_audio, dtype=dtype)
387
-
388
- channels = 1 if mono else 2
389
- audio_array = audio_array.reshape((-1, channels)).T
390
- if audio_array.ndim > 1 and channels == 1:
391
- audio_array = np.mean(
392
- audio_array, axis=tuple(range(audio_array.ndim - 1))
393
- )
394
-
395
- len_samples = float(audio_array.shape[-1])
396
-
397
- duration = len_samples / sr
398
-
399
- print(f"Частота дискретизации: {sr}")
400
-
401
- return audio_array.copy(), sr, duration
402
- else:
403
- raise FileExistsError("Указанного файла не существует")
404
-
405
- else:
406
- raise NotInputFileSpecified("Не указан путь к файлу")
407
-
408
- def write(
409
- self,
410
- o: str | os.PathLike | Callable | None = None,
411
- array: np.ndarray = np.array([], dtype=np.float32),
412
- sr: int = 44100,
413
- of: (
414
- str
415
- | Literal["mp3", "wav", "flac", "ogg", "opus", "m4a", "aac", "ac3", "aiff"]
416
- | None
417
- ) = None,
418
- br: str | int | None = None,
419
- ) -> str:
420
- if isinstance(array, np.ndarray):
421
-
422
- if len(array.shape) == 1:
423
- array = array.reshape(-1, 1)
424
- elif len(array.shape) == 2:
425
- if array.shape[0] == 2:
426
- array = array.T
427
- else:
428
- raise ValueError(
429
- "numpy-массив должен быть либо одномерным, либо двухмерным"
430
- )
431
-
432
- if array.dtype == np.int16:
433
- input_format = "s16le"
434
- elif array.dtype == np.int32:
435
- input_format = "s32le"
436
- elif array.dtype == np.float32:
437
- input_format = "f32le"
438
- elif array.dtype == np.float64:
439
- input_format = "f64le"
440
- else:
441
- raise NotSupportedDataType(
442
- f"Этот тип данных не поддерживается {array.dtype}"
443
- )
444
-
445
- if array.shape[1] == 1:
446
- audio_bytes = array.tobytes()
447
-
448
- channels = 1
449
-
450
- elif array.shape[1] == 2:
451
- audio_bytes = array.tobytes()
452
-
453
- channels = 2
454
- else:
455
- raise ValueError("numpy-массив должен содержать 1 или 2 канала")
456
-
457
- else:
458
- raise ValueError("Вход должен быть numpy-массивом")
459
-
460
- if o:
461
- if isinstance(o, Path):
462
- o = str(o)
463
- output_dir = os.path.dirname(o)
464
- output_base = os.path.basename(o)
465
- output_name, output_ext = os.path.splitext(output_base)
466
- if output_dir != "":
467
- os.makedirs(output_dir, exist_ok=True)
468
- if output_ext == "":
469
- if of:
470
- o += f".{of}"
471
- else:
472
- o += f".mp3"
473
- elif output_ext == ".":
474
- if of:
475
- o += f"{of}"
476
- else:
477
- o += f"mp3"
478
- else:
479
- raise NotOutputFileSpecified("Не указан путь к выходному файлу")
480
-
481
- if of:
482
- if of in self.output_formats:
483
- output_name, output_ext = os.path.splitext(o)
484
- if output_ext == f".{of}":
485
- pass
486
- else:
487
- o = f"{os.path.join(output_dir, output_name)}.{of}"
488
- else:
489
- raise NotSupportedFormat(f"Неподдерживаемый формат: {of}")
490
- else:
491
- of = os.path.splitext(o)[1].strip(".")
492
- if of in self.output_formats:
493
- pass
494
- else:
495
- raise NotSupportedFormat(f"Неподдерживаемый формат: {of}")
496
-
497
- if sr:
498
- if isinstance(sr, int):
499
- sample_rate_fixed = self.fit_sr(f=of, sr=sr)
500
- elif isinstance(sr, float):
501
- sr = int(sr)
502
- sample_rate_fixed = self.fit_sr(f=of, sr=sr)
503
- else:
504
- raise SampleRateError(
505
- f"Частота дискретизации должна быть числом\n\nЗначение: {sr}\nТип: {type(sr)}"
506
- )
507
- else:
508
- raise SampleRateError("Не указана частота дискретизации")
509
-
510
- bitrate_fixed = "320k"
511
-
512
- if of not in ["wav", "flac", "aiff"]:
513
- if br:
514
- if isinstance(br, int):
515
- bitrate_fixed = self.fit_br(f=of, br=br)
516
- elif isinstance(br, float):
517
- bitrate_fixed = self.fit_br(f=of, br=int(br))
518
- elif isinstance(br, str):
519
- bitrate_fixed = self.fit_br(f=of, br=int(br.strip("k").strip("K")))
520
- else:
521
- bitrate_fixed = self.fit_br(f=of, br=320)
522
- else:
523
- bitrate_fixed = self.fit_br(of, 320)
524
-
525
- format_settings = {
526
- "wav": [
527
- "-c:a",
528
- "pcm_f32le",
529
- "-sample_fmt",
530
- "flt",
531
- ],
532
- "aiff": [
533
- "-c:a",
534
- "pcm_f32be",
535
- "-sample_fmt",
536
- "flt",
537
- ],
538
- "flac": [
539
- "-c:a",
540
- "flac",
541
- "-compression_level",
542
- "12",
543
- "-sample_fmt",
544
- "s32",
545
- ],
546
- "mp3": [
547
- "-c:a",
548
- "libmp3lame",
549
- "-b:a",
550
- f"{bitrate_fixed}k",
551
- ],
552
- "ogg": [
553
- "-c:a",
554
- "libvorbis",
555
- "-b:a",
556
- f"{bitrate_fixed}k",
557
- ],
558
- "opus": [
559
- "-c:a",
560
- "libopus",
561
- "-b:a",
562
- f"{bitrate_fixed}k",
563
- ],
564
- "m4a": [
565
- "-c:a",
566
- "aac",
567
- "-b:a",
568
- f"{bitrate_fixed}k",
569
- ],
570
- "aac": [
571
- "-c:a",
572
- "aac",
573
- "-b:a",
574
- f"{bitrate_fixed}k",
575
- ],
576
- "ac3": [
577
- "-c:a",
578
- "ac3",
579
- "-b:a",
580
- f"{bitrate_fixed}k",
581
- ],
582
- }
583
-
584
- cmd = [
585
- self.ffmpeg_path,
586
- "-y",
587
- "-f",
588
- input_format,
589
- "-ar",
590
- str(sr),
591
- "-ac",
592
- str(channels),
593
- "-i",
594
- "pipe:0",
595
- "-ac",
596
- str(channels),
597
- ]
598
-
599
- cmd.extend(["-ar", str(sample_rate_fixed)])
600
- cmd.extend(format_settings[of])
601
- o_dir, o_base = os.path.split(o)
602
- o_base_n, o_base_ext = os.path.splitext(o_base)
603
- o_base_n = self.sanitize(o_base_n)
604
- o_base_n = self.short(o_base_n)
605
- o = os.path.join(o_dir, f"{o_base_n}{o_base_ext}")
606
- o = self.iter(o)
607
- cmd.append(o)
608
-
609
- process = subprocess.Popen(
610
- cmd,
611
- stdin=subprocess.PIPE,
612
- stdout=subprocess.PIPE,
613
- stderr=subprocess.PIPE,
614
- )
615
-
616
- try:
617
- stdout, stderr = process.communicate(input=audio_bytes, timeout=300)
618
- except subprocess.TimeoutExpired:
619
- process.kill()
620
- raise ErrorEncode("FFmpeg timeout: операция заняла слишком много времени")
621
-
622
- if process.returncode != 0:
623
- raise ErrorEncode(
624
- f"FFmpeg завершился с ошибкой (код: {process.returncode})"
625
- )
626
-
627
- return os.path.abspath(o)
628
-
629
-
630
- class Inverter(Audio):
631
- def __init__(self):
632
- super().__init__()
633
- self.test = "test"
634
- self.w_types = [
635
- "boxcar",
636
- "triang",
637
- "blackman",
638
- "hamming",
639
- "hann",
640
- "bartlett",
641
- "flattop",
642
- "parzen",
643
- "bohman",
644
- "blackmanharris",
645
- "nuttall",
646
- "barthann",
647
- "cosine",
648
- "exponential",
649
- "tukey",
650
- "taylor",
651
- "lanczos",
652
- ]
653
-
654
- def load_audio(self, filepath):
655
- try:
656
- y, sr, _ = self.read(i=filepath, sr=None, mono=False)
657
- return y, sr
658
- except Exception as e:
659
- print(f"Ошибка загрузки аудио: {e}")
660
- return None, None
661
-
662
- def process_channel(
663
- self, y1_ch, y2_ch, sr, method, w_size=2048, overlap=2, w_type="hann"
664
- ):
665
- HOP_LENGTH = w_size // overlap
666
- if method == "waveform":
667
- return y1_ch - y2_ch
668
-
669
- elif method == "spectrogram":
670
- S1 = librosa.stft(
671
- y1_ch, n_fft=w_size, hop_length=HOP_LENGTH, win_length=w_size
672
- )
673
- S2 = librosa.stft(
674
- y2_ch, n_fft=w_size, hop_length=HOP_LENGTH, win_length=w_size
675
- )
676
-
677
- mag1 = np.abs(S1)
678
- mag2 = np.abs(S2)
679
-
680
- mag_result = np.maximum(mag1 - mag2, 0)
681
-
682
- phase = np.angle(S1)
683
-
684
- S_result = mag_result * np.exp(1j * phase)
685
-
686
- return librosa.istft(
687
- S_result,
688
- n_fft=w_size,
689
- hop_length=HOP_LENGTH,
690
- win_length=w_size,
691
- length=len(y1_ch),
692
- )
693
-
694
- def process_audio(
695
- self,
696
- audio1_path,
697
- audio2_path,
698
- out_format,
699
- method,
700
- output_path="./inverted.mp3",
701
- w_size=2048,
702
- overlap=2,
703
- w_type="hann",
704
- ):
705
- y1, sr1 = self.load_audio(audio1_path)
706
- y2, sr2 = self.load_audio(audio2_path)
707
-
708
- if sr1 is None or sr2 is None:
709
- raise Exception("Произошла ошибка при чтении файлов")
710
-
711
- channels1 = 1 if y1.ndim == 1 else y1.shape[0]
712
- channels2 = 1 if y2.ndim == 1 else y2.shape[0]
713
-
714
- if channels1 > 1:
715
- y1 = y1.T
716
- else:
717
- y1 = y1.reshape(-1, 1)
718
-
719
- if channels2 > 1:
720
- y2 = y2.T
721
- else:
722
- y2 = y2.reshape(-1, 1)
723
-
724
- if sr1 != sr2:
725
- if channels2 > 1:
726
- y2_resampled_list = []
727
- for c in range(channels2):
728
- channel_resampled = librosa.resample(
729
- y2[:, c], orig_sr=sr2, target_sr=sr1
730
- )
731
- y2_resampled_list.append(channel_resampled)
732
-
733
- min_channel_length = min(len(ch) for ch in y2_resampled_list)
734
-
735
- y2_resampled = np.zeros(
736
- (min_channel_length, channels2), dtype=np.float32
737
- )
738
- for c, channel in enumerate(y2_resampled_list):
739
- y2_resampled[:, c] = channel[:min_channel_length]
740
-
741
- y2 = y2_resampled
742
- else:
743
- y2 = librosa.resample(y2[:, 0], orig_sr=sr2, target_sr=sr1)
744
- y2 = y2.reshape(-1, 1)
745
- sr2 = sr1
746
-
747
- min_len = min(len(y1), len(y2))
748
- y1 = y1[:min_len]
749
- y2 = y2[:min_len]
750
-
751
- result_channels = []
752
-
753
- if channels1 == 1 and channels2 > 1:
754
- y2 = y2.mean(axis=1, keepdims=True)
755
- channels2 = 1
756
-
757
- for c in range(channels1):
758
- y1_ch = y1[:, c]
759
-
760
- if channels2 == 1:
761
- y2_ch = y2[:, 0]
762
- else:
763
- y2_ch = y2[:, min(c, channels2 - 1)]
764
-
765
- result_ch = self.process_channel(
766
- y1_ch, y2_ch, sr1, method, w_size=w_size, overlap=overlap, w_type=w_type
767
- )
768
- result_channels.append(result_ch)
769
-
770
- if len(result_channels) > 1:
771
- result = np.column_stack(result_channels)
772
- else:
773
- result = np.array(result_channels[0])
774
-
775
- if result.ndim > 1:
776
- for c in range(result.shape[1]):
777
- channel = result[:, c]
778
- max_val = np.max(np.abs(channel))
779
- if max_val > 0:
780
- result[:, c] = channel * 0.9 / max_val
781
- else:
782
- max_val = np.max(np.abs(result))
783
- if max_val > 0:
784
- result = result * 0.9 / max_val
785
-
786
- inverted = self.write(
787
- o=output_path, array=result.T, sr=sr1, of=out_format, br="320k"
788
- )
789
- return inverted
 
1
+ import os
2
+ from pathlib import Path
3
+ import sys
4
+ import json
5
+ import subprocess
6
+ import numpy as np
7
+ from typing import Literal
8
+ from collections.abc import Callable
9
+ from pathlib import Path
10
+ from numpy.typing import DTypeLike
11
+ import tempfile
12
+ import librosa
13
+
14
+ if not __package__:
15
+ from namer import Namer
16
+ else:
17
+ from .namer import Namer
18
+
19
+
20
+ class NotInputFileSpecified(Exception):
21
+ pass
22
+
23
+
24
+ class NotOutputFileSpecified(Exception):
25
+ pass
26
+
27
+
28
+ class NotSupportedDataType(Exception):
29
+ pass
30
+
31
+
32
+ class ErrorDecode(Exception):
33
+ pass
34
+
35
+
36
+ class ErrorEncode(Exception):
37
+ pass
38
+
39
+
40
+ class NotSupportedFormat(Exception):
41
+ pass
42
+
43
+
44
+ class SampleRateError(Exception):
45
+ pass
46
+
47
+
48
+ class FileIsNotAudio(Exception):
49
+ pass
50
+
51
+
52
+ class Audio(Namer):
53
+ def __init__(self):
54
+ super().__init__()
55
+ self.ffmpeg_path = os.environ.get("MVSEPLESS_FFMPEG", "ffmpeg")
56
+ self.ffprobe_path = os.environ.get("MVSEPLESS_FFPROBE", "ffprobe")
57
+ self.output_formats = (
58
+ "mp3",
59
+ "wav",
60
+ "flac",
61
+ "ogg",
62
+ "opus",
63
+ "m4a",
64
+ "aac",
65
+ "ac3",
66
+ "aiff",
67
+ )
68
+ self.input_formats = (
69
+ "mp3",
70
+ "wav",
71
+ "flac",
72
+ "ogg",
73
+ "opus",
74
+ "m4a",
75
+ "aac",
76
+ "ac3",
77
+ "aiff",
78
+ "mp4",
79
+ "mkv",
80
+ "webm",
81
+ "avi",
82
+ "mov",
83
+ "ts",
84
+ )
85
+ self.supported_dtypes = ("int16", "int32", "float32", "float64")
86
+ self.dtypes_dict = {
87
+ "int16": "s16le",
88
+ "int32": "s32le",
89
+ "float32": "f32le",
90
+ "float64": "f64le",
91
+ np.int16: "s16le",
92
+ np.int32: "s32le",
93
+ np.float32: "f32le",
94
+ np.float64: "f64le",
95
+ }
96
+ self.bitrate_limit = {
97
+ "mp3": {"min": 8, "max": 320},
98
+ "aac": {"min": 8, "max": 512},
99
+ "m4a": {"min": 8, "max": 512},
100
+ "ac3": {"min": 32, "max": 640},
101
+ "ogg": {"min": 64, "max": 500},
102
+ "opus": {"min": 6, "max": 512},
103
+ }
104
+ self.sample_rates = {
105
+ "mp3": {
106
+ "supported": (
107
+ 44100,
108
+ 48000,
109
+ 32000,
110
+ 22050,
111
+ 24000,
112
+ 16000,
113
+ 11025,
114
+ 12000,
115
+ 8000,
116
+ )
117
+ },
118
+ "opus": {"supported": (48000, 24000, 16000, 12000, 8000)},
119
+ "m4a": {
120
+ "supported": (
121
+ 96000,
122
+ 88200,
123
+ 64000,
124
+ 48000,
125
+ 44100,
126
+ 32000,
127
+ 24000,
128
+ 22050,
129
+ 16000,
130
+ 12000,
131
+ 11025,
132
+ 8000,
133
+ 7350,
134
+ )
135
+ },
136
+ "aac": {
137
+ "supported": (
138
+ 96000,
139
+ 88200,
140
+ 64000,
141
+ 48000,
142
+ 44100,
143
+ 32000,
144
+ 24000,
145
+ 22050,
146
+ 16000,
147
+ 12000,
148
+ 11025,
149
+ 8000,
150
+ 7350,
151
+ )
152
+ },
153
+ "ac3": {
154
+ "supported": (
155
+ 48000,
156
+ 44100,
157
+ 32000,
158
+ )
159
+ },
160
+ "ogg": {"min": 6, "max": 192000},
161
+ "wav": {"min": 0, "max": float("inf")},
162
+ "aiff": {"min": 0, "max": float("inf")},
163
+ "flac": {"min": 0, "max": 192000},
164
+ }
165
+ self.check_ffmpeg()
166
+ self.check_ffprobe()
167
+
168
+ def check_ffmpeg(self):
169
+ try:
170
+ ffmpeg_version_output = subprocess.check_output(
171
+ [self.ffmpeg_path, "-version"], text=True
172
+ )
173
+ except FileNotFoundError:
174
+ if "PYTEST_CURRENT_TEST" not in os.environ:
175
+ raise FileNotFoundError(
176
+ "FFMPEG не установлен. Укажите путь к установленному FFMPEG через переменную окружения MVSEPLESS_FFMPEG"
177
+ )
178
+
179
+ def check_ffprobe(self):
180
+ try:
181
+ ffmpeg_version_output = subprocess.check_output(
182
+ [self.ffprobe_path, "-version"], text=True
183
+ )
184
+ except FileNotFoundError:
185
+ if "PYTEST_CURRENT_TEST" not in os.environ:
186
+ raise FileNotFoundError(
187
+ "FFPROBE не установлен. Укажите путь к установленному FFPROBE через переменную окружения MVSEPLESS_FFPROBE"
188
+ )
189
+
190
+ def fit_sr(
191
+ self,
192
+ f: (
193
+ str
194
+ | Literal["mp3", "wav", "flac", "ogg", "opus", "m4a", "aac", "ac3", "aiff"]
195
+ ) = "mp3",
196
+ sr: int = 44100,
197
+ ) -> int:
198
+ format_info = self.sample_rates.get(f.lower())
199
+
200
+ if not format_info:
201
+ return None
202
+
203
+ if "supported" in format_info:
204
+ supported_rates = format_info["supported"]
205
+ if sr in supported_rates:
206
+ return sr
207
+
208
+ return min(supported_rates, key=lambda x: abs(x - sr))
209
+
210
+ elif "min" in format_info and "max" in format_info:
211
+ min_rate = format_info["min"]
212
+ max_rate = format_info["max"]
213
+
214
+ if sr < min_rate:
215
+ return min_rate
216
+ elif sr > max_rate:
217
+ return max_rate
218
+ else:
219
+ return sr
220
+
221
+ return None
222
+
223
+ def fit_br(
224
+ self,
225
+ f: (
226
+ str
227
+ | Literal["mp3", "wav", "flac", "ogg", "opus", "m4a", "aac", "ac3", "aiff"]
228
+ ) = "mp3",
229
+ br: int = 320,
230
+ ) -> int:
231
+ if f not in self.bitrate_limit:
232
+ raise NotSupportedFormat(f"Формат {f} не поддерживается")
233
+
234
+ limits = self.bitrate_limit[f]
235
+
236
+ if br < limits["min"]:
237
+ return limits["min"]
238
+ elif br > limits["max"]:
239
+ return limits["max"]
240
+ else:
241
+ return br
242
+
243
+ def get_info(
244
+ self,
245
+ i: str | os.PathLike | Callable | None = None,
246
+ ) -> dict[int, dict[int, float]]:
247
+ audio_info = {}
248
+ if i:
249
+ if isinstance(i, Path):
250
+ i = str(i)
251
+ if os.path.exists(i):
252
+ cmd = [
253
+ self.ffprobe_path,
254
+ "-i",
255
+ i,
256
+ "-v",
257
+ "quiet",
258
+ "-hide_banner",
259
+ "-show_entries",
260
+ "stream=index,sample_rate,duration",
261
+ "-select_streams",
262
+ "a",
263
+ "-of",
264
+ "json",
265
+ ]
266
+
267
+ process = subprocess.Popen(
268
+ cmd,
269
+ stdin=subprocess.PIPE,
270
+ stdout=subprocess.PIPE,
271
+ stderr=subprocess.PIPE,
272
+ )
273
+
274
+ stdout, stderr = process.communicate()
275
+
276
+ if process.returncode != 0:
277
+ print(f"STDERR: {stderr.decode('utf-8')}")
278
+ print(f"STDOUT: {stdout.decode('utf-8')}")
279
+
280
+ json_output = json.loads(stdout)
281
+ streams = json_output["streams"]
282
+ if not streams:
283
+ pass
284
+
285
+ else:
286
+ for a, stream in enumerate(streams):
287
+ audio_info[a] = {
288
+ "sample_rate": int(stream.get("sample_rate", 0)),
289
+ "duration": float(stream.get("duration", 0)),
290
+ }
291
+
292
+ return audio_info
293
+
294
+ else:
295
+ raise FileExistsError("Указанного файла не существует")
296
+
297
+ else:
298
+ raise NotInputFileSpecified("Не указан путь к файлу")
299
+
300
+ def check(self, i: str | os.PathLike | Callable | None = None) -> bool:
301
+ if i:
302
+ if isinstance(i, Path):
303
+ i = str(i)
304
+ if os.path.exists(i):
305
+ info = self.get_info(i=i)
306
+ if info:
307
+ list_streams = list(info.keys())
308
+ if len(list_streams) > 0:
309
+ if info[0].get("sample_rate") > 0:
310
+ return True
311
+ else:
312
+ return False
313
+ else:
314
+ return False
315
+ else:
316
+ return False
317
+ else:
318
+ raise FileExistsError("Указанного файла не существует")
319
+ else:
320
+ raise NotInputFileSpecified("Не указан путь к файлу")
321
+
322
+ def read(
323
+ self,
324
+ i: str | os.PathLike | Callable | None = None,
325
+ sr: int | None = None,
326
+ mono: bool = False,
327
+ dtype: DTypeLike = np.float32,
328
+ s: int = 0,
329
+ ) -> tuple[np.ndarray, int, float]:
330
+ output_format = self.dtypes_dict.get(dtype, None)
331
+ if not output_format:
332
+ raise NotSupportedDataType(f"Этот тип данных не поддерживается {dtype}")
333
+ if i:
334
+ if isinstance(i, Path):
335
+ i = str(i)
336
+ if os.path.exists(i):
337
+ audio_info = self.get_info(i=i)
338
+ list_streams = list(audio_info.keys())
339
+ if audio_info.get(s, False):
340
+ stream = s
341
+ else:
342
+ if len(list_streams) > 0:
343
+ stream = 0
344
+ else:
345
+ raise FileIsNotAudio("В входном файле нет аудио потоков")
346
+
347
+ sample_rate_input = audio_info[stream]["sample_rate"]
348
+ if sample_rate_input == 0:
349
+ raise FileIsNotAudio("В входном файле нет аудио потоков")
350
+
351
+ cmd = [
352
+ self.ffmpeg_path,
353
+ "-i",
354
+ i,
355
+ "-map",
356
+ f"0:a:{stream}",
357
+ "-vn",
358
+ "-f",
359
+ output_format,
360
+ "-ac",
361
+ "1" if mono else "2",
362
+ ]
363
+
364
+ if sr:
365
+ cmd.extend(["-ar", str(sr)])
366
+ else:
367
+ sr = sample_rate_input
368
+
369
+ cmd.append("pipe:1")
370
+
371
+ process = subprocess.Popen(
372
+ cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, bufsize=10**8
373
+ )
374
+
375
+ try:
376
+
377
+ raw_audio, stderr = process.communicate(timeout=300)
378
+
379
+ if process.returncode != 0:
380
+ raise ErrorDecode(f"FFmpeg error: {stderr.decode()}")
381
+
382
+ except subprocess.TimeoutExpired:
383
+ process.kill()
384
+ raise ErrorDecode("FFmpeg timeout при чтении файла")
385
+
386
+ audio_array = np.frombuffer(raw_audio, dtype=dtype)
387
+
388
+ channels = 1 if mono else 2
389
+ audio_array = audio_array.reshape((-1, channels)).T
390
+ if audio_array.ndim > 1 and channels == 1:
391
+ audio_array = np.mean(
392
+ audio_array, axis=tuple(range(audio_array.ndim - 1)), dtype=dtype
393
+ )
394
+
395
+ len_samples = float(audio_array.shape[-1])
396
+
397
+ duration = len_samples / sr
398
+
399
+ print(f"Частота дискретизации: {sr}")
400
+
401
+ return audio_array.copy(), sr, duration
402
+ else:
403
+ raise FileExistsError("Указанного файла не существует")
404
+
405
+ else:
406
+ raise NotInputFileSpecified("Не указан путь к файлу")
407
+
408
+ def write(
409
+ self,
410
+ o: str | os.PathLike | Callable | None = None,
411
+ array: np.ndarray = np.array([], dtype=np.float32),
412
+ sr: int = 44100,
413
+ of: (
414
+ str
415
+ | Literal["mp3", "wav", "flac", "ogg", "opus", "m4a", "aac", "ac3", "aiff"]
416
+ | None
417
+ ) = None,
418
+ br: str | int | None = None,
419
+ ) -> str:
420
+ if isinstance(array, np.ndarray):
421
+
422
+ if len(array.shape) == 1:
423
+ array = array.reshape(-1, 1)
424
+ elif len(array.shape) == 2:
425
+ if array.shape[0] == 2:
426
+ array = array.T
427
+ else:
428
+ raise ValueError(
429
+ "numpy-массив должен быть либо одномерным, либо двухмерным"
430
+ )
431
+
432
+ if array.dtype == np.int16:
433
+ input_format = "s16le"
434
+ elif array.dtype == np.int32:
435
+ input_format = "s32le"
436
+ elif array.dtype == np.float32:
437
+ input_format = "f32le"
438
+ elif array.dtype == np.float64:
439
+ input_format = "f64le"
440
+ else:
441
+ raise NotSupportedDataType(
442
+ f"Этот тип данных не поддерживается {array.dtype}"
443
+ )
444
+
445
+ if array.shape[1] == 1:
446
+ audio_bytes = array.tobytes()
447
+
448
+ channels = 1
449
+
450
+ elif array.shape[1] == 2:
451
+ audio_bytes = array.tobytes()
452
+
453
+ channels = 2
454
+ else:
455
+ raise ValueError("numpy-массив должен содержать 1 или 2 канала")
456
+
457
+ else:
458
+ raise ValueError("Вход должен быть numpy-массивом")
459
+
460
+ if o:
461
+ if isinstance(o, Path):
462
+ o = str(o)
463
+ output_dir = os.path.dirname(o)
464
+ output_base = os.path.basename(o)
465
+ output_name, output_ext = os.path.splitext(output_base)
466
+ if output_dir != "":
467
+ os.makedirs(output_dir, exist_ok=True)
468
+ if output_ext == "":
469
+ if of:
470
+ o += f".{of}"
471
+ else:
472
+ o += f".mp3"
473
+ elif output_ext == ".":
474
+ if of:
475
+ o += f"{of}"
476
+ else:
477
+ o += f"mp3"
478
+ else:
479
+ raise NotOutputFileSpecified("Не указан путь к выходному файлу")
480
+
481
+ if of:
482
+ if of in self.output_formats:
483
+ output_name, output_ext = os.path.splitext(o)
484
+ if output_ext == f".{of}":
485
+ pass
486
+ else:
487
+ o = f"{os.path.join(output_dir, output_name)}.{of}"
488
+ else:
489
+ raise NotSupportedFormat(f"Неподдерживаемый формат: {of}")
490
+ else:
491
+ of = os.path.splitext(o)[1].strip(".")
492
+ if of in self.output_formats:
493
+ pass
494
+ else:
495
+ raise NotSupportedFormat(f"Неподдерживаемый формат: {of}")
496
+
497
+ if sr:
498
+ if isinstance(sr, int):
499
+ sample_rate_fixed = self.fit_sr(f=of, sr=sr)
500
+ elif isinstance(sr, float):
501
+ sr = int(sr)
502
+ sample_rate_fixed = self.fit_sr(f=of, sr=sr)
503
+ else:
504
+ raise SampleRateError(
505
+ f"Частота дискретизации должна быть числом\n\nЗначение: {sr}\nТип: {type(sr)}"
506
+ )
507
+ else:
508
+ raise SampleRateError("Не указана частота дискретизации")
509
+
510
+ bitrate_fixed = "320k"
511
+
512
+ if of not in ["wav", "flac", "aiff"]:
513
+ if br:
514
+ if isinstance(br, int):
515
+ bitrate_fixed = self.fit_br(f=of, br=br)
516
+ elif isinstance(br, float):
517
+ bitrate_fixed = self.fit_br(f=of, br=int(br))
518
+ elif isinstance(br, str):
519
+ bitrate_fixed = self.fit_br(f=of, br=int(br.strip("k").strip("K")))
520
+ else:
521
+ bitrate_fixed = self.fit_br(f=of, br=320)
522
+ else:
523
+ bitrate_fixed = self.fit_br(of, 320)
524
+
525
+ format_settings = {
526
+ "wav": [
527
+ "-c:a",
528
+ "pcm_f32le",
529
+ "-sample_fmt",
530
+ "flt",
531
+ ],
532
+ "aiff": [
533
+ "-c:a",
534
+ "pcm_f32be",
535
+ "-sample_fmt",
536
+ "flt",
537
+ ],
538
+ "flac": [
539
+ "-c:a",
540
+ "flac",
541
+ "-compression_level",
542
+ "12",
543
+ "-sample_fmt",
544
+ "s32",
545
+ ],
546
+ "mp3": [
547
+ "-c:a",
548
+ "libmp3lame",
549
+ "-b:a",
550
+ f"{bitrate_fixed}k",
551
+ ],
552
+ "ogg": [
553
+ "-c:a",
554
+ "libvorbis",
555
+ "-b:a",
556
+ f"{bitrate_fixed}k",
557
+ ],
558
+ "opus": [
559
+ "-c:a",
560
+ "libopus",
561
+ "-b:a",
562
+ f"{bitrate_fixed}k",
563
+ ],
564
+ "m4a": [
565
+ "-c:a",
566
+ "aac",
567
+ "-b:a",
568
+ f"{bitrate_fixed}k",
569
+ ],
570
+ "aac": [
571
+ "-c:a",
572
+ "aac",
573
+ "-b:a",
574
+ f"{bitrate_fixed}k",
575
+ ],
576
+ "ac3": [
577
+ "-c:a",
578
+ "ac3",
579
+ "-b:a",
580
+ f"{bitrate_fixed}k",
581
+ ],
582
+ }
583
+
584
+ cmd = [
585
+ self.ffmpeg_path,
586
+ "-y",
587
+ "-f",
588
+ input_format,
589
+ "-ar",
590
+ str(sr),
591
+ "-ac",
592
+ str(channels),
593
+ "-i",
594
+ "pipe:0",
595
+ "-ac",
596
+ str(channels),
597
+ ]
598
+
599
+ cmd.extend(["-ar", str(sample_rate_fixed)])
600
+ cmd.extend(format_settings[of])
601
+ o_dir, o_base = os.path.split(o)
602
+ o_base_n, o_base_ext = os.path.splitext(o_base)
603
+ o_base_n = self.sanitize(o_base_n)
604
+ o_base_n = self.short(o_base_n)
605
+ o = os.path.join(o_dir, f"{o_base_n}{o_base_ext}")
606
+ o = self.iter(o)
607
+ cmd.append(o)
608
+
609
+ process = subprocess.Popen(
610
+ cmd,
611
+ stdin=subprocess.PIPE,
612
+ stdout=subprocess.PIPE,
613
+ stderr=subprocess.PIPE,
614
+ )
615
+
616
+ try:
617
+ stdout, stderr = process.communicate(input=audio_bytes, timeout=300)
618
+ except subprocess.TimeoutExpired:
619
+ process.kill()
620
+ raise ErrorEncode("FFmpeg timeout: операция заняла слишком много времени")
621
+
622
+ if process.returncode != 0:
623
+ raise ErrorEncode(
624
+ f"FFmpeg завершился с ошибкой (код: {process.returncode})"
625
+ )
626
+
627
+ return os.path.abspath(o)
628
+
629
+
630
+ class Inverter(Audio):
631
+ def __init__(self):
632
+ super().__init__()
633
+ self.test = "test"
634
+ self.w_types = [
635
+ "boxcar",
636
+ "triang",
637
+ "blackman",
638
+ "hamming",
639
+ "hann",
640
+ "bartlett",
641
+ "flattop",
642
+ "parzen",
643
+ "bohman",
644
+ "blackmanharris",
645
+ "nuttall",
646
+ "barthann",
647
+ "cosine",
648
+ "exponential",
649
+ "tukey",
650
+ "taylor",
651
+ "lanczos",
652
+ ]
653
+
654
+ def load_audio(self, filepath):
655
+ try:
656
+ y, sr, _ = self.read(i=filepath, sr=None, mono=False)
657
+ return y, sr
658
+ except Exception as e:
659
+ print(f"Ошибка загрузки аудио: {e}")
660
+ return None, None
661
+
662
+ def process_channel(
663
+ self, y1_ch, y2_ch, sr, method, w_size=2048, overlap=2, w_type="hann"
664
+ ):
665
+ HOP_LENGTH = w_size // overlap
666
+ if method == "waveform":
667
+ return y1_ch - y2_ch
668
+
669
+ elif method == "spectrogram":
670
+ S1 = librosa.stft(
671
+ y1_ch, n_fft=w_size, hop_length=HOP_LENGTH, win_length=w_size
672
+ )
673
+ S2 = librosa.stft(
674
+ y2_ch, n_fft=w_size, hop_length=HOP_LENGTH, win_length=w_size
675
+ )
676
+
677
+ mag1 = np.abs(S1)
678
+ mag2 = np.abs(S2)
679
+
680
+ mag_result = np.maximum(mag1 - mag2, 0)
681
+
682
+ phase = np.angle(S1)
683
+
684
+ S_result = mag_result * np.exp(1j * phase)
685
+
686
+ return librosa.istft(
687
+ S_result,
688
+ n_fft=w_size,
689
+ hop_length=HOP_LENGTH,
690
+ win_length=w_size,
691
+ length=len(y1_ch),
692
+ )
693
+
694
+ def process_audio(
695
+ self,
696
+ audio1_path,
697
+ audio2_path,
698
+ out_format,
699
+ method,
700
+ output_path="./inverted.mp3",
701
+ w_size=2048,
702
+ overlap=2,
703
+ w_type="hann",
704
+ ):
705
+ y1, sr1 = self.load_audio(audio1_path)
706
+ y2, sr2 = self.load_audio(audio2_path)
707
+
708
+ if sr1 is None or sr2 is None:
709
+ raise Exception("Произошла ошибка при чтении файлов")
710
+
711
+ channels1 = 1 if y1.ndim == 1 else y1.shape[0]
712
+ channels2 = 1 if y2.ndim == 1 else y2.shape[0]
713
+
714
+ if channels1 > 1:
715
+ y1 = y1.T
716
+ else:
717
+ y1 = y1.reshape(-1, 1)
718
+
719
+ if channels2 > 1:
720
+ y2 = y2.T
721
+ else:
722
+ y2 = y2.reshape(-1, 1)
723
+
724
+ if sr1 != sr2:
725
+ if channels2 > 1:
726
+ y2_resampled_list = []
727
+ for c in range(channels2):
728
+ channel_resampled = librosa.resample(
729
+ y2[:, c], orig_sr=sr2, target_sr=sr1
730
+ )
731
+ y2_resampled_list.append(channel_resampled)
732
+
733
+ min_channel_length = min(len(ch) for ch in y2_resampled_list)
734
+
735
+ y2_resampled = np.zeros(
736
+ (min_channel_length, channels2), dtype=np.float32
737
+ )
738
+ for c, channel in enumerate(y2_resampled_list):
739
+ y2_resampled[:, c] = channel[:min_channel_length]
740
+
741
+ y2 = y2_resampled
742
+ else:
743
+ y2 = librosa.resample(y2[:, 0], orig_sr=sr2, target_sr=sr1)
744
+ y2 = y2.reshape(-1, 1)
745
+ sr2 = sr1
746
+
747
+ min_len = min(len(y1), len(y2))
748
+ y1 = y1[:min_len]
749
+ y2 = y2[:min_len]
750
+
751
+ result_channels = []
752
+
753
+ if channels1 == 1 and channels2 > 1:
754
+ y2 = y2.mean(axis=1, keepdims=True)
755
+ channels2 = 1
756
+
757
+ for c in range(channels1):
758
+ y1_ch = y1[:, c]
759
+
760
+ if channels2 == 1:
761
+ y2_ch = y2[:, 0]
762
+ else:
763
+ y2_ch = y2[:, min(c, channels2 - 1)]
764
+
765
+ result_ch = self.process_channel(
766
+ y1_ch, y2_ch, sr1, method, w_size=w_size, overlap=overlap, w_type=w_type
767
+ )
768
+ result_channels.append(result_ch)
769
+
770
+ if len(result_channels) > 1:
771
+ result = np.column_stack(result_channels)
772
+ else:
773
+ result = np.array(result_channels[0])
774
+
775
+ if result.ndim > 1:
776
+ for c in range(result.shape[1]):
777
+ channel = result[:, c]
778
+ max_val = np.max(np.abs(channel))
779
+ if max_val > 0:
780
+ result[:, c] = channel * 0.9 / max_val
781
+ else:
782
+ max_val = np.max(np.abs(result))
783
+ if max_val > 0:
784
+ result = result * 0.9 / max_val
785
+
786
+ inverted = self.write(
787
+ o=output_path, array=result.T, sr=sr1, of=out_format, br="320k"
788
+ )
789
+ return inverted