fft-channel-vocoder 1.0.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.
Files changed (24) hide show
  1. fft_channel_vocoder-1.0.0/LICENSE +21 -0
  2. fft_channel_vocoder-1.0.0/PKG-INFO +161 -0
  3. fft_channel_vocoder-1.0.0/README.md +146 -0
  4. fft_channel_vocoder-1.0.0/fft_channel_vocoder/__init__.py +6 -0
  5. fft_channel_vocoder-1.0.0/fft_channel_vocoder/__main__.py +6 -0
  6. fft_channel_vocoder-1.0.0/fft_channel_vocoder/buffers.py +70 -0
  7. fft_channel_vocoder-1.0.0/fft_channel_vocoder/clean_audio.py +157 -0
  8. fft_channel_vocoder-1.0.0/fft_channel_vocoder/clean_io.py +55 -0
  9. fft_channel_vocoder-1.0.0/fft_channel_vocoder/config.py +2 -0
  10. fft_channel_vocoder-1.0.0/fft_channel_vocoder/fft.py +191 -0
  11. fft_channel_vocoder-1.0.0/fft_channel_vocoder/main.py +133 -0
  12. fft_channel_vocoder-1.0.0/fft_channel_vocoder/midi_synth.py +155 -0
  13. fft_channel_vocoder-1.0.0/fft_channel_vocoder/noise_generators.py +175 -0
  14. fft_channel_vocoder-1.0.0/fft_channel_vocoder/pitch_corrector.py +236 -0
  15. fft_channel_vocoder-1.0.0/fft_channel_vocoder/scale_synth.py +118 -0
  16. fft_channel_vocoder-1.0.0/fft_channel_vocoder.egg-info/PKG-INFO +161 -0
  17. fft_channel_vocoder-1.0.0/fft_channel_vocoder.egg-info/SOURCES.txt +22 -0
  18. fft_channel_vocoder-1.0.0/fft_channel_vocoder.egg-info/dependency_links.txt +1 -0
  19. fft_channel_vocoder-1.0.0/fft_channel_vocoder.egg-info/entry_points.txt +2 -0
  20. fft_channel_vocoder-1.0.0/fft_channel_vocoder.egg-info/requires.txt +3 -0
  21. fft_channel_vocoder-1.0.0/fft_channel_vocoder.egg-info/top_level.txt +1 -0
  22. fft_channel_vocoder-1.0.0/pyproject.toml +18 -0
  23. fft_channel_vocoder-1.0.0/setup.cfg +4 -0
  24. fft_channel_vocoder-1.0.0/setup.py +20 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Lawrence Perez
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,161 @@
1
+ Metadata-Version: 2.4
2
+ Name: fft_channel_vocoder
3
+ Version: 1.0.0
4
+ Summary: FFT-based channel vocoder that applies spectral envelope transfer
5
+ Author: Your Name
6
+ Requires-Python: >=3.8
7
+ Description-Content-Type: text/markdown
8
+ License-File: LICENSE
9
+ Requires-Dist: numpy
10
+ Requires-Dist: scipy
11
+ Requires-Dist: mido
12
+ Dynamic: author
13
+ Dynamic: license-file
14
+ Dynamic: requires-python
15
+
16
+ # FFT Channel Vocoder
17
+
18
+ > **Built by a blind programmer and musician, for musicians**
19
+
20
+ A Python package that applies spectral envelope transfer using FFT-based processing. The vocoder takes a modulator (voice) signal and imposes its spectral envelope onto a carrier signal.
21
+
22
+ Designed with accessibility as a core principle: CLI first, no GUI, cross-platform, automated batch processingβ€”built by a blind developer for everyone who deserves equal access to audio tools.
23
+
24
+ ## Documentation
25
+
26
+ - πŸ“š **[Documentation](docs/index.md)** β€” Installation, tutorials, configuration, and troubleshooting
27
+ - 🎨 **[Design Philosophy](design/design.md)** β€” Why we built it this way, design decisions, and algorithm choices
28
+ - βš™οΈ **[Algorithm Deep Dive](design/algorithm.md)** β€” Technical details for audio engineers and researchers
29
+
30
+ ## Features
31
+
32
+ - **Spectral Envelope Transfer**: Extracts formant information from voice and applies it to carrier signals
33
+ - **Multiple Input Formats**: Supports voice files, MIDI files (synthesized to carrier waves), pre-generated synth wave files, and scale-based pitch correction
34
+ - **Pitch Correction**: Optional automatic pitch detection and correction to user-defined musical scales with noise gate
35
+ - **Batch Processing**: Automatically processes multiple input files with consistent naming patterns
36
+ - **Generator-based Design**: Uses Python generators for efficient iteration through numbered file sequences
37
+ - **Accessibility First**: CLI interface, fully accessible to screen readers, works across platforms
38
+
39
+ ## Installation
40
+
41
+ ### From source
42
+ ```bash
43
+ cd fft_channel_vocoder
44
+ pip install -e .
45
+ ```
46
+
47
+ ## Usage
48
+
49
+ ### Command Line
50
+
51
+ Run the vocoder via the `vocode` command:
52
+ ```bash
53
+ vocode
54
+ ```
55
+
56
+ Or using Python module syntax:
57
+ ```bash
58
+ python3 -m fft_channel_vocoder
59
+ ```
60
+
61
+ ### Input Structure
62
+
63
+ The vocoder expects files organized in an `input/` directory:
64
+
65
+ ```
66
+ input/
67
+ β”œβ”€β”€ voice1.wav # Modulator signals
68
+ β”œβ”€β”€ voice2.wav
69
+ β”œβ”€β”€ melody1.mid # MIDI files to synthesize as carrier
70
+ β”œβ”€β”€ melody2.mid
71
+ β”œβ”€β”€ synth1.wav # Pre-generated synth wave files as carrier
72
+ β”œβ”€β”€ synth2.wav
73
+ β”œβ”€β”€ synth3.wav
74
+ β”œβ”€β”€ scale1.txt # Scale files for pitch correction (one note per line)
75
+ └── scale2.txt
76
+ ```
77
+
78
+ **Scale File Format:**
79
+ Each scale file contains one note class per line. Supported note classes are: `c`, `c#`, `d`, `d#`, `e`, `f`, `f#`, `g`, `g#`, `a`, `a#`, `b`. Comments (lines starting with `#`) and blank lines are ignored.
80
+
81
+ Example `scale1.txt` (C Major scale):
82
+ ```
83
+ # Major scale
84
+ c
85
+ d
86
+ e
87
+ f
88
+ g
89
+ a
90
+ b
91
+ ```
92
+
93
+ ### Processing Flow
94
+
95
+ For each voice file, the vocoder:
96
+
97
+ 1. **MIDI Processing**: Synthesizes each MIDI file into a carrier wave and vocodes with the voice
98
+ 2. **Synth Wave Processing**: Loads each pre-generated synth wave file and vocodes with the voice
99
+ 3. **Pitch Correction**: Detects pitch from the voice and snaps to a user-defined scale, synthesizes a carrier wave, and vocodes with the voice
100
+ 4. **Whisper Generation**: Creates a stereo whisper track by vocoding the voice with white noise
101
+
102
+ **Pitch Correction Details:**
103
+ - Analyzes the voice for dominant frequencies in the range 50-2000 Hz
104
+ - Snaps detected pitches to a defined musical scale (octave-independent)
105
+ - Uses a noise gate (-40 dB by default) to prevent unwanted tuning during silence or low-amplitude content
106
+ - Maintains the last detected note when below the noise gate threshold
107
+
108
+ ### Output Structure
109
+
110
+ Processed files are saved to `output/`:
111
+
112
+ ```
113
+ output/
114
+ β”œβ”€β”€ voice1_melody1.wav # Voice + MIDI synthesis
115
+ β”œβ”€β”€ voice1_melody2.wav
116
+ β”œβ”€β”€ voice1_synth1.wav # Voice + Synth wave 1
117
+ β”œβ”€β”€ voice1_synth2.wav
118
+ β”œβ”€β”€ voice1_synth3.wav
119
+ β”œβ”€β”€ voice1_scale1.wav # Voice + Pitch-corrected carrier (scale 1)
120
+ β”œβ”€β”€ voice1_scale2.wav # Voice + Pitch-corrected carrier (scale 2)
121
+ └── voice1_whisper.wav # Stereo whisper track
122
+ ```
123
+
124
+ ## Configuration
125
+
126
+ Edit `fft_channel_vocoder/config.py` to adjust:
127
+
128
+ - `sample_rate`: Default 96,000 Hz
129
+ - `fft_size`: FFT window power (2^12 = 4096 samples)
130
+
131
+ ## Algorithm
132
+
133
+ The vocoder works in 4 steps:
134
+
135
+ 1. **STFT Analysis**: Compute Short-Time Fourier Transform for both voice and carrier
136
+ 2. **Spectral Smoothing**: Apply Gaussian blur to extract formant envelopes
137
+ 3. **Envelope Transfer**: Apply spectral whitening to the carrier, then scale by voice envelope
138
+ 4. **Reconstruction**: Inverse STFT with original carrier phase to recover time-domain signal
139
+
140
+ ## Demonstration
141
+
142
+ Hear the FFT Vocoder in action:
143
+
144
+ **[FFT Vocoder Demo Video](https://www.youtube.com/watch?v=-T7fRM0AHqs)** β€” Original voice, vocoded track, and vocoded track with music + code displayed on screen
145
+
146
+ ## Module Reference
147
+
148
+ - `main.py`: Core pipeline and file iteration
149
+ - `fft.py`: FFT vocoding algorithm
150
+ - `clean_io.py`: Audio file I/O with resampling
151
+ - `clean_audio.py`: Audio preprocessing and validation
152
+ - `config.py`: Global configuration parameters
153
+ - `midi_synth.py`: MIDI to audio synthesis
154
+ - `pitch_corrector.py`: Pitch detection and scale-based note snapping
155
+ - `scale_synth.py`: Pitch-corrected carrier synthesis
156
+ - `noise_generators.py`: Noise generation utilities
157
+ - `buffers.py`: Buffer management utilities
158
+
159
+ ## Disclaimer
160
+
161
+ This project was developed with AI-assisted development. While some parts of the code were built with AI assistance, the program ideas, architecture, and design philosophy are original.
@@ -0,0 +1,146 @@
1
+ # FFT Channel Vocoder
2
+
3
+ > **Built by a blind programmer and musician, for musicians**
4
+
5
+ A Python package that applies spectral envelope transfer using FFT-based processing. The vocoder takes a modulator (voice) signal and imposes its spectral envelope onto a carrier signal.
6
+
7
+ Designed with accessibility as a core principle: CLI first, no GUI, cross-platform, automated batch processingβ€”built by a blind developer for everyone who deserves equal access to audio tools.
8
+
9
+ ## Documentation
10
+
11
+ - πŸ“š **[Documentation](docs/index.md)** β€” Installation, tutorials, configuration, and troubleshooting
12
+ - 🎨 **[Design Philosophy](design/design.md)** β€” Why we built it this way, design decisions, and algorithm choices
13
+ - βš™οΈ **[Algorithm Deep Dive](design/algorithm.md)** β€” Technical details for audio engineers and researchers
14
+
15
+ ## Features
16
+
17
+ - **Spectral Envelope Transfer**: Extracts formant information from voice and applies it to carrier signals
18
+ - **Multiple Input Formats**: Supports voice files, MIDI files (synthesized to carrier waves), pre-generated synth wave files, and scale-based pitch correction
19
+ - **Pitch Correction**: Optional automatic pitch detection and correction to user-defined musical scales with noise gate
20
+ - **Batch Processing**: Automatically processes multiple input files with consistent naming patterns
21
+ - **Generator-based Design**: Uses Python generators for efficient iteration through numbered file sequences
22
+ - **Accessibility First**: CLI interface, fully accessible to screen readers, works across platforms
23
+
24
+ ## Installation
25
+
26
+ ### From source
27
+ ```bash
28
+ cd fft_channel_vocoder
29
+ pip install -e .
30
+ ```
31
+
32
+ ## Usage
33
+
34
+ ### Command Line
35
+
36
+ Run the vocoder via the `vocode` command:
37
+ ```bash
38
+ vocode
39
+ ```
40
+
41
+ Or using Python module syntax:
42
+ ```bash
43
+ python3 -m fft_channel_vocoder
44
+ ```
45
+
46
+ ### Input Structure
47
+
48
+ The vocoder expects files organized in an `input/` directory:
49
+
50
+ ```
51
+ input/
52
+ β”œβ”€β”€ voice1.wav # Modulator signals
53
+ β”œβ”€β”€ voice2.wav
54
+ β”œβ”€β”€ melody1.mid # MIDI files to synthesize as carrier
55
+ β”œβ”€β”€ melody2.mid
56
+ β”œβ”€β”€ synth1.wav # Pre-generated synth wave files as carrier
57
+ β”œβ”€β”€ synth2.wav
58
+ β”œβ”€β”€ synth3.wav
59
+ β”œβ”€β”€ scale1.txt # Scale files for pitch correction (one note per line)
60
+ └── scale2.txt
61
+ ```
62
+
63
+ **Scale File Format:**
64
+ Each scale file contains one note class per line. Supported note classes are: `c`, `c#`, `d`, `d#`, `e`, `f`, `f#`, `g`, `g#`, `a`, `a#`, `b`. Comments (lines starting with `#`) and blank lines are ignored.
65
+
66
+ Example `scale1.txt` (C Major scale):
67
+ ```
68
+ # Major scale
69
+ c
70
+ d
71
+ e
72
+ f
73
+ g
74
+ a
75
+ b
76
+ ```
77
+
78
+ ### Processing Flow
79
+
80
+ For each voice file, the vocoder:
81
+
82
+ 1. **MIDI Processing**: Synthesizes each MIDI file into a carrier wave and vocodes with the voice
83
+ 2. **Synth Wave Processing**: Loads each pre-generated synth wave file and vocodes with the voice
84
+ 3. **Pitch Correction**: Detects pitch from the voice and snaps to a user-defined scale, synthesizes a carrier wave, and vocodes with the voice
85
+ 4. **Whisper Generation**: Creates a stereo whisper track by vocoding the voice with white noise
86
+
87
+ **Pitch Correction Details:**
88
+ - Analyzes the voice for dominant frequencies in the range 50-2000 Hz
89
+ - Snaps detected pitches to a defined musical scale (octave-independent)
90
+ - Uses a noise gate (-40 dB by default) to prevent unwanted tuning during silence or low-amplitude content
91
+ - Maintains the last detected note when below the noise gate threshold
92
+
93
+ ### Output Structure
94
+
95
+ Processed files are saved to `output/`:
96
+
97
+ ```
98
+ output/
99
+ β”œβ”€β”€ voice1_melody1.wav # Voice + MIDI synthesis
100
+ β”œβ”€β”€ voice1_melody2.wav
101
+ β”œβ”€β”€ voice1_synth1.wav # Voice + Synth wave 1
102
+ β”œβ”€β”€ voice1_synth2.wav
103
+ β”œβ”€β”€ voice1_synth3.wav
104
+ β”œβ”€β”€ voice1_scale1.wav # Voice + Pitch-corrected carrier (scale 1)
105
+ β”œβ”€β”€ voice1_scale2.wav # Voice + Pitch-corrected carrier (scale 2)
106
+ └── voice1_whisper.wav # Stereo whisper track
107
+ ```
108
+
109
+ ## Configuration
110
+
111
+ Edit `fft_channel_vocoder/config.py` to adjust:
112
+
113
+ - `sample_rate`: Default 96,000 Hz
114
+ - `fft_size`: FFT window power (2^12 = 4096 samples)
115
+
116
+ ## Algorithm
117
+
118
+ The vocoder works in 4 steps:
119
+
120
+ 1. **STFT Analysis**: Compute Short-Time Fourier Transform for both voice and carrier
121
+ 2. **Spectral Smoothing**: Apply Gaussian blur to extract formant envelopes
122
+ 3. **Envelope Transfer**: Apply spectral whitening to the carrier, then scale by voice envelope
123
+ 4. **Reconstruction**: Inverse STFT with original carrier phase to recover time-domain signal
124
+
125
+ ## Demonstration
126
+
127
+ Hear the FFT Vocoder in action:
128
+
129
+ **[FFT Vocoder Demo Video](https://www.youtube.com/watch?v=-T7fRM0AHqs)** β€” Original voice, vocoded track, and vocoded track with music + code displayed on screen
130
+
131
+ ## Module Reference
132
+
133
+ - `main.py`: Core pipeline and file iteration
134
+ - `fft.py`: FFT vocoding algorithm
135
+ - `clean_io.py`: Audio file I/O with resampling
136
+ - `clean_audio.py`: Audio preprocessing and validation
137
+ - `config.py`: Global configuration parameters
138
+ - `midi_synth.py`: MIDI to audio synthesis
139
+ - `pitch_corrector.py`: Pitch detection and scale-based note snapping
140
+ - `scale_synth.py`: Pitch-corrected carrier synthesis
141
+ - `noise_generators.py`: Noise generation utilities
142
+ - `buffers.py`: Buffer management utilities
143
+
144
+ ## Disclaimer
145
+
146
+ This project was developed with AI-assisted development. While some parts of the code were built with AI assistance, the program ideas, architecture, and design philosophy are original.
@@ -0,0 +1,6 @@
1
+ """FFT Channel Vocoder - Apply spectral envelope transfer using FFT processing."""
2
+
3
+ from .main import main, files_exist, whisper
4
+
5
+ __version__ = "0.1.0"
6
+ __all__ = ["main", "files_exist", "whisper"]
@@ -0,0 +1,6 @@
1
+ """Entry point for the fft_channel_vocoder package."""
2
+
3
+ from .main import main
4
+
5
+ if __name__ == "__main__":
6
+ main()
@@ -0,0 +1,70 @@
1
+ # buffers.py
2
+
3
+ # holds a carrier buffer and synthesizer generators for MIDI and other purposes
4
+
5
+ import numpy as np
6
+ from .config import sample_rate
7
+
8
+
9
+ class Carrier_Buffer:
10
+ """Pre-allocated audio buffer for building a carrier wave from note events."""
11
+
12
+ def __init__(self, total_duration):
13
+ """Allocate a silent buffer sized for the full duration.
14
+
15
+ Args:
16
+ total_duration: Length of the audio in seconds.
17
+ """
18
+ self.total_samples = int(np.ceil(total_duration * sample_rate))
19
+ self.carrier = np.zeros(self.total_samples, dtype=np.float32)
20
+
21
+ def add_wave(self, start_seconds, end_seconds, frequency):
22
+ """Mix a wave into the buffer over a time range.
23
+
24
+ Uses white noise for frequency <= 0 (silent/rest regions) and a
25
+ sawtooth wave otherwise.
26
+
27
+ Args:
28
+ start_seconds: Start time of the note in seconds.
29
+ end_seconds: End time of the note in seconds.
30
+ frequency: Pitch in Hz. Pass 0 or negative to write white noise.
31
+ """
32
+ # Safety check:
33
+ # Check if start is less than duration, and
34
+ # end is capped to the length if end_seconds is longer than duration.
35
+ start = int(round(start_seconds * sample_rate))
36
+ if start > self.total_samples:
37
+ raise ValueError("Start must be before duration")
38
+ end = min(int(round(end_seconds * sample_rate)), self.total_samples)
39
+ n = end - start
40
+ if n <= 0:
41
+ return
42
+ if frequency <= 0:
43
+ # Add white noise
44
+ self.carrier[start:end] += self.white_noise(n)
45
+ else:
46
+ # Append a wave with the frequency
47
+ self.carrier[start:end] += self.sawtooth_wave(frequency, n)
48
+
49
+
50
+ def sawtooth_wave(self, frequency, num_samples):
51
+ """Generate a sawtooth wave at the given frequency.
52
+
53
+ Args:
54
+ frequency: Pitch of the wave in Hz.
55
+ num_samples: Number of samples to generate.
56
+
57
+ Returns:
58
+ 1D float32 numpy array of the sawtooth waveform in the range [-1, 1].
59
+ """
60
+ indices = np.arange(num_samples)
61
+ # Calculates phase from 0 to 1
62
+ phase = (frequency * indices / sample_rate) % 1.0
63
+ waveform = 2.0 * phase - 1.0
64
+ return waveform.astype(np.float32)
65
+
66
+
67
+ def white_noise(self, num_samples):
68
+ """Generate white noise."""
69
+ return np.random.uniform(-1.0, 1.0, num_samples).astype(np.float32)
70
+
@@ -0,0 +1,157 @@
1
+ # clean_audio.py
2
+
3
+ # Organized audio pypeline for Vocoder
4
+ # handles the heavy DSP work before applying FFT vocoder, when loading and saving files.
5
+
6
+ # Source of truth
7
+ from .config import sample_rate
8
+ # import other libraries
9
+ import numpy as np
10
+ from math import gcd
11
+ from scipy.signal import resample_poly
12
+
13
+
14
+ def make_stereo(left, right):
15
+ """
16
+ Validates two audio arrays and combines them into a stereo array.
17
+ Assumes audio_check(array, skip_mono_check) is already defined.
18
+ """
19
+ # Validate both arrays using your check function
20
+ # Ensuring skip_mono_check is False as requested
21
+ audio_check(left,skip_mono_check=False)
22
+ audio_check(right, skip_mono_check=False)
23
+
24
+ # Check if they are the same length before stacking
25
+ if left.shape != right.shape:
26
+ raise ValueError(f"Arrays must have the same shape. Got {a.shape} and {b.shape}")
27
+
28
+ # Stack arrays along a new axis to create (samples, 2)
29
+ # np.stack(..., axis=-1) is standard for interleaved stereo
30
+ stereo_audio = np.column_stack((left, right))
31
+
32
+ return stereo_audio
33
+
34
+
35
+
36
+ def resample(audio, current_sample_rate):
37
+ """Resample audio to the project sample rate using polyphase filtering.
38
+
39
+ Args:
40
+ audio: Numpy array of audio samples.
41
+ current_sample_rate: The sample rate of the input audio in Hz.
42
+
43
+ Returns:
44
+ Numpy array resampled to the project sample rate, or the original
45
+ array unchanged if it already matches.
46
+ """
47
+ if current_sample_rate == sample_rate:
48
+ return audio
49
+
50
+ greatest_common_divisor = gcd(current_sample_rate, sample_rate)
51
+ up = sample_rate // greatest_common_divisor
52
+ down = current_sample_rate // greatest_common_divisor
53
+ return resample_poly(audio, up, down)
54
+
55
+ def make_mono(audio):
56
+ """Convert a multi-channel audio array to mono by averaging channels.
57
+
58
+ Args:
59
+ audio: Numpy array of shape (samples,) or (samples, channels).
60
+
61
+ Returns:
62
+ 1D numpy array. Already-mono input is returned unchanged.
63
+ """
64
+ if len(audio.shape) > 1:
65
+ return np.mean(audio, axis=1)
66
+ else:
67
+ return audio
68
+
69
+ def convert_float32(audio):
70
+ """Convert an audio array to float32.
71
+
72
+ int16 input is scaled to the [-1.0, 1.0] range. Any other dtype is cast
73
+ directly to float32 without rescaling.
74
+
75
+ Args:
76
+ audio: Numpy array of audio samples.
77
+
78
+ Returns:
79
+ float32 numpy array.
80
+ """
81
+ if audio.dtype == np.int16:
82
+ return audio.astype(np.float32) / 32768.0
83
+ return audio.astype(np.float32)
84
+
85
+ def normalise(audio):
86
+ """Normalise audio so the peak absolute value is 1.0.
87
+
88
+ Args:
89
+ audio: Numpy array of audio samples.
90
+
91
+ Returns:
92
+ Numpy array scaled so the loudest sample has magnitude 1.0.
93
+ """
94
+ # We use a small epsilon to avoid division by zero
95
+ norm_factor = np.max(np.abs(audio)) + 1e-9
96
+ return audio / norm_factor
97
+
98
+ # Safety check - ensure audio is valid before processing
99
+
100
+ def audio_check(audio, skip_mono_check=True):
101
+ """Validate that audio is a non-empty numpy array.
102
+
103
+ Args:
104
+ audio: The value to validate.
105
+ skip_mono_check: When False, also assert the array is 1D (mono).
106
+
107
+ Raises:
108
+ TypeError: If audio is not a numpy array.
109
+ ValueError: If audio is not mono (when skip_mono_check is False) or is empty.
110
+ """
111
+ if not isinstance(audio, np.ndarray):
112
+ raise TypeError(f"voice must be np.ndarray, got {type(audio)}")
113
+
114
+ if not skip_mono_check and audio.ndim != 1:
115
+ raise ValueError("Audio must be mono (1D arrays)")
116
+
117
+ if len(audio) == 0:
118
+ raise ValueError("Empty audio passed to vocoder")
119
+
120
+ def clean(audio, current_sample_rate=None, skip_mono_conversion=False):
121
+ """Normalise and prepare an audio array for processing or saving.
122
+
123
+ Runs in order: validate β†’ resample β†’ make mono β†’ convert to float32 β†’
124
+ normalise. Resampling is skipped when current_sample_rate is None or
125
+ already matches the project rate.
126
+
127
+ Args:
128
+ audio: Numpy array of audio samples.
129
+ current_sample_rate: Sample rate of the input audio in Hz, or None to
130
+ skip resampling.
131
+ skip_mono_conversion: When True, skip the make_mono step (needed when
132
+ saving stereo output).
133
+
134
+ Returns:
135
+ Cleaned float32 numpy array ready for processing or writing to disk.
136
+ """
137
+ # safety checks
138
+ audio_check(audio)
139
+ # If the synth returned an empty list or array, stop here
140
+ if audio is None or len(audio) == 0:
141
+ print("Warning: Received empty audio buffer in clean(). Returning silent array.")
142
+ return np.zeros(1024, dtype=np.float32) # Return a small slice of silence
143
+
144
+ # Change the sample rate if file_sample_rate is supplied
145
+ print("Fixing audio.")
146
+ if current_sample_rate is not None and current_sample_rate != sample_rate:
147
+ new_audio = resample(audio, current_sample_rate) # Avoid forshadowing
148
+ else:
149
+ new_audio = audio
150
+ # Make the track Mono
151
+ if not skip_mono_conversion:
152
+ new_audio = make_mono(new_audio)
153
+ # Convert to float 32
154
+ new_audio = convert_float32(new_audio)
155
+ # Convert to Mono
156
+ new_audio = normalise(new_audio)
157
+ return new_audio
@@ -0,0 +1,55 @@
1
+ # clean_io.py
2
+
3
+ # responsible for opening and saving files and passing them as Numpy arrays, and cleaning up audio as soon as files are opened and saved.
4
+
5
+ from .config import sample_rate
6
+ from . import clean_audio
7
+ import numpy as np
8
+ from scipy.io import wavfile
9
+
10
+ def does_exist(filename):
11
+ """Check whether a file exists on disk.
12
+
13
+ Args:
14
+ filename: Path-like or string path to check.
15
+
16
+ Returns:
17
+ True if the file can be opened for reading, False otherwise.
18
+ """
19
+ try:
20
+ with open(filename, "r") as f:
21
+ return True
22
+ except:
23
+ return False
24
+
25
+ def load(filename):
26
+ """Load a WAV file and return a cleaned mono float32 array.
27
+
28
+ Resamples to the project sample rate if needed and runs the full
29
+ clean_audio pipeline.
30
+
31
+ Args:
32
+ filename: Path-like or string path to a WAV file.
33
+
34
+ Returns:
35
+ 1D float32 numpy array normalised to [-1.0, 1.0].
36
+ """
37
+ # Load the wav files
38
+ file_sample_rate, audio = wavfile.read(filename)
39
+
40
+ # Clean audio
41
+ return clean_audio.clean(audio, file_sample_rate)
42
+
43
+ def save(filename, audio):
44
+ """Clean and write audio to a WAV file at the project sample rate.
45
+
46
+ Args:
47
+ filename: Destination path for the WAV file.
48
+ audio: Numpy array of audio samples to save. May be mono or stereo.
49
+ """
50
+ # Cleanup
51
+ new_audio = clean_audio.clean(audio, skip_mono_conversion=True)
52
+
53
+ # Save to file
54
+ wavfile.write(filename, sample_rate, new_audio.astype(np.float32))
55
+
@@ -0,0 +1,2 @@
1
+ sample_rate = 96_000
2
+ fft_size = 12