Jacky2305 commited on
Commit
8e9b3a3
·
1 Parent(s): 9af9e2c

Switch to librosa pitch_shift + custom formant shift (no parselmouth dependency)

Browse files
Files changed (1) hide show
  1. app_simple.py +66 -27
app_simple.py CHANGED
@@ -1,21 +1,20 @@
1
  #!/usr/bin/env python3
2
- """Simple Elderly Voice Converter using pitch/formant shifting (librosa + parselmouth)."""
3
  import os
4
  import tempfile
5
  import gradio as gr
6
  import numpy as np
7
  import soundfile as sf
8
  import librosa
9
- import parselmouth
10
- from parselmouth.praat import call
11
 
12
  TITLE = "🎭 Elderly Voice Converter (Pitch + Formant Shift)"
13
  DESCRIPTION = """
14
  基于 **音调偏移 + 共振峰偏移** 的轻量级声音转换,模拟老年人声线特征。
15
 
16
  **原理:**
17
- - **降低基频 (Pitch)**:老年人声带松弛,基频更低
18
- - **降低共振峰**:声道变长/肌肉松弛,共振峰下移
19
  - **添加气声**:模拟声带闭合不全的气息感
20
 
21
  **使用方法:**
@@ -48,39 +47,78 @@ def convert_voice_simple(
48
  # Load audio
49
  y, sr = librosa.load(input_audio_path, sr=None, mono=True)
50
 
51
- # Convert to parselmouth Sound
52
- sound = parselmouth.Sound(y, sampling_frequency=sr)
53
-
54
- # 1. Pitch shift using PSOLA (Change pitch command)
55
  if pitch_shift != 0:
56
- pitch_ratio = 2 ** (pitch_shift / 12.0)
57
- # Change pitch: pitch_ratio, time_step, min_pitch, max_pitch
58
- sound = call(sound, "Change pitch", pitch_ratio, 0.01, 50, 500)
59
 
60
- # 2. Formant shift (Change formants command)
61
  if formant_shift != 0:
62
- formant_ratio = 2 ** (formant_shift / 12.0)
63
- # Change formants: formant_ratio, new_pitch_median
64
- sound = call(sound, "Change formants", formant_ratio, 1.0)
65
 
66
- # 3. Add breathiness (mix with noise)
67
  if breathiness > 0:
68
- noise = np.random.normal(0, breathiness * 0.1, len(sound.values[0]))
69
- sound.values[0] = sound.values[0] * (1 - breathiness) + noise * breathiness
70
-
71
- # Get result
72
- result_audio = sound.values[0].astype(np.float32)
73
- result_sr = sound.sampling_frequency
74
 
75
- # Resample if needed
76
- if result_sr != sr:
77
- result_audio = librosa.resample(result_audio, orig_sr=result_sr, target_sr=sr)
78
 
79
  # Save
80
- sf.write(output_audio_path, result_audio, sr, subtype='PCM_16')
81
  return output_audio_path
82
 
83
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
  def process_audio(
85
  audio_file,
86
  pitch_shift,
@@ -104,6 +142,7 @@ def process_audio(
104
 
105
  return output_path, "✅ 转换完成!"
106
  except Exception as e:
 
107
  return None, f"❌ 转换失败: {str(e)}"
108
 
109
 
 
1
  #!/usr/bin/env python3
2
+ """Simple Elderly Voice Converter using librosa for pitch shift + simple formant shift."""
3
  import os
4
  import tempfile
5
  import gradio as gr
6
  import numpy as np
7
  import soundfile as sf
8
  import librosa
9
+ import librosa.effects
 
10
 
11
  TITLE = "🎭 Elderly Voice Converter (Pitch + Formant Shift)"
12
  DESCRIPTION = """
13
  基于 **音调偏移 + 共振峰偏移** 的轻量级声音转换,模拟老年人声线特征。
14
 
15
  **原理:**
16
+ - **降低基频 (Pitch)**:使用 librosa PSOLA 算法降低音调
17
+ - **降低共振峰**:通过频谱包络偏移模拟声道变长
18
  - **添加气声**:模拟声带闭合不全的气息感
19
 
20
  **使用方法:**
 
47
  # Load audio
48
  y, sr = librosa.load(input_audio_path, sr=None, mono=True)
49
 
50
+ # 1. Pitch shift using librosa's PSOLA (reliable)
 
 
 
51
  if pitch_shift != 0:
52
+ y = librosa.effects.pitch_shift(y, sr=sr, n_steps=pitch_shift)
 
 
53
 
54
+ # 2. Formant shift via spectral envelope manipulation
55
  if formant_shift != 0:
56
+ y = shift_formants(y, sr, formant_shift)
 
 
57
 
58
+ # 3. Add breathiness
59
  if breathiness > 0:
60
+ noise = np.random.normal(0, breathiness * 0.1 * np.std(y), len(y))
61
+ y = y * (1 - breathiness) + noise * breathiness
 
 
 
 
62
 
63
+ # Normalize to prevent clipping
64
+ y = y / (np.max(np.abs(y)) + 1e-6) * 0.95
 
65
 
66
  # Save
67
+ sf.write(output_audio_path, y, sr, subtype='PCM_16')
68
  return output_audio_path
69
 
70
 
71
+ def shift_formants(y, sr, formant_shift_semitones):
72
+ """
73
+ Shift formants by manipulating spectral envelope.
74
+ Positive = higher formants, Negative = lower formants.
75
+ """
76
+ if formant_shift_semitones == 0:
77
+ return y
78
+
79
+ # Convert semitones to frequency ratio
80
+ ratio = 2 ** (formant_shift_semitones / 12.0)
81
+
82
+ # STFT
83
+ n_fft = 2048
84
+ hop_length = 512
85
+ stft = librosa.stft(y, n_fft=n_fft, hop_length=hop_length)
86
+ magnitude = np.abs(stft)
87
+ phase = np.angle(stft)
88
+
89
+ # Shift spectral envelope along frequency axis
90
+ # Simple approach: interpolate magnitude spectrum
91
+ n_freqs = magnitude.shape[0]
92
+ new_freqs = np.linspace(0, n_freqs - 1, int(n_freqs / ratio))
93
+
94
+ if ratio < 1: # Lower formants (stretch)
95
+ # Interpolate to expand
96
+ from scipy.interpolate import interp1d
97
+ interp_func = interp1d(np.arange(n_freqs), magnitude, axis=0, kind='linear', fill_value='extrapolate')
98
+ magnitude = interp_func(new_freqs)
99
+ # Truncate or pad to original size
100
+ if magnitude.shape[0] > n_freqs:
101
+ magnitude = magnitude[:n_freqs]
102
+ else:
103
+ pad = np.zeros((n_freqs - magnitude.shape[0], magnitude.shape[1]))
104
+ magnitude = np.vstack([magnitude, pad])
105
+ else: # Higher formants (compress)
106
+ from scipy.interpolate import interp1d
107
+ interp_func = interp1d(np.arange(n_freqs), magnitude, axis=0, kind='linear', fill_value='extrapolate')
108
+ magnitude = interp_func(new_freqs)
109
+ if magnitude.shape[0] < n_freqs:
110
+ pad = np.zeros((n_freqs - magnitude.shape[0], magnitude.shape[1]))
111
+ magnitude = np.vstack([magnitude, pad])
112
+ else:
113
+ magnitude = magnitude[:n_freqs]
114
+
115
+ # Reconstruct
116
+ stft_new = magnitude * np.exp(1j * phase)
117
+ y_new = librosa.istft(stft_new, hop_length=hop_length, length=len(y))
118
+
119
+ return y_new
120
+
121
+
122
  def process_audio(
123
  audio_file,
124
  pitch_shift,
 
142
 
143
  return output_path, "✅ 转换完成!"
144
  except Exception as e:
145
+ import traceback
146
  return None, f"❌ 转换失败: {str(e)}"
147
 
148