-
Notifications
You must be signed in to change notification settings - Fork 0
/
audio_gen.py
executable file
·171 lines (144 loc) · 5.45 KB
/
audio_gen.py
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
#!/usr/bin/env python3
import pydub.exceptions
from pydub import AudioSegment
from pydub.effects import normalize
from pprint import pprint
import glob
import random
from dataclasses import dataclass
from itertools import zip_longest
from collections import Counter
from loguru import logger
import sys, os
import shutil
from config import MINUTE, SECOND, CONFIG, TIMINGS
from concurrent.futures import ProcessPoolExecutor
logger.remove()
logger.add(sys.stdout, colorize=True, format="<green>{elapsed}</green> <level>{message}</level>")
log = logger.info
def main(**kwargs):
audio_folder = CONFIG["audio_folder"]
out = f'{audio_folder}/_other/out/public.ogg'
generate_file(out_filepath=out, **kwargs)
def _process_audio_in_a_play(p):
p.sound.audio = process_audio(p.sound.audio)
p.sound.foi = True
return p
def generate_file(audio_folder=None, out_filepath=None, seed=None):
log('Start')
if audio_folder:
CONFIG["audio_folder"] = audio_folder
seed = seed or random.randint(100, 999)
log('Creating structure')
structure = make_structure()
log('Structure ready')
pprint(Counter(p.sound.type for p in structure))
log('Normalizing audio')
pool = ProcessPoolExecutor()
structure = list(pool.map(_process_audio_in_a_play, structure))
log('Compiling audio...')
audio = compile(structure)
if not out_filepath:
out_filepath = f'{CONFIG["audio_folder"]}/_other/out/out-{seed}.ogg'
log(f'Converting to ogg at {out_filepath}')
audio.export(out_filepath, 'ogg')#, parameters=['-ac', '1'])
log(f'Done. Wrote {os.path.getsize(out_filepath) // (1024):,}KB')
return out_filepath
# breakpoint()
def compile(structure):
CROSSFADE_TIME = 10
output = []
cursor = -1 * MINUTE
for play in structure:
output.append(AudioSegment.silent(play.begin - cursor + 2*CROSSFADE_TIME))
print(f'added {(play.begin - cursor) // 1000 } seconds of silence')
output.append(play.sound.audio)
print(play)
cursor = play.end
output[0] = output[0].overlay( # add sound in beggining
process_audio(AudioSegment.from_file(f'{CONFIG["audio_folder"]}/placeholder.ogg', 'ogg')))
def binary_join(a_list): # sequential append is slow on pydub, this binary join is 3x faster
if len(a_list) == 0: return AudioSegment.empty()
if len(a_list) == 1: return a_list[0]
half = len(a_list)//2
return binary_join(a_list[:half]).append(binary_join(a_list[half:]), crossfade=CROSSFADE_TIME)
return binary_join(output)
# def sequential_join(a_list):
# return functools.reduce(lambda x, y: x+y, a_list)
# return sequential_join(output)
def make_structure(seed=None):
random.seed(seed)
structure = []
for sound, times in TIMINGS.items():
structure += [Play(Sound(sound), end=timing.time) for timing in times]
structure = sorted(structure, key=lambda play: play.end)
conflicts = collect_conflicts(structure)
for conflict in conflicts:
last_begin = None
for i in conflict:
if last_begin is not None:
i.end = last_begin - CONFIG['interval_between_conflicts'] # play this before
last_begin = i.begin
return sorted(structure, key=lambda play: play.end)
def collect_conflicts(intervals):
from itertools import combinations
conflicts = [(a, b) for a, b in combinations(intervals, 2) if a.intersects(b)]
out = []
for a, b in conflicts:
for conflict_group in out:
if a in conflict_group or b in conflict_group:
conflict_group.append(a)
conflict_group.append(b)
break
else:
out.append([a,b])
return out
@dataclass
class SoundType:
TYPES = {}
@classmethod
def get_type(self, type_name):
type = self.TYPES.get(type_name) or SoundType(type_name)
self.TYPES[type_name] = type
return type
name: str
def __init__(self, name):
self.name = name
self.files = []
def shuffle_a_file(self):
if random.random() > CONFIG['random_humanizer_factor'] or not self.files:
self.files = glob.glob(f'{CONFIG["audio_folder"]}/{self.name}/*') or [f'{CONFIG["audio_folder"]}/placeholder.ogg']
file = random.choice(self.files)
self.files.remove(file)
return file
@dataclass
class Sound:
file: str
type: str
def __init__(self, type):
self.type = type
self.file = SoundType.get_type(type).shuffle_a_file()
try:
self.audio = AudioSegment.from_file(self.file, self.file.split('.')[-1])
except pydub.exceptions.CouldntDecodeError:
print(f'ERROR: FILE {self.file} not supported')
raise
def process_audio(audio):
return audio.set_channels(1).set_frame_rate(44100).set_sample_width(2).compress_dynamic_range().normalize()
@dataclass
class Play:
sound: Sound
end: int # ms
@property
def begin(s): return s.end - len(s.sound.audio)
def __repr__(self):
def time(ms):
s = ms // 1000
sign = 1 if s >= 0 else -1
addon = 0 if s >= 0 else 1
return f'{ (s//60)+addon :02.0f}:{ (sign*s)%60 :02.0f} - {ms}'
return f'Play(sound={self.sound!r}, times: {time(self.begin)} => {time(self.end)}'
def intersects(self, other):
return other.begin <= self.begin <= other.end or other.begin <= self.end <= other.end
if __name__ == '__main__':
main()