ALWOAL's picture
Upload folder using huggingface_hub
4a9b62b verified
Raw History Blame Contribute Delete
11.6 kB
# -*- coding: utf-8 -*-
# Portions of this code are based on ContentVec.
# The original source code can be found at:
# https://github.com/auspicious3000/contentvec/blob/d746688a32940f4bee410ed7c87ec9cf8ff04f74/contentvec/data/audio/audio_utils_1.py
#
# Copyright (c) 2025 Yiwei Guo
# Licensed under the MIT license.
import os
import numpy as np
from scipy.signal import sosfilt
from scipy import signal as sg
import sox
def make_lowshelf(g, fc, Q, fs=44100):
"""Generate filter coefficients for 2nd order Lowshelf filter.
This function follows the code from the JUCE DSP library
which can be found in `juce_IIRFilter.cpp`.
The design equations are based upon those found in the Cookbook
formulae for audio equalizer biquad filter coefficients
by Robert Bristow-Johnson.
https://www.w3.org/2011/audio/audio-eq-cookbook.html
Args:
g (float): Gain factor in dB.
fc (float): Cutoff frequency in Hz.
Q (float): Q factor.
fs (float): Sampling frequency in Hz.
Returns:
tuple: (b, a) filter coefficients
"""
# convert gain from dB to linear
g = np.power(10,(g/20))
# initial values
A = np.max([0.0, np.sqrt(g)])
aminus1 = A - 1
aplus1 = A + 1
omega = (2 * np.pi * np.max([fc, 2.0])) / fs
coso = np.cos(omega)
beta = np.sin(omega) * np.sqrt(A) / Q
aminus1TimesCoso = aminus1 * coso
# coefs calculation
b0 = A * (aplus1 - aminus1TimesCoso + beta)
b1 = A * 2 * (aminus1 - aplus1 * coso)
b2 = A * (aplus1 - aminus1TimesCoso - beta)
a0 = aplus1 + aminus1TimesCoso + beta
a1 = -2 * (aminus1 + aplus1 * coso)
a2 = aplus1 + aminus1TimesCoso - beta
# output coefs
#b = np.array([b0/a0, b1/a0, b2/a0])
#a = np.array([a0/a0, a1/a0, a2/a0])
return np.array([[b0/a0, b1/a0, b2/a0, 1.0, a1/a0, a2/a0]])
def make_highself(g, fc, Q, fs=44100):
"""Generate filter coefficients for 2nd order Highshelf filter.
This function follows the code from the JUCE DSP library
which can be found in `juce_IIRFilter.cpp`.
The design equations are based upon those found in the Cookbook
formulae for audio equalizer biquad filter coefficients
by Robert Bristow-Johnson.
https://www.w3.org/2011/audio/audio-eq-cookbook.html
Args:
g (float): Gain factor in dB.
fc (float): Cutoff frequency in Hz.
Q (float): Q factor.
fs (float): Sampling frequency in Hz.
Returns:
tuple: (b, a) filter coefficients
"""
# convert gain from dB to linear
g = np.power(10,(g/20))
# initial values
A = np.max([0.0, np.sqrt(g)])
aminus1 = A - 1
aplus1 = A + 1
omega = (2 * np.pi * np.max([fc, 2.0])) / fs
coso = np.cos(omega)
beta = np.sin(omega) * np.sqrt(A) / Q
aminus1TimesCoso = aminus1 * coso
# coefs calculation
b0 = A * (aplus1 + aminus1TimesCoso + beta)
b1 = A * -2 * (aminus1 + aplus1 * coso)
b2 = A * (aplus1 + aminus1TimesCoso - beta)
a0 = aplus1 - aminus1TimesCoso + beta
a1 = 2 * (aminus1 - aplus1 * coso)
a2 = aplus1 - aminus1TimesCoso - beta
# output coefs
#b = np.array([b0/a0, b1/a0, b2/a0])
#a = np.array([a0/a0, a1/a0, a2/a0])
return np.array([[b0/a0, b1/a0, b2/a0, 1.0, a1/a0, a2/a0]])
def make_peaking(g, fc, Q, fs=44100):
"""Generate filter coefficients for 2nd order Peaking EQ.
This function follows the code from the JUCE DSP library
which can be found in `juce_IIRFilter.cpp`.
The design equations are based upon those found in the Cookbook
formulae for audio equalizer biquad filter coefficients
by Robert Bristow-Johnson.
https://www.w3.org/2011/audio/audio-eq-cookbook.html
Args:
g (float): Gain factor in dB.
fc (float): Cutoff frequency in Hz.
Q (float): Q factor.
fs (float): Sampling frequency in Hz.
Returns:
tuple: (b, a) filter coefficients
"""
# convert gain from dB to linear
g = np.power(10,(g/20))
# initial values
A = np.max([0.0, np.sqrt(g)])
omega = (2 * np.pi * np.max([fc, 2.0])) / fs
alpha = np.sin(omega) / (Q * 2)
c2 = -2 * np.cos(omega)
alphaTimesA = alpha * A
alphaOverA = alpha / A
# coefs calculation
b0 = 1 + alphaTimesA
b1 = c2
b2 = 1 - alphaTimesA
a0 = 1 + alphaOverA
a1 = c2
a2 = 1 - alphaOverA
# output coefs
#b = np.array([b0/a0, b1/a0, b2/a0])
#a = np.array([a0/a0, a1/a0, a2/a0])
return np.array([[b0/a0, b1/a0, b2/a0, 1.0, a1/a0, a2/a0]])
def params2sos(G, Fc, Q, fs):
"""Convert 5 band EQ paramaters to 2nd order sections.
Takes a vector with shape (13,) of denormalized EQ parameters
and calculates filter coefficients for each of the 5 filters.
These coefficients (2nd order sections) are then stored into a
single (5,6) matrix. This matrix can be fed to `scipy.signal.sosfreqz()`
in order to determine the frequency response of the cascasd of
all five biquad filters.
Args:
x (float): Gain factor in dB.
fs (float): Sampling frequency in Hz.
Returns:
ndarray: filter coefficients for 5 band EQ stored in (5,6) matrix.
[[b1_0, b1_1, b1_2, a1_0, a1_1, a1_2], # lowshelf coefficients
[b2_0, b2_1, b2_2, a2_0, a2_1, a2_2], # first band coefficients
[b3_0, b3_1, b3_2, a3_0, a3_1, a3_2], # second band coefficients
[b4_0, b4_1, b4_2, a4_0, a4_1, a4_2], # third band coefficients
[b5_0, b5_1, b5_2, a5_0, a5_1, a5_2]] # highshelf coefficients
"""
# generate filter coefficients from eq params
c0 = make_lowshelf(G[0], Fc[0], Q[0], fs=fs)
c1 = make_peaking (G[1], Fc[1], Q[1], fs=fs)
c2 = make_peaking (G[2], Fc[2], Q[2], fs=fs)
c3 = make_peaking (G[3], Fc[3], Q[3], fs=fs)
c4 = make_peaking (G[4], Fc[4], Q[4], fs=fs)
c5 = make_peaking (G[5], Fc[5], Q[5], fs=fs)
c6 = make_peaking (G[6], Fc[6], Q[6], fs=fs)
c7 = make_peaking (G[7], Fc[7], Q[7], fs=fs)
c8 = make_peaking (G[8], Fc[8], Q[8], fs=fs)
c9 = make_highself(G[9], Fc[9], Q[9], fs=fs)
# stuff coefficients into second order sections structure
sos = np.concatenate([c0,c1,c2,c3,c4,c5,c6,c7,c8,c9], axis=0)
return sos
import parselmouth
def change_gender(x, fs, lo, hi, ratio_fs, ratio_ps, ratio_pr):
s = parselmouth.Sound(x, sampling_frequency=fs)
f0 = s.to_pitch_ac(pitch_floor=lo, pitch_ceiling=hi, time_step=0.8/lo)
f0_np = f0.selected_array['frequency']
f0_med = np.median(f0_np[f0_np!=0]).item()
ss = parselmouth.praat.call([s, f0], "Change gender", ratio_fs, f0_med*ratio_ps, ratio_pr, 1.0)
return ss.values.squeeze(0)
def change_gender_f0(x, fs, lo, hi, ratio_fs, new_f0_med, ratio_pr):
s = parselmouth.Sound(x, sampling_frequency=fs)
ss = parselmouth.praat.call(s, "Change gender", lo, hi, ratio_fs, new_f0_med, ratio_pr, 1.0)
return ss.values.squeeze(0)
def parse_range(range_str):
# (a,b)|(c,d)|p
terms = range_str.split("|")
assert len(terms)==3, range_str
prob = float(terms[-1])
ranges = [eval(terms[0]), eval(terms[1])]
return ranges, prob
class FormantF0Perturb:
def __init__(self, male_formant_range, female_formant_range, female_f0_range):
# male_formant_range: (0.9,0.95)|(1.05,1.3)|0.25
# first range, second range, prob of first range
self.Qmin = 2
self.Qmax = 5
self.male_formant_range, self.male_formant_prob = parse_range(male_formant_range)
self.female_formant_range, self.female_formant_prob = parse_range(female_formant_range)
self.female_f0_range, self.female_f0_prob = parse_range(female_f0_range)
def get_perturb_param(self, gender):
# formant shift and pitch shift.
if gender == "M":
if np.random.random()<=self.male_formant_prob:
ratio_fs = np.random.uniform(self.male_formant_range[0][0], self.male_formant_range[0][1])
else:
ratio_fs = np.random.uniform(self.male_formant_range[1][0], self.male_formant_range[1][1])
return 75, 250, ratio_fs, 1.0
else:
if np.random.random()<=self.female_formant_prob:
ratio_fs = np.random.uniform(self.female_formant_range[0][0], self.female_formant_range[0][1])
else:
ratio_fs = np.random.uniform(self.female_formant_range[1][0], self.female_formant_range[1][1])
if np.random.random()<=self.female_f0_prob:
ratio_ps = np.random.uniform(self.female_f0_range[0][0], self.female_f0_range[0][1])
else:
ratio_ps = np.random.uniform(self.female_f0_range[1][0], self.female_f0_range[1][1])
return 100, 400, ratio_fs, ratio_ps
def random_eq(self, wav, sr):
Fc = np.exp(np.linspace(np.log(60), np.log(7600), 10))
# rng = np.random.default_rng()
z = np.random.uniform(0, 1, size=(10,))
Q = self.Qmin * (self.Qmax / self.Qmin)**z
G = np.random.uniform(-12, 12, size=(10,))
sos = params2sos(G, Fc, Q, sr)
wav = sosfilt(sos, wav)
return wav
def __call__(self, wav, sr, gender):
# gender: "F" or "M"
lo, hi, ratio_fs, ratio_ps = self.get_perturb_param(gender)
# fs: formant shift. This controls timbre (higher -> more female). ps: pitch shift. This controls absolute pitch level
# We fix the pitch range parameter to 1. So that we don't change the pitch variations
ratio_pr = 1
ss = change_gender(wav, sr, lo, hi, ratio_fs, ratio_ps, ratio_pr)
return ss
class SpeedPerturb:
def __init__(self, male_range, female_range):
self.male_range, self.male_prob = parse_range(male_range)
self.female_range, self.female_prob = parse_range(female_range)
def __call__(self, wav, sr, gender):
if gender == "F":
ranges, prob = self.male_range, self.male_prob
else:
ranges, prob = self.female_range, self.female_prob
if np.random.random()<=prob:
speed_alpha = np.random.uniform(ranges[0][0], ranges[0][1])
else:
speed_alpha = np.random.uniform(ranges[1][0], ranges[1][1])
self.transformer = sox.Transformer()
self.transformer.set_globals(verbosity=1) # disable warnings.
self.transformer.speed(factor=speed_alpha)
self.transformer.tempo(factor=1/speed_alpha) # recover the original length. Only affects timbre and pitch.
wav_perturb = self.transformer.build_array(input_array=wav, sample_rate_in=sr)
return wav_perturb
class IdentityPerturb:
def __init__(self, ):
pass
def __call__(self,wav, sr, gender):
return wav
class Perturbs:
def __init__(self, formant_f0_perturb_params, speed_perturb_params, speed_perturb_prob):
self.speed_perturb_prob = speed_perturb_prob
if speed_perturb_prob!=0.0:
self.speed_perturber = SpeedPerturb(**speed_perturb_params)
else:
self.speed_perturber = IdentityPerturb()
if speed_perturb_prob!=1.0:
self.formant_perturber = FormantF0Perturb(**formant_f0_perturb_params)
else:
self.formant_perturber = IdentityPerturb()
def __call__(self, wav, sr, gender):
if np.random.random()<=self.speed_perturb_prob:
return self.speed_perturber(wav,sr,gender)
else:
return self.formant_perturber(wav,sr,gender)