-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathaudio_editor.py
More file actions
245 lines (197 loc) · 9.84 KB
/
audio_editor.py
File metadata and controls
245 lines (197 loc) · 9.84 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
from collections import defaultdict
from enum import Enum
from pathlib import Path
from threading import Lock, Thread
from typing import DefaultDict, Union
from pydub import AudioSegment
from PyQt6.QtCore import QObject, pyqtSignal
from class_of_changes import (ChangeOfOctave, ChangeOfSampleRate,
ChangeOfSpeed, ChangeOfVolume, CutOutFragment,
GlueFragments, Reverse)
from stack import Stack
ten = 1000
class WorkerSignals(QObject):
"""Класс сигналов для обработки событий в фоновых потоках.
Атрибуты:
finished: Сигнал, испускаемый при успешном завершении операции.
error: Сигнал, испускаемый при возникновении ошибки."""
finished = pyqtSignal()
error = pyqtSignal(str)
class Extension(Enum):
Wav = ".wav"
Mp3 = ".mp3"
class AudioEditor:
"""Класс аудиоредактора, который выполняет все манипуляции со звуком."""
_history_of_actions: Stack[
Union[ChangeOfSpeed, ChangeOfVolume, CutOutFragment, GlueFragments, Reverse, ChangeOfSampleRate, ChangeOfOctave]
]
def __init__(self, path: str):
"""Инициализирует аудиоредактор.
Args:
path (str): Путь к аудиофайлу для редактирования.
Raises:
TypeException: Если файл не в формате WAV или MP3.
"""
self._signals = WorkerSignals()
self._history_of_actions = Stack()
self._history_of_actions_str: list[str] = []
self._changes: DefaultDict[str, list[ChangeOfVolume | ChangeOfSpeed | ChangeOfSampleRate | ChangeOfOctave]] = (
defaultdict(list)
)
self._reversed = False
self._song = None
self._lock = Lock()
self._song_path = Path(path)
song_name = self._song_path.name
extension = self._song_path.suffix
if extension == Extension.Wav.value:
self._song = AudioSegment.from_wav(song_name)
elif extension == Extension.Mp3.value:
self._song = AudioSegment.from_mp3(song_name)
else:
raise TypeException("Wrong type of audio-extension")
self._current = self._song
def change_volume(self, x: float) -> None:
"""Изменяет громкость аудио.
Создает объект Change_Of_Volume, выполняет execute и добавляет действие в историю.
Аргументы:
x (float): Значение изменения громкости.
"""
action = ChangeOfVolume(x)
self.apply_change(action, "volume", x)
def change_speed(self, x: float) -> None:
"""Изменяет скорость воспроизведения аудио.
Создает объект Change_Of_Speed, выполняет execute и добавляет действие в историю.
Аргументы:
x (float): Значение изменения скорости.
"""
action = ChangeOfSpeed(x)
self.apply_change(action, "speed", x)
def change_sample_rate(self, x: int) -> None:
"""Изменяет частоту аудио.
Создает объект ChangeOfSampleRate, выполняет execute и добавляет действие в историю.
Аргументы:
x (int): Значение изменения частоты.
"""
action = ChangeOfSampleRate(x)
self.apply_change(action, "sample_rate", x)
def change_octave(self, x: float) -> None:
"""Изменяет высоту аудио.
Создает объект ChangeOfOctave, выполняет execute и добавляет действие в историю.
Аргументы:
x (float): Значение изменения высоты.
"""
action = ChangeOfOctave(x)
self.apply_change(action, "octave", x)
def apply_change(
self,
action: Union[ChangeOfVolume, ChangeOfSpeed, ChangeOfSampleRate, ChangeOfOctave],
change_type: str,
value: float,
) -> None:
self._history_of_actions.push(action)
self._changes[action.__class__.__name__].append(action)
self._history_of_actions_str.append(f"changed {change_type} on {value}")
def cut_fragment(self, first_index: int, second_index: int, path: str) -> None:
"""Вырезает фрагмент аудио в отдельном потоке.
Args:
first_index (int): Начальный индекс фрагмента (в миллисекундах).
second_index (int): Конечный индекс фрагмента (в миллисекундах).
path (str): Путь для сохранения вырезанного фрагмента."""
def worker() -> None:
try:
with self._lock:
current_song = self.update_original_song()
action = CutOutFragment(first_index, second_index)
self._song = action.execute(self._song)
cut_out = current_song[first_index:second_index]
cut_out.export(path)
self._history_of_actions.push(action)
self._history_of_actions_str.append(f"cut out fragment from {first_index} to {second_index}")
self._signals.finished.emit()
except Exception as e:
self._signals.error.emit(f"Error in audio processing: {e}")
Thread(target=worker, daemon=True).start()
def glue_fragments(self, fragment_one: AudioSegment, fragment_two: AudioSegment, song_path: str) -> None:
"""Склеивает два аудиофрагмента и сохраняет результат.
Args:
fragment_one (AudioSegment): Первый аудиофрагмент.
fragment_two (AudioSegment): Второй аудиофрагмент.
song_path (str): Путь для сохранения склеенного аудио."""
current = self.update_original_song()
if not fragment_one:
fragment_one = current
if not fragment_two:
fragment_two = current
action = GlueFragments(fragment_one, fragment_two)
song = action.execute()
if song_path:
song.export(song_path)
self._history_of_actions_str.append("glued two fragments")
def save(self, path: str) -> None:
"""Сохраняет текущее состояние аудиофайла.
Аргументы:
path (str): Путь для сохранения файла.
"""
self._song = self.update_original_song()
self._song.export(path)
def history_changes(self) -> list[str]:
"""Возвращает историю изменений в виде списка строк.
Возвращает:
list[str]: Список описаний выполненных действий.
"""
if self._history_of_actions_str:
return self._history_of_actions_str
return []
def backwards(self) -> None:
"""Переворачивает аудиофайл (воспроизведение в обратном порядке).
Создает объект Reverse, выполняет execute и добавляет действие в историю.
"""
if self._current:
action = Reverse()
self._history_of_actions.push(action)
self._history_of_actions_str.append("backwards")
self._reversed = not self._reversed
def undo(
self,
) -> (
Union[ChangeOfVolume, ChangeOfSpeed, ChangeOfSampleRate, CutOutFragment, GlueFragments, Reverse, ChangeOfOctave]
| None
):
"""Отменяет последнее действие.
Возвращает:
Any | None: Возвращает объект последнего действия (для Change_Of_Speed/Volume),
либо None для других действий.
"""
with self._lock:
if not self._history_of_actions.is_empty():
action = self._history_of_actions.pop()
if isinstance(action, CutOutFragment):
self._song = action.song
if isinstance(action, Reverse):
self._reversed = not self._reversed
if self._changes[action.__class__.__name__]:
self._changes[action.__class__.__name__].pop()
self._history_of_actions_str.append("undo the last action")
if action.change_slider():
return action
return None
def update_original_song(self) -> AudioSegment:
"""Обновляет оригинальный аудиофайл с учетом всех изменений.
Возвращает:
AudioSegment: Обновленный аудиофайл.
"""
result_song = self._song
for change in self._changes:
if len(self._changes[change]) >= 1:
result_song = self._changes[change][-1].execute(result_song)
if self._reversed and result_song:
result_song = result_song.reverse()
return result_song
class TypeException(Exception):
"""Исключение для неверного типа аудиофайла.
Аргументы:
message (str): Сообщение об ошибке.
"""
def __init__(self, message: str) -> None:
super().__init__(message)