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.
- fft_channel_vocoder-1.0.0/LICENSE +21 -0
- fft_channel_vocoder-1.0.0/PKG-INFO +161 -0
- fft_channel_vocoder-1.0.0/README.md +146 -0
- fft_channel_vocoder-1.0.0/fft_channel_vocoder/__init__.py +6 -0
- fft_channel_vocoder-1.0.0/fft_channel_vocoder/__main__.py +6 -0
- fft_channel_vocoder-1.0.0/fft_channel_vocoder/buffers.py +70 -0
- fft_channel_vocoder-1.0.0/fft_channel_vocoder/clean_audio.py +157 -0
- fft_channel_vocoder-1.0.0/fft_channel_vocoder/clean_io.py +55 -0
- fft_channel_vocoder-1.0.0/fft_channel_vocoder/config.py +2 -0
- fft_channel_vocoder-1.0.0/fft_channel_vocoder/fft.py +191 -0
- fft_channel_vocoder-1.0.0/fft_channel_vocoder/main.py +133 -0
- fft_channel_vocoder-1.0.0/fft_channel_vocoder/midi_synth.py +155 -0
- fft_channel_vocoder-1.0.0/fft_channel_vocoder/noise_generators.py +175 -0
- fft_channel_vocoder-1.0.0/fft_channel_vocoder/pitch_corrector.py +236 -0
- fft_channel_vocoder-1.0.0/fft_channel_vocoder/scale_synth.py +118 -0
- fft_channel_vocoder-1.0.0/fft_channel_vocoder.egg-info/PKG-INFO +161 -0
- fft_channel_vocoder-1.0.0/fft_channel_vocoder.egg-info/SOURCES.txt +22 -0
- fft_channel_vocoder-1.0.0/fft_channel_vocoder.egg-info/dependency_links.txt +1 -0
- fft_channel_vocoder-1.0.0/fft_channel_vocoder.egg-info/entry_points.txt +2 -0
- fft_channel_vocoder-1.0.0/fft_channel_vocoder.egg-info/requires.txt +3 -0
- fft_channel_vocoder-1.0.0/fft_channel_vocoder.egg-info/top_level.txt +1 -0
- fft_channel_vocoder-1.0.0/pyproject.toml +18 -0
- fft_channel_vocoder-1.0.0/setup.cfg +4 -0
- 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,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
|
+
|