effects ordering changed
MODIFY
docs/tutorials/effects.py
docs/tutorials/effects.py
# extension: .py
# format_name: light
# format_version: '1.5'
-# jupytext_version: 1.5.1
+# jupytext_version: 1.5.0
# kernelspec:
# display_name: Python 3
# language: python
@@ -60,10 +60,11 @@ print(mix.signal_duration)
# + tags=[]
another_signal = mix.apply_effects()
print(another_signal.signal_duration)
+
print(new_signal.effects_applied)
# -
-# To clear out the current effects chain without applying effect, use `reset_effects_chain`.
+# To clear out the current effects chain without applying effects, use `reset_effects_chain`.
# It will not reverse effects already applied!
#
# If `apply_effects` is called with empty effects chain, then it returns itself.
@@ -80,12 +81,11 @@ mix.tremolo(5, .6).high_pass(12000)
mix.apply_effects(overwrite=True)
mix.effects_applied
-# FFmpeg effects will be applied AFTER SoX effects, irregardless of the order the hooks
-# are applied. The effects `time_stretch` and `pitch_shift` are SoX effects. All others
-# are FFmpeg effects. For example, the two statements will result in the same altered signal:
+# If `user_order=False`, FFmpeg effects will be applied AFTER SoX effects, irregardless of the order the hooks
+# are applied. One may want to disable user order for a significant speed up when applying multiple effects. The effects `time_stretch` and `pitch_shift` are SoX effects. All others are FFmpeg effects. For example, the two statements will result in the same altered signal:
-signal_1 = mix.pitch_shift(4).tremolo(5, .6).apply_effects()
-signal_2 = mix.tremolo(5, .6).pitch_shift(4).apply_effects()
+signal_1 = mix.pitch_shift(4).tremolo(5, .6).apply_effects(user_order=False)
+signal_2 = mix.tremolo(5, .6).pitch_shift(4).apply_effects(user_order=False)
signal_1.effects_applied == signal_2.effects_applied
# Now we will list the effects provided in nussl.
@@ -164,12 +164,12 @@ nussl.utils.visualize_spectrogram(high_pmix, y_axis="log")
# Applys tremolo/vibrato filter on the audio signal, with a modulation frequency of `mod_freq` Hz, and modulation amplitude of `mod_depth`.
# Tremolo
-trem_mix = mix.tremolo(5, .4).apply_effects()
+trem_mix = mix.tremolo(4, .4).apply_effects()
trem_mix.embed_audio()
nussl.utils.visualize_spectrogram(trem_mix, y_axis="log")
# Vibrato
-vib_mix = mix.vibrato(8, .7).apply_effects()
+vib_mix = mix.vibrato(4, .4).apply_effects()
vib_mix.embed_audio()
nussl.utils.visualize_spectrogram(vib_mix, y_axis="log")
@@ -208,7 +208,7 @@ nussl.utils.visualize_spectrogram(col_mix, y_axis="log")
## Apply two chorus filters
delays = [40, 60]
-decays = [.7, .2]
+decays = [.4, .2]
speeds = [.9, .8]
depths = [.8, .6]
in_gain = 1

MODIFY
nussl/core/audio_signal.py
nussl/core/audio_signal.py
@@ -201,8 +201,7 @@ class AudioSignal(object):
self.stft_params = stft_params
# Effects
- self._ffmpeg_effects_chain = []
- self._sox_effects_chain = []
+ self._effects_chain = []
self.effects_applied = []
def __str__(self):
@@ -1656,7 +1655,7 @@ class AudioSignal(object):
# Effect Hooks #
##################################
- def apply_effects(self, reset=True, overwrite=False):
+ def apply_effects(self, reset=True, overwrite=False, user_order=True):
"""
Effects are stored in an "effects chain", which refers to a queue of effects that will be
applied to an AudioSignal object when `apply_effects` is called. We can add an effect to
@@ -1706,12 +1705,15 @@ class AudioSignal(object):
>>> audio_signal.effects_applied
["tremolo", "highpass"]
- FFmpeg effects will be applied AFTER SoX effects, irregardless of the order the hooks
- are applied. The effects `time_stretch` and `pitch_shift` are SoX effects. All others
- are FFmpeg effects. For example, the two statements will result in the same altered signal:
+ If user_order is false, FFmpeg effects will be applied AFTER SoX effects, irregardless of
+ the order the hooks are applied. The effects `time_stretch` and `pitch_shift` are SoX effects.
+ All others are FFmpeg effects. This may be done for speed, as applying all FFmpeg effects without
+ interuption will be faster than being interrupted with a SoX effect.
- >>> signal_1 = audio_signal.pitch_shift(4).tremolo(5, .6).apply_effects()
- >>> signal_2 = audio_signal.tremolo(5, .6).pitch_shift(4).apply_effects()
+ For example, the two statements will result in the same altered signal:
+
+ >>> signal_1 = audio_signal.pitch_shift(4).tremolo(5, .6).apply_effects(user_order=False)
+ >>> signal_2 = audio_signal.tremolo(5, .6).pitch_shift(4).apply_effects(user_order=False)
>>> signal_1.effects_applied == signal_2.effects_applied
True
@@ -1720,16 +1722,18 @@ class AudioSignal(object):
Default=True
overwrite (bool): If True, overwrites existing audio_data in AudioSignal. Default=False
Also clears out stft_data.
+ user_order (bool): If True, applied effects in the user provided order. If False, applies SoX
+ effects first, then FFmpeg effects.
Returns:
self or new_signal (AudioSignal): If overwrite=True, then returns initially AudioSignal
with edited audio_data. Otherwise, returns a new AudioSignal new_signal.
"""
- new_signal = self
- if self._sox_effects_chain:
- new_signal = effects.apply_effects_sox(new_signal, self._sox_effects_chain)
- if self._ffmpeg_effects_chain:
- new_signal = effects.apply_effects_ffmpeg(new_signal, self._ffmpeg_effects_chain)
+
+ if user_order:
+ new_signal = self._apply_user_ordered_effects()
+ else:
+ new_signal = self._apply_sox_ffmpeg_ordered_effects()
if reset:
self.reset_effects_chain()
@@ -1741,6 +1745,40 @@ class AudioSignal(object):
return new_signal
+ def _apply_user_ordered_effects(self):
+ new_signal = self
+ next_chain = []
+ idx = 0
+ while idx < len(self._effects_chain) or next_chain:
+ if idx < len(self._effects_chain) and (not next_chain
+ or type(next_chain[-1]) == type(self._effects_chain[idx])):
+ next_chain.append(self._effects_chain[idx])
+ idx += 1
+ else:
+ if isinstance(next_chain[0], effects.SoXFilter):
+ new_signal = effects.apply_effects_sox(new_signal, next_chain)
+ elif isinstance(next_chain[0], effects.FFmpegFilter):
+ new_signal = effects.apply_effects_ffmpeg(new_signal, next_chain)
+ next_chain = []
+ return new_signal
+
+ def _apply_sox_ffmpeg_ordered_effects(self):
+ new_signal = self
+ sox_effects_chain = []
+ ffmpeg_effects_chain = []
+ for f in self._effects_chain:
+ if isinstance(f, effects.FFmpegFilter):
+ ffmpeg_effects_chain.append(f)
+ elif isinstance(f, effects.SoXFilter):
+ sox_effects_chain.append(f)
+
+ if sox_effects_chain:
+ new_signal = effects.apply_effects_sox(new_signal, sox_effects_chain)
+ if ffmpeg_effects_chain:
+ new_signal = effects.apply_effects_ffmpeg(new_signal, ffmpeg_effects_chain)
+
+ return new_signal
+
def make_effect(self, effect, **kwargs):
"""
Calls another effect hook. An example:
@@ -1773,8 +1811,7 @@ class AudioSignal(object):
This will not remove effects that have already been applied!
"""
- self._ffmpeg_effects_chain = []
- self._sox_effects_chain = []
+ self._effects_chain = []
return self
def time_stretch(self, factor):
@@ -1794,7 +1831,7 @@ class AudioSignal(object):
Returns:
self: Inital AudioSignal with updated effect chains
"""
- self._sox_effects_chain.append(effects.time_stretch(factor))
+ self._effects_chain.append(effects.time_stretch(factor))
return self
def pitch_shift(self, shift):
@@ -1814,7 +1851,7 @@ class AudioSignal(object):
Returns:
self: Inital AudioSignal with updated effect chains
"""
- self._sox_effects_chain.append(effects.pitch_shift(shift))
+ self._effects_chain.append(effects.pitch_shift(shift))
return self
def low_pass(self, freq, poles=2, width_type="h", width=.707):
@@ -1840,7 +1877,7 @@ class AudioSignal(object):
Returns:
self: Inital AudioSignal with updated effect chains
"""
- self._ffmpeg_effects_chain.append(effects.low_pass(freq, poles=poles,
+ self._effects_chain.append(effects.low_pass(freq, poles=poles,
width_type=width_type, width=width))
return self
@@ -1867,7 +1904,7 @@ class AudioSignal(object):
Returns:
self: Inital AudioSignal with updated effect chains
"""
- self._ffmpeg_effects_chain.append(effects.high_pass(freq, poles=poles,
+ self._effects_chain.append(effects.high_pass(freq, poles=poles,
width_type=width_type, width=width))
return self
@@ -1887,7 +1924,7 @@ class AudioSignal(object):
Returns:
self: Inital AudioSignal with updated effect chains
"""
- self._ffmpeg_effects_chain.append(effects.tremolo(mod_freq, mod_depth))
+ self._effects_chain.append(effects.tremolo(mod_freq, mod_depth))
return self
def vibrato(self, mod_freq, mod_depth):
@@ -1906,7 +1943,7 @@ class AudioSignal(object):
Returns:
self: Inital AudioSignal with updated effect chains
"""
- self._ffmpeg_effects_chain.append(effects.vibrato(mod_freq, mod_depth))
+ self._effects_chain.append(effects.vibrato(mod_freq, mod_depth))
return self
def chorus(self, delays, decays, speeds, depths, in_gain=.4, out_gain=.4):
@@ -1929,7 +1966,7 @@ class AudioSignal(object):
Returns:
self: Inital AudioSignal with updated effect chains
"""
- self._ffmpeg_effects_chain.append(effects.chorus(delays, decays,
+ self._effects_chain.append(effects.chorus(delays, decays,
speeds, depths, in_gain=.4, out_gain=.4))
return self
@@ -1956,7 +1993,7 @@ class AudioSignal(object):
Returns:
self: Inital AudioSignal with updated effect chains
"""
- self._ffmpeg_effects_chain.append(effects.phaser(in_gain=.4, out_gain=.74, delay=3,
+ self._effects_chain.append(effects.phaser(in_gain=.4, out_gain=.74, delay=3,
decay=.4, speed=.5, _type="triangular"))
return self
@@ -1984,7 +2021,7 @@ class AudioSignal(object):
Returns:
self: Inital AudioSignal with updated effect chains
"""
- self._ffmpeg_effects_chain.append(effects.flanger(delay=0, depth=2, regen=0, width=71,
+ self._effects_chain.append(effects.flanger(delay=0, depth=2, regen=0, width=71,
speed=.5, phase=25, shape="sinusoidal", interp="linear"))
return self
@@ -2023,7 +2060,7 @@ class AudioSignal(object):
Returns:
self: Inital AudioSignal with updated effect chains
"""
- self._ffmpeg_effects_chain.append(effects.emphasis(level_in,
+ self._effects_chain.append(effects.emphasis(level_in,
level_out, _type="col", mode='production'))
return self
@@ -2060,7 +2097,7 @@ class AudioSignal(object):
Returns:
self: Inital AudioSignal with updated effect chains
"""
- self._ffmpeg_effects_chain.append(effects.compressor(level_in, mode="downward", reduction_ratio=2,
+ self._effects_chain.append(effects.compressor(level_in, mode="downward", reduction_ratio=2,
attack=20, release=250, makeup=1, knee=2.8284, link="average",
detection="rms", mix=1, threshold=.125))
return self
@@ -2088,7 +2125,7 @@ class AudioSignal(object):
Returns:
self: Inital AudioSignal with updated effect chains
"""
- self._ffmpeg_effects_chain.append(effects.equalizer(bands))
+ self._effects_chain.append(effects.equalizer(bands))
return self

MODIFY
nussl/core/effects.py
nussl/core/effects.py
@@ -45,7 +45,7 @@ class FilterFunction:
def __str__(self):
params = ",".join(f"{p}={v}" for p, v in self.params.items())
- return f"{self.filter} with params {params}"
+ return f"{self.filter} (params: {params})"
def __call__(self, stream):
return self.func(stream)

MODIFY
tests/core/test_effects.py
tests/core/test_effects.py
@@ -364,18 +364,22 @@ def test_hooks(mix_and_sources, check_against_regression_data):
}])
)
- assert len(signal._ffmpeg_effects_chain) == 10
- assert len(signal._sox_effects_chain) == 2
+ assert len(signal._effects_chain) == 12
- augmented_signal = signal.apply_effects(reset=False)
+ augmented_signal = signal.apply_effects(reset=False, user_order=False)
+ assert len(signal._effects_chain) == 12
+ assert len(augmented_signal.effects_applied) == 12
+ assert len(augmented_signal._effects_chain) == 0
reg_path = path.join(REGRESSION_PATH, "hooks.json")
fx_regression(augmented_signal.audio_data, reg_path, check_against_regression_data)
- augmented_signal.time_stretch(.7).apply_effects(overwrite=True)
+ augmented_signal.time_stretch(.7).apply_effects(overwrite=True, user_order=False)
- assert len(augmented_signal._ffmpeg_effects_chain) == 0
- assert len(augmented_signal._sox_effects_chain) == 0
+ assert len(augmented_signal._effects_chain) == 0
+ assert len(augmented_signal.effects_applied) == 25
+
+ assert str(augmented_signal.effects_applied[-1]) == "time_stretch (params: factor=0.7)"
equal_signal = augmented_signal.apply_effects()
assert equal_signal == augmented_signal
@@ -406,7 +410,7 @@ def test_make_effect(mix_and_sources, check_against_regression_data):
}])
)
signal.apply_effects(reset=False)
- augmented_signal = signal.apply_effects(reset=False, overwrite=True)
+ augmented_signal = signal.apply_effects(reset=False, overwrite=True, user_order=False)
reg_path = path.join(REGRESSION_PATH, "hooks.json")
# This should result in the same signal in test_hooks
fx_regression(augmented_signal.audio_data, reg_path, check_against_regression_data)
@@ -414,3 +418,15 @@ def test_make_effect(mix_and_sources, check_against_regression_data):
with pytest.raises(AudioSignalException):
signal.make_effect("fail")
+
+def test_order(mix_and_sources):
+ mix, _ = mix_and_sources
+ mix.reset_effects_chain().time_stretch(1.5).tremolo(.3,.4).pitch_shift(5).vibrato(.3, .4)
+ ## User order
+ signal0 = mix.apply_effects(reset=False)
+
+ ## SoX-FFmpeg Order
+ signal1 = mix.apply_effects(reset=False, user_order=False)
+
+ assert signal0.effects_applied != signal1.effects_applied
+ assert len(signal0.effects_applied) == len(signal1.effects_applied)

effects ordering changed