forked from jkmaxwell/medusa_tools
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmedusa_wav_preprocessor.py
More file actions
executable file
·103 lines (81 loc) · 3.54 KB
/
Copy pathmedusa_wav_preprocessor.py
File metadata and controls
executable file
·103 lines (81 loc) · 3.54 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
#!/usr/bin/env python3
import os
import wave
import numpy as np
from scipy import signal
import soundfile as sf
import argparse
import shutil
from pathlib import Path
def ensure_mono(data, channels):
"""Convert stereo to mono by averaging channels if needed."""
if channels > 1:
return np.mean(data, axis=1)
return data
def resample_to_44100(data, original_sr):
"""Resample audio to 44.1kHz if needed."""
if original_sr != 44100:
samples = len(data)
new_samples = int(samples * 44100 / original_sr)
return signal.resample(data, new_samples)
return data
def normalize_audio(data):
"""Normalize audio to -1.0 to 1.0 range."""
return data / np.max(np.abs(data))
def find_zero_crossings(data):
"""Find zero crossing points in the audio."""
return np.where(np.diff(np.signbit(data)))[0]
def extract_single_cycle(data, sample_rate):
"""Extract a single cycle waveform starting at a zero crossing."""
# Target length for Medusa wavetables (based on original format)
target_length = 2048 # Adjust this based on actual Medusa requirements
# Find zero crossings
zero_crossings = find_zero_crossings(data)
if len(zero_crossings) < 2:
# If no clear cycles, just take the first chunk
return signal.resample(data[:target_length], target_length)
# Find the first complete cycle
for i in range(len(zero_crossings) - 1):
cycle_length = zero_crossings[i + 1] - zero_crossings[i]
if cycle_length > 20: # Minimum cycle length to avoid noise
cycle = data[zero_crossings[i]:zero_crossings[i + 1]]
# Resample to target length
return signal.resample(cycle, target_length)
# Fallback if no good cycle found
return signal.resample(data[:target_length], target_length)
def process_wav_file(input_path, output_path):
"""Process a WAV file to meet Medusa requirements."""
# Read audio file
data, sample_rate = sf.read(input_path)
# Convert to mono if stereo
data = ensure_mono(data, len(data.shape))
# Resample to 44.1kHz if needed
data = resample_to_44100(data, sample_rate)
# Normalize audio
data = normalize_audio(data)
# Extract single cycle
data = extract_single_cycle(data, 44100)
# Save as 16-bit WAV
sf.write(output_path, data, 44100, subtype='PCM_16')
def main():
parser = argparse.ArgumentParser(description='Process WAV files for Medusa wavetable format')
parser.add_argument('input_dir', help='Directory containing input WAV files')
parser.add_argument('output_dir', help='Directory for processed WAV files')
args = parser.parse_args()
# Create output directory
os.makedirs(args.output_dir, exist_ok=True)
# Process WAV files
input_files = sorted(Path(args.input_dir).glob('*.wav'))[:64] # Limit to 64 files
for i, input_file in enumerate(input_files):
output_file = Path(args.output_dir) / f'wavetable_{i:02d}.wav'
print(f'Processing {input_file.name} -> {output_file.name}')
process_wav_file(input_file, output_file)
# Create .id file (required for recompilation)
id_file = output_file.with_suffix('.id')
with open(id_file, 'wb') as f:
f.write(i.to_bytes(4, 'little'))
print(f'\nProcessed {len(input_files)} files')
print('Now you can use medusa_wavetable_tool.py to recompile the processed files:')
print(f'./medusa_wavetable_tool.py recompile {args.output_dir} --output new_wavetables.polyend')
if __name__ == '__main__':
main()