## Soundgen 3.0.0
## Tips & demos on project's homepage: http://cogsci.se/soundgen.html
## Please cite as: Anikin, A. (2019). Soundgen: an open-source tool for synthesizing nonverbal vocalizations. Behavior Research Methods, 51(2), 778-792.
##
## Attaching package: 'soundgen'
## The following object is masked from 'package:stats':
##
## spectrum
The function soundgen is intended for the synthesis of
animal vocalizations, including human non-linguistic vocalizations like
sighs, moans, screams, etc. It can also create non-biological sounds
that require precise control over spectral and temporal modulations,
such as special sound effects in computer games or acoustic stimuli for
scientific experiments. Soundgen is NOT meant to be used for
text-to-speech conversion.
Soundgen uses a parametric algorithm, which means that sounds can be synthesized de novo, without requiring an audio recording as a template. The output is completely determined by the values of control parameters in a deterministic or stochastic fashion. Soundgen generates and filters two sources of excitation - sine waves and white noise - and mixes them.
The rest of this vignette will unpack this last statement and
demonstrate how soundgen can be used in practice. To simplify setting
the control parameters and visualizing the output, the soundgen package
includes an interactive Shiny app. To start the app, type
soundgen_app() from R or try it online at
cogsci.se/soundgen.html.
To generate sounds from the console, use the function
soundgen. Each section of the vignette focuses on a
particular aspect of sound generation, both describing the relevant
arguments of soundgen and explaining how they can be set in
the Shiny app. Note that some advanced features, notably vectorization
of several arguments, are not implemented in the app and are only
accessible from the console.
TIP: this vignette is a hands-on, non-technical tutorial focusing on how to use soundgen in order to synthesize new sounds. For a more rigorous and theoretical discussion, please refer to Anikin, A. (2019). Soundgen: an open-source tool for synthesizing nonverbal vocalizations. Behavior Research Methods, 51(2), 778-792.
There are several other R packages that offer sound synthesis,
notably tuneR, seewave, and
phonTools. Both seewave and tuneR
implement straightforward ways to synthesize pulses and square,
triangular, or sine waves as well as noise with adjustable (linear)
spectral slope. You can also create multiple harmonics with both
amplitude and frequency modulation using seewave::synth()
and seewave::synth2(). To add formants and thus create
different vowels, try phonTools::vowelsynth(). Basic tonal
synthesis and many acoustic manipulations can also be performed using
the open-source program Praat. If this is ample for your needs, you
might want to try these alternatives first.
So why bother with soundgen? First, it takes customization and flexibility of sound synthesis much further. You will appreciate this flexibility if your aim is to produce convincing biological sounds. And second, it’s a higher-level tool with dedicated subroutines for things like controlling the rolloff (relative energy of different harmonics), adding moving formants and antiformants, mixing harmonic and noise components, controlling voice changes over multiple syllables, adding stochasticity to imitate unpredictable voice changes common in biological sound production, and more. In other words, soundgen offers powerful control over low-level acoustic characteristics of synthesized sounds with the benefit of also offering transparent, meaningful high-level parameters intended for rapid and straightforward specification of whole bouts of vocalizing.
Because of this high-level control, you don’t really have to think about the math of sound synthesis in order to use soundgen (although if you do, that helps). This vignette also assumes that the reader has some training in phonetics or bioacoustics, particularly for sections on formants and subharmonics.
Feel free to skip this section if you are only interested in using soundgen, not in how it works under the hood.
Soundgen’s credo is to start with a few control parameters (e.g., the intonation contour, the amount of noise, the number of syllables and their duration, etc.) and to generate a corresponding audio stream, which will sound like a biological vocalization (a bark, a laugh, etc). The core algorithm for generating a single voiced segment implements the standard source-filter model (Fant, 1971). The voiced component is generated as a sum of sine waves, and aperiodic component as turbulent noise, and both components are then passed through a frequency filter simulating the effect of a vocal tract. This process can be conceptually divided into three stages:
Note that soundgen currently implements only sine wave synthesis of voiced fragments. This is different from modeling glottal cycles themselves, as in phonetic models and some popular text-to-speech engines (e.g. Klatt, 1980). Normally multiple glottal cycles are generated simultaneously, with no pauses in between them (no closed phase) and with a continuously changing f0. It is also possible to add a closed phase. In future versions of soundgen there may be an option to use a particular parametric model of the glottal cycle as excitation source as an alternative to generating a separate sine wave for each harmonic.
Some form of noise is synthesized in most sound generators. In soundgen noise is created in the frequency domain (i.e., as a spectrogram) and then converted into a time series via inverse FFT. Noise has its own noise spectrum - by default, flat up to a certain threshold, followed by user-specified linear or exponential rolloff (Johnson, 2011).
Having briefly looked at the fundamental principles of sound
generation, we proceed to control parameters. The aim of the following
presentation is to offer practical tips on using soundgen. For further
information on more fundamental principles of acoustics and sound
synthesis, you may find the vignettes in seewave very
helpful, or you can check out the book on sound synthesis in R by Jérôme
Sueur, the author of the seewave package. Some essential
references are also listed at the end of this vignette, especially those
sources that have inspired particular routines in soundgen.
To generate a sound, you can either type soundgen_app()
to open an interactive Shiny app or call soundgen() from R
console with manually specified parameters. The app offers nice
visualizations and is more user-friendly if you are not used to
programming, but note that it doesn’t support some advanced features
(e.g., vectorization of some control parameters). An object called
presets contains a collection of presets that demonstrate
some of the possibilities. More information is available on the
project’s homepage at
https://cogsci.se/soundgen.html.
Audio playback may fail, depending on your platform and installed
software. Soundgen has a helper function called playme()
that accepts both Wave objects and simple numeric vectors. Depending on
your system (Windows / MacOS / Linux), it attempts to find a suitable
default player. If soundgen(play = TRUE) plays out of the
box, great. If it throws an error or you don’t hear any sound, look for
a player that works on your system. You may need to change the default
player in options()$soundgen.player and/or install
additional software. See ?playme and the
seewave
vignette on sound input/output for an in-depth discussion of audio
playback in R. Sueur (2018, p. 100) recommends Windows Media Player on
Windows, AudioUnits for Mac OS, SoX for Linux (the player is called
“play”), or VLC on any platform. I recommend “play” or “aplay” (ALSA) on
Linux, “afplay” on Macs, and VLC on Windows.
To control playback behavior in this vignette if you want to run the
code locally, simply set the global variable playback to
the player that works on your machine, for ex.:
The basic workflow from R console is as follows:
s001 = soundgen(play = playback) # default sound: a short [a] by a male speaker
# 's001' is a numeric vector - the waveform. You can save it, play it, plot it, ...# names(presets) # speakers in the preset library
# names(presets$Chimpanzee) # presets per speaker
s002 = eval(parse(text = presets$Chimpanzee$Scream_conflict)) # a screaming chimp
# playme(s002)TIP To save the synthesized sound, either set
soundgen(..., saveAudio = TRUE), which saves the result as
soundgen.wav in the working
directory, or use a function likeseewave::savewav()ortuneR::writeWave()
The basic workflow in the Shiny app is as follows:
soundgen_app(). RStudio should
open it in the default web browser - Firefox and Chrome are known to
work.TIP The interactive app soundgen_app() gives you the
exact R code for calling soundgen(), which you can
copy-paste into your R environment and generate manually the same sound
as the one you have created in the app. If in doubt about the right
format for a particular argument, you can use the app first, copy-paste
the code into your R console, and modify it as needed. You can also
import an existing formula into the app, adjust the parameters in an
interactive environment, and then export it again. BUT: the app can only
use a single value for many parameters that are vectorized when called
from the command line (rolloff, jitterDep, etc.).
If you need to generate a single syllable without pauses, the only
temporal parameter you have to set is sylLen (“Syllable
length, ms” in the app). For a bout of several syllables, you have two
options:
nSyl (“Number of syllables” in the app). Noise is
then allowed to fill in the pauses (if noise is longer than the voiced
part), and you can specify an amplitude contour, intonation contour, and
formant transitions that will span the entire bout. For ex., if the
vowel sequence in a three-syllable bout is “uai”, the output will be
approximately “[u] – pause – [a] – pause – [i]”.s003 = soundgen(formants = 'uai', repeatBout = 1, nSyl = 3, play = playback)
# to replay without re-generating the sound, type "playme(s003)"repeatBout (“Repeat bout # times” in the app). This
is the same as calling soundgen repeatedly with the same
settings or clicking the Generate button in the app several times. If
temperature = 0 and there is no jitter/shimmer, you will get exactly the
same sound repeated each time, otherwise some variation will be
introduced. For the same “uai” example, the output will be “[uai] –
pause – [uai] – pause – [uai]”.Like most arguments to soundgen, sylLen and
pauseLen can also be vectors. For example, if you want to
synthesize 5 syllables of progressively shorter duration and separated
by increasingly longer pauses, you can write:
s005 = soundgen(nSyl = 5,
sylLen = c(300, 100), # linearly decreasing from 300 to 100 ms
pauseLen = c(50, 150), # increasing from 50 to 150 ms
plot = TRUE,
play = playback)For more complicated changes in the length of syllables or pauses,
you can include longer sequences of values. You can also interpolate
smooth contours from a few anchor points - for ex., using the function
interpolate (see “Intonation” for examples). The length of
your input vector doesn’t matter: it will be up- or downsampled
automatically. This also works with all other vectorized arguments to
soundgen (rolloff, jitterDep, vibratoFreq, etc). An extra argument
addSilence (ms) controls how much the entire synthesized
sound is padded with silence.
s006 = soundgen(
nSyl = 10,
sylLen = c(60, 200, 90, 50, 50), # quickly up to 200 and down to 50
pauseLen = c(50, 60, 80, 150), # increasingly long pauses
plot = TRUE,
play = playback
)As a special case, your values will be used without interpolation if you provide exactly as many as needed:
s007 = soundgen(
nSyl = 5,
sylLen = c(300, 100, 400, 50, 100), # 5 syllables, 5 values
pauseLen = c(50, 150, 50, 100), # 4 pauses, 4 values
plot = TRUE,
play = playback
)You can use both repeatBout and nSyl
simultaneously. The pause between bouts is equal to the length of the
first pause between syllables (i.e., 100 ms in the following
example):
s008 = soundgen(
repeatBout = 2,
nSyl = 3,
sylLen = c(300, 100),
pauseLen = c(100, 50),
plot = TRUE,
play = playback
)Note that all pauses between syllables have to be positive. A
negative pause (overlap) between bouts is allowed, but you have to
enforce it with invalidArgAction = "ignore":
s009 = soundgen(
repeatBout = 2,
sylLen = 300,
pauseLen = -50,
plot = TRUE,
play = playback,
invalidArgAction = 'ignore'
)## Warning in validatePars(p, gp, permittedValues, invalidArgAction):
## pauseLen should be between 0 and 1000; override with caution
Soundgen literally generates a sine wave corresponding to f0 and
several more sine waves corresponding to its harmonics. f0 can vary over
time. To specify its contour with as few parameters as possible, the
solution adopted in soundgen is to take one or more anchors as input and
draw a smooth curve through all anchors with interpolation, staying
between pitchFloor and pitchCeiling (Hz).
In the simplest case, all anchors are equidistant, dividing the sound into equal time steps. Anchors can then be specified as a numeric vector. For example:
# downward chirp
s011 = soundgen(pitch = 3000:2000, play = playback,
samplingRate = 44100, pitchSamplingRate = 44100)
# when f0 is high, increase samplingRate and pitchSamplingRate for better quality# 3rd quarter silent (only works with a single syllable!)
s013 = soundgen(pitch = c(150, 200, NA, 110),
sylLen = 700, play = playback) You can also use a mathematical formula to produce very precise pitch modulation, just check that the values are on the right scale. For example, sinusoidal pitch modulation can be created as follows:
anchors = (sin(1:70 / 3) * .25 + 1) * 350
plot(anchors, type = 'l', xlab = 'Time (points)', ylab = 'Pitch (Hz)')
s014 = soundgen(pitch = anchors, sylLen = 1000, play = playback)For more flexibility, anchors can also be specified at arbitrary,
irregularly spaced time points using the “anchor format” - a dataframe
or list with two columns: time (ms or 0 to 1) and
value (in the case of pitch, this is frequency in Hz). The
function that interpolates smooth contours of f0 and other parameters is
interpolate(). When you generate sounds,
soundgen() has an argument
smoothing(list = ...), where you can put the settings
passed on to interpolate(). Time can range from 0 to 1, or
it can be specified in ms – it makes no difference, since the produced
contour is rescaled to match syllable duration.
For example, say we want f0 first to increase sharply from 350 to 700
Hz and then to slowly return to baseline. Time anchors can then be
specified as c(0, .1, 1) (think of it as “start”, “10%”,
and “end” of the sound). A sound with this intonation can be generated
as follows:
s015 = soundgen(
sylLen = 900, play = playback,
pitch = list(time = c(0, .1, 1), # or (c(0, 90, 900)) - in ms
value = c(350, 700, 350)))Beware of smoothing! A curve interpolated from a few anchors is not uniquely defined, and the interpolation algorithm has a major effect on its shape. Compare:
anchors = data.frame(time = c(0, .1, 1), value = c(350, 700, 350))
par(mfrow = c(2, 2))
interpolate(anchors$time, anchors$value, xout = seq(0, 1, .01),
method = 'linear', plot = TRUE)
interpolate(anchors$time, anchors$value, xout = seq(0, 1, .01),
method = 'splineFC', plot = TRUE)
interpolate(anchors$time, anchors$value, xout = seq(0, 1, .01),
method = 'loess', span = 0.5, plot = TRUE)
interpolate(anchors$time, anchors$value, xout = seq(0, 1, .01),
method = 'loess', span = 1, plot = TRUE)
# likewise: soundgen(smoothing = list(interpol = 'approx')), etc.To get more complex and precisely controlled curves, add more
anchors. If you are not satisfied with the smooth curve generated by
soundgen() based on your anchors, you can interpolate
first, as shown above, and feed the verified output of
interpolate() to soundgen(). Or you can use
analyze(), pitch_app(), Praat, etc. to extract
the pitch contour of an existing recording and feed it to
soundgen().
TIP Many arguments to soundgen are vectorized, and most
vectorized arguments understand the “anchor format” you just encountered
above, namely something like
my_argument = list(time = ..., value = ...), where time can
be in ms or ~[0, 1]. See ?soundgen for a complete list of anchor-format
arguments and keep in mind two important special cases that use a
slightly different format: formants and noise
(see below). And remember to check that interpolation looks
reasonable!
The assumption behind specifying an entire contour with a few
discrete anchors is that the contour is smooth and continuous. However,
there may be special occasions when you do want a discontinuity such as
an instantaneous pitch jump. The default behavior is to make a jump if
two anchors are closer than one percent of the syllable length (as
specified with the default jumpThres = 0.01). To make a
pitch jump, you thus provide two values of f0 that are very close in
time, for example:
s016 = soundgen(sylLen = 800, plot = TRUE, play = playback,
pitch = list(time = c(0, .2, .201, .4, 1),
value = c(900, 1200, 1800, 2000, 1500)),
samplingRate = 22050)## pitchSamplingRate should be much higher than the highest pitch; resetting to 20000 Hz
To draw f0 contour in the Shiny app, use “Intonation / Intonation syllable” tab and click the intonation plot to add anchors. Soundgen then generates a smooth curve through these anchors. If you click the plot close to an existing anchor, the anchor moves to where you clicked; if you click far from any existing anchor, a new anchor is added. To remove an anchor, double-click it. To go back to a straight line, click the button labeled “Flatten pitch contour”. Exactly the same principles apply to all anchors in soundgen_app (pitch, amplitude, mouth opening, and noise). Note also that all contours are rescaled when the duration changes, with the single exception of negative time anchors for noise (i.e. the length of pre-syllable aspiration does not depend on syllable duration).
If the bout consists of several syllables (nSyl > 1),
you can also specify the overall intonation over several syllables using
pitchGlobal (app: “Intonation / Intonation global”). The
global intonation contour specifies the deviation of pitch per syllable
from the main pitch contour in semitones, i.e. 12 semitones = 1 octave.
In other words, it shows how much higher or lower the average pitch of
each syllable is compared to the base pitch contour. For ex., we can
generate five seagull-like sounds, which have the same intonation
contour within each syllable, but which vary in average pitch spanning
about an octave in an inverted U-shaped curve. Note that the number of
anchors need not equal the number of syllables:
s017 = soundgen(nSyl = 5, sylLen = 200, pauseLen = 140,
plot = TRUE, play = playback,
pitch = list(time = c(0, 0.65, 1),
value = c(977, 1540, 826)),
pitchGlobal = list(time = c(0, .5, 1),
value = c(-6, 7, 0)))TIP Calling soundgen with argument
plot = TRUE produces a spectrogram using another function
from soundgen package, spectrogram. Type
?spectrogram and see the dedicated vignette for plotting
tips and advanced options. You can also plot the waveform produced by
soundgen using any other function,
e.g. seewave::spectro
Vibrato adds sinusoidal frequency modulation (FM) to f0 contour. In
contrast to irregular jitter and temperature-related random drift, this
FM is regular, although it may vary stochastically if
temperature > 0:
s018 = soundgen(vibratoDep = 0:3, # semitones
vibratoFreq = 7:5, # Hz
sylLen = 2000, pitch = c(300, 280),
play = playback, plot = TRUE)# stochastic vibrato (different every time)
s019 = soundgen(vibratoDep = rnorm(n = 10, mean = .5, sd = .1),
vibratoFreq = rnorm(n = 10, mean = 5, sd = .5),
sylLen = 2000, pitch = c(300, 280),
play = playback, plot = TRUE)Random variation is a cornerstone of synthesis with soundgen. The
overall amount of stochasticity is controlled by a single high-level
parameter, temperature (app: “Main / Hypers”). If
temperature = 0 and there is no jitter/shimmer, you will
get exactly the same sound by executing the same call to
soundgen repeatedly. If temperature > 0,
each generated sound will be somewhat different, even if all the control
parameters are exactly the same (unless you use
set.seed()). In particular, there is random variation in
syllable structure, all contours (intonation, breathing, amplitude,
mouth opening), many voice effects (jitter, subharmonics, etc),
user-specified formants, etc.
Code example:
# the sound is a bit different each time, because temperature is above zero
s020 = soundgen(repeatBout = 5, temperature = 0.3, play = playback)
# Setting repeatBout = 5 is equivalent to:
# for (i in 1:5) soundgen(temperature = 0.3, play = playback)If you don’t want stochastic behavior, set temperature to zero. But
note that some effects, notably jitter and subharmonics, will then be
added in an all-or-nothing manner: either to the entire sound or not at
all. Also note that additional formants will not be created above the
user-specified ones if temperature is exactly 0. In practice it may be
better to set temperature to a very small positive value like 0.01 or
1e-6. You can also change the extent to which temperature affects
different parameters (e.g., if you want more variation in intonation and
less variation in syllable structure). To do so, use
tempEffects, which is a list of scaling coefficients that
determine how much different parameters vary at a given temperature.
tempEffects includes the following scaling
coefficients:
amplDep: random fluctuations of user-specified
amplitude anchors across syllables (if nSyl > 1)amplDriftDep: drift of amplitude mirroring pitch
driftformDisp: irregularity of the dispersion of stochastic
formants that are added above user-specified formants (if any) at
distances consistent with the specified length of the vocal tract
vocalTractformDrift: random drift of formantsglottisDep: proportion of glottal cycle with closed
glottisnoiseDep: random fluctuations of user-specified noise
anchors across syllables (if nSyl > 1)pitchDep: random fluctuations of user-specified pitch
anchors across syllables (if nSyl > 1)pitchDriftDep: amount of slow random drift of f0 (the
higher, the more f0 changes)pitchDriftFreq: frequency of slow random drift of f0
(the higher, the faster f0 changes)rolloffDriftDep: drift of rolloff mirroring pitch
driftspecDep: random fluctuations of rolloff, nonlinear
effects, attacksubDriftDep: drift of subharmonic frequency and
bandwidth mirroring pitch driftsylLenDep: random fluctuations of the duration of
syllables and pauses between syllablesThe default value of each scaling parameter is 1. To enhance a particular component of stochastic behavior, set the corresponding coefficient to a value >1; to disable stochastic variation of a particular group of parameters completely, set its scaling coefficient to zero.
To simplify usage, there are a few other hyper-parameters. They are redundant in the sense that they are not strictly necessary to produce the full range of sounds, but they offer convenient shortcuts by making it possible to control several low-level parameters at once in a coordinated manner. Hyper-parameters are marked “hyper” in the Shiny app.
For example, to imitate the effect of varying body size, you can use
maleFemale. Since formants are not specified
in this example, but temperature is above zero, a schwa-like sound with
approximately equidistant formants is generated using
vocalTract (cm) to calculate the expected formant
dispersion:
s022 = soundgen(
maleFemale = -1, # male: 50% lower f0 (-1 octave), 20% lower formants, 25% longer vocal tract
formants = NA, pitch = 220, vocalTract = 15, play = playback)s023 = soundgen(
maleFemale = 0, # neutral (default)
formants = NA, pitch = 220, vocalTract = 15, play = playback)s024 = soundgen(
maleFemale = 1, # female: 100% higher f0 (+1 octave), 25% higher formants, 25% shorter vocal tract
formants = NA, pitch = 220, vocalTract = 15, play = playback)To change the basic voice quality along the breathy-creaky continuum,
use creakyBreathy. It affects the rolloff of harmonics, the
type and strength of pitch effects (jitter, subharmonics), the amount of
aspiration noise, and formant bandwidths. For example:
Use ampl and amplGlobal to modulate the
amplitude (loudness) of an individual syllable or a polysyllabic bout,
respectively. In the app, they are found under “Amplitude / Amplitude
syllable” and “Amplitude / Amplitude global”. Note that
ampl (dB) affects only the voiced component, while
amplGlobal, attackLen (“Attack length, ms” in
the app), and amDep (“Amplitude / Amplitude modulation / AM
depth” in the app) affect both the voiced and the aperiodic (noise)
components. Avoid attackLen = 0, since that can cause
clicks.
# each syllable has a 10-dB dip in the middle (note the dumbbell shapes
# in the oscillogram under the spectrogram), and there is an overall fade-out
# over the entire bout
s026 = soundgen(
nSyl = 4,
ampl = list(time = c(0, .3, 1), # unequal time steps
value = c(0, -10, 0)), # dB, 0 = max possible ampl
amplGlobal = c(0, -20), # this fade-out applies to noise as well
noise = -20,
plot = TRUE, heights = c(1, 1), play = playback)The dynamic amplitude range is determined by
dynamicRange. This parameter sets the minimum level of
loudness, below which components are discarded as essentially silence.
For maximum sound quality, set a high dynamicRange, like 120 dB. This
helps to avoid artifacts like audibly clicking harmonics, but it also
slows down sound generation. The default is 80 dB.
Rapid amplitude modulation imitating a trill is implemented by
multiplying the synthesized waveform by a wave with adjustable
amType (“sine” or “logistic”), shape amShape
(logistic only), frequency amFreq, and amplitude
amDep:
s027 = soundgen(
sylLen = 1000, formants = NA,
# set the depth of AM (0% = none, 100% = max)
amDep = c(0, 100),
# set AM frequency in Hz (vectorized)
amFreq = c(50, 25),
# set the shape: 0 = close to sine, -1 = notches, +1 = clicks
amShape = 0,
# asymmetrical attack: 20 ms at the beginning and 140 ms at the end
attackLen = c(20, 140),
plot = TRUE, heights = c(1, 1), ylim = c(0, 1), windowLength = 100,
play = playback)Note the difference between sinusoidal and non-sinusoidal AM. If the modulating signal is a pure tone (sine wave), it creates a single pair of extra harmonics at ± amFreq (the frequency of AM) around each harmonic of f0. Non-sinusoidal AM, in contrast, creates sidebands at ± {g0 x integer} around each harmonic of f0. For a given depth of AM, the more its shape deviates from a sine wave, the more pronounced the sidebands. Compare:
s027a = soundgen(pitch = 900, amFreq = 100, amDep = 50, amType = 'sine',
plot = TRUE, ylim = c(0, 4), heights = c(1, 1))s027b = soundgen(pitch = 900, amFreq = 100, amDep = 50, amShape = .25,
plot = TRUE, ylim = c(0, 4), heights = c(1, 1))A common special case of modifying the amplitude envelope of a
synthesized or recorded sound is compression, which helps to make the
amplitude remain relatively stable throughout the duration of the
signal. There is a separate function for achieving this, namely
compressor() AKA flatEnv():
s = rnorm(500) * seq(1, 0, length.out = 500)
s1 = compressor(s, samplingRate = 1000, plot = TRUE,
killDC = TRUE, windowLength_points = 50)## Warning in plot.xy(xy.coords(x, y), type = type, ...): "windowLength_points" is
## not a graphical parameter
## Warning in plot.xy(xy.coords(x, y), type = type, ...): "windowLength_points" is
## not a graphical parameter
Another common modification is to fade the sound in and/or out. One
way to do this is to change the attack (which affects both the beginning
and the end) or to use amplitude anchors. On other occasions, or if your
sound already exists and you want to change it, the way to go about it
is to use a separate function, fade(). This also gives you
more options, e.g. different attack shapes, while soundgen() defaults to
linear fade-in/out for attack.
# Create a sound with sharp attack
s028 = soundgen(sylLen = 300, pitch = 800, addSilence = 0, attackLen = 10)
osc(s028)## samplingRate not specified; defaulting to 16000
s029 = fade(s028, fadeIn = 50, fadeOut = 100, samplingRate = 16000,
shape = 'logistic', steepness = 1, plot = TRUE)
# playme(s029)
# different fades are available: linear, logarithmic, etcTIP: attackLen in soundgen is applied to both periodic and aperiodic components before filtering (i.e., before formants are added). In case of artifacts increase attackLen or apply fade() after synthesizing the sound.
Argument formants (tab “Tract / Formants” in the app)
sets the formants – frequency bands used to filter the excitation
source. Just as an equalizer in a sound player amplifies some
frequencies and dampens others, appropriate filters can be applied to a
tonal sound to make it resemble a human voice saying different vowels.
Formants are created in the frequency domain using all-pole models if
all formant amplitudes are positive and pole-zero models if there are
anti-formants with negative amplitudes (Stevens, 2000, ch. 3).
Using presets for callers M1 and F1, you can directly specify a
string of vowels. When you call soundgen with
formants = 'aouuuui' or some such character string, the
values are always taken from presets$M1$Formants when
soundgen() is called from the console. In
soundgen_app(), presets$F1$Formants can also
be used if the speaker is “F1”. Formants can remain the same throughout
the vocalizations, or they can move. For example,
formants = 'ai' produces a sound that goes smoothly from
[a] to [i], while formants = 'aaai' produces mostly [a]
with a rapid transition to [i] at the very end. Argument
formantDep (“Formant prominence” in the app) adjusts the
overall effect of all formant filters at once, and
formantWidth scales all bandwidths.
Presets give you some rudimentary control over vowels. More subtle control is necessary for animal sounds, as well as for human vowels that are not included in the presets dictionary or for non-default speakers. For such cases you will have to specify at least the frequency of each formant (and optionally, also amplitude, bandwidth, and time stamps for each value). The easiest, and normally sufficient, approach is to specify frequencies only and have soundgen() figure out the appropriate amplitude and bandwidth for each formant. Bandwidth is calculated from frequency using a formula derived from human phonetic research. Namely, above 500 Hz it follows the original formula known as “TMF-1963” (Tappert, Martony, and Fant, 1963), and below 500 Hz it applies a correction to allow for energy losses at low frequencies (Khodai-Joopari & Clermont, 2002). Below 250 Hz the bandwidth starts to decrease again, in a purely empirical attempt to achieve reasonable values even for formant frequencies below ordinary human range. See the internal function soundgen:::getBandwidth() if you are interested and note that for anything but ordinary human voices it may be safer to specify formant bandwidths manually.
freqs = 2 ^ seq(log2(20), log2(20000), length.out = 500)
plot(freqs, soundgen:::getBandwidth(freqs), type = 'l',
log = 'xy', xlab = 'Center frequency, Hz',
ylab = 'Bandwidth, Hz',
main = 'Default formant bandwidths')
abline(v = 250, lty = 3)
abline(v = 500, lty = 3)Formant amplitudes are normally assumed to be determined by their frequency and bandwidth (see Stevens, 2000), but you can override this by specifying amplitudes explicitly. Note that changing the amplitude (or frequency, or bandwidth) of one formant affects the entire spectrum above it.
For moving formants, provide multiple values, which assumes equal time steps, or specify time points explicitly, where time varies from 0 to 1 (to be scaled appropriately depending on the length of sound). For example:
# shorthand specification with three stationary formants
formants = c(300, 2500, 3200)
# shorthand specification with two moving formants
formants = list(f1 = c(300, 900), f2 = c(2500, 1500))
# full specification with two moving formants and non-default amplitude and bandwidth
formants = list(
f1 = list(freq = c(300, 900),
amp = c(30, 10),
width = 120),
f2 = list(time = c(0, .2, 1), # "time" is only needed for non-equidistant anchors
freq = c(2500, 2400, 1500),
amp = 30,
width = c(150, 220, 240)))Feed these lists into soundgen() to hear what they sound like.
In addition to user-specified formants, higher formants are added
automatically based on the vocal tract length estimated from the
user-specified formant frequency values. The function that estimates
vocal tract length is imaginatively called estimateVTL:
## [1] 15.73333
A more general function called schwa() both estimates
VTL and allows you to compare measured formant frequencies with those
expected for a neutral schwa sound and perform more sophisticated
operations with formants. See ?schwa for more details if
you are working with vowels.
## $vtl_apparent
## [1] 16.08047
##
## $dF
## [1] 1100.714
##
## $ff_measured
## [1] 820 1320 2550 4100
##
## $ff_schwa
## [1] 550.3571 1651.0714 2751.7857 3852.5000
##
## $ff_relative
## [1] 48.994160 -20.051914 -7.332901 6.424400
##
## $ff_relative_semitones
## [1] 6.903069 -3.874375 -1.318451 1.077947
##
## $ff_relative_dF
## [1] 0.2449708 -0.3007787 -0.1833225 0.2248540
It is usually useful to allow soundgen to create upper formants
automatically based on the estimated VTL, since upper formants are
typically less perceptually salient. As a result, we don’t want to spend
too much time specifying them manually, but still we don’t want them to
be completely absent, either, since that makes for very weak high
frequencies in the spectrum. An approximation based on standard
open-closed tube models is often good enough for calculating the missing
formant frequencies. You can remove them by setting
temperature = 0 or formantDepStoch = 0, but
note that without higher formants the entire spectrum loses energy at
higher frequencies:
Another useful method is to specify vocal tract length without any formants. In this case soundgen() approximates a neutral schwa sound for an animal with a vocal tract that looks like a uniform tube of this length. Crude, but often sufficient. A toy example (check presets for some more realistic sounds created using this method):
s034 = soundgen(
sylLen = 800, formants = NULL, rolloff = -6,
vocalTract = c(12, 18, 19), formantCeiling = 5,
play = playback, plot = TRUE)The contribution of formants above the Nyquist frequency, which are
not present in discrete digital audio, is estimated automatically as a
high-pass filter. For more precision or when modeling unusual vocal
tracts, you can increase formantCeiling to ~5-10 (i.e., 5
times the Nyquist frequency), which means that above-Nyqist formants are
modeled explicitly.
Vocal tract length can be variable, providing an easy way to create
parallel formant transitions. Unlike the argument “mouth”, variable
“vocalTract” parameter recalculates formant bandwidths (unless these are
specified manually) and is thus more accurate, but it requires you to
specify a reasonable vocal tract length, in cm. If you provide both a
few formant frequencies and a variable vocalTract, formants
are synthesized as specified at the initial value of
vocalTract (that is, at the very beginning of the sound)
and can deviate afterwards as VTL changes. Careful with such
combinations: make sure the initial value of the VTL is
reasonable for these formant frequencies (any VTL is fine towards the
end of the sound - the user-specified formants will move accordingly).
Examples:
formants = list(f1 = c(500, 250), f2 = c(1500, 2800), f3 = 3500, f4 = 4300)
estimateVTL(formants) # 13.6 cm - so the initial VTL should be close to 13.6## [1] 13.58204
s035 = soundgen(
sylLen = 800, rolloff = -6, formants = formants,
vocalTract = list(time = c(0, .3, 1), value = c(14, 28, 25)),
play = playback, plot = TRUE, main = 'Good initial VTL')# wrong: VTL too high for these formants, causing an overlap
s036 = soundgen(
sylLen = 800, rolloff = -6, formants = formants,
vocalTract = list(time = c(0, .3, 1), value = c(18, 28, 25)),
play = playback, plot = TRUE, main = 'Bad initial VTL')Sometimes it may be useful to view moving formants before
synthesizing the sound, or you may want to create a formant filter and
then apply it to another sound with transplantFormants().
The way to create and view a formant filter is to call the function that
handles all formant processing under the hood in soundgen, namely
getFormantFilter:
# plotting directly from getFormantFilter() in spectrogram form
s = getFormantFilter(
nr = 1024, # freq bins in FFT frame (wl %/% 2 + 1)
nc = 50, # time bins
samplingRate = 16000,
formants = list(f1 = c(500, 250),
f2 = c(1500, 2800),
f3 = 3500, f4 = 4300),
plot = TRUE,
dur = 1500, # just an example
colorTheme = 'seewave',
lipRad = 6) # lip radiation, dB/octaveNote that, in addition to formants, lip and nose radiation are also handled by this function (see the next section on mouth opening). This has the effect of boosting high frequencies.
TIP When using the app, you can start with a formant preset by typing in a vowel string, and then you can modify it. This way you don’t have to remember the right format. If you edit the list of formants and nothing in the sound seems to be changing, there may be a misprint, missing comma, etc.
For even more advanced spectral filters, you can specify both formants (poles) and antiformants (zeros). This may be useful if you want to create a nasalized sound. The numbering of formants is arbitrary, as long as they are arranged in the right order. For example, if you want to insert a new formant between F1 and F2 without renaming all higher formants, call it “f1.5” or something like that. It is important to use a non-integer number, since otherwise these additional formants will be inappropriately used to estimate the length of vocal tract and to add stochastic formants above the ones you specify (that is, if temperature > 0 and vocalTract = NA).
For example, a slow transition from [a] to [a nasalized] might be coded as follows (note that formant f1.7 has negative amplitude, so f1.5 and f1.7 form a pole-zero pair):
formants = list(
f1 = list(time = c(0, 1), freq = c(880, 900),
amp = c(25, 15), width = c(80, 120)),
f1.5 = list(time = c(0, 1), freq = 600,
amp = c(0, 15), width = 80), # additional pole
f1.7 = list(time=c(0, 1), freq = 750,
amp = c(0, -15), width = 80), # zero
f2 = list(time = c(0, 1), freq = c(1480, 1250),
amp = c(30, 20), width = c(120, 200)),
f3 = list(time=c(0, 1), freq = c(2900, 3100),
amp = 25, width = 200))
s037 = soundgen(sylLen = 1500, play = playback, pitch = 140, formants = formants)
spectrogram(s037, samplingRate = 16000, ylim = c(0, 4), contrast = .5,
windowLength = 10, step = 5, colorTheme = 'seewave')If you look at the filter towards the end of the sound, you can observe the additional zero-pole pair between the first and second formant:
se = getFormantFilter(nr = 512, nc = 100, formants = formants)
plot(as.numeric(rownames(se)), 20 * log10(se[, ncol(se)]),
type = 'l', xlab = 'KHz', ylab = 'dB')In addition to variable vocal tract length (vocalTract
argument), an even easier shortcut for creating parallel formant
transitions without coding all transitions by hand is provided by the
mouth argument (in the app, tab “Tract / Mouth opening”).
This can be thought of as a hyper-parameter offering an easy way to
define moving formants within a bout (easy because you don’t need to
know the VTL in cm): all formants simply go down relative to specified
values as the mouth closes and rise as it opens (see Moore, 2016).
In addition, an open mouth has lip radiation, which has the effect of
amplifying higher frequencies. Lip radiation is replaced by nose
radiation when the mouth is completely closed, dampening the higher
frequencies, and the vowel is automatically nasalized using a simple
approximation (Hawkins & Stevens, 1985). Basically, with the mouth
closed we switch from a tube open at one end to a tube closed at both
ends and coupled with a (simplified) nasal cavity. Despite being a crude
model of what really happens when a vocalizing animal closes its mouth,
in many cases mouth can save you a lot of manual coding of
formants. Here is a simple example, with the mouth gradually opening and
closing again:
s038 = soundgen(sylLen = 1200, play = playback, pitch = 140,
mouth = list(time = c(0, .3, .75, 1),
value = c(0, 0, .7, 0)))
# mouth = 0 --> closed; 0.5 = neutral (default, no effect); 1 = fully open
spectrogram(s038, samplingRate = 16000,
ylim = c(0, 4), contrast = .5,
windowLength = 10, step = 5,
colorTheme = 'seewave')TIP Here and elsewhere, I talk about applying soundgen to the
task of synthesizing non-human sounds. It does work, but be aware that
many computational routines are based on human phonetic research, simply
because there is vastly more data available on human vocal production.
For example, formant bandwidths and spectral consequences of
nasalization are estimated based on human phonetics, but it is far from
clear to what extent these equations are applicable to sounds produced
by non-human mammals. Bird calls are again a whole new ball game. And
once you move on to insects or non-biological sounds, just forget about
hyperparameters like mouth and code everything at the
lowest possible level.
The standard source-filter model (Fant, 1971) assumes that the
vibration of the vocal folds is independent of the configuration of the
supraglottal vocal tract - that is, that the source and filter are
independent. However, in some situations this assumption does not hold,
and the filter can have a noticeable effect on the vibration of the
vocal folds. An example of such source-filter interaction that is
implemented in soundgen is formant locking, in which the fundamental
frequency or a higher harmonic becomes temporarily “locked” to the
frequency of a formant. The relevant parameter is
formantLocking (0 = none, 1 = the entire sound, vector form
also accepted). See the internal function
soundgen:::lockToFormants() for more information and
examples.
In humans, spontaneous formant locking is usually observed in high-pitched sounds like screams, although formant matching can also be performed intentionally in order to maximize sonority, as in soprano singing, or to produce unusual vocal effects such as Tuvinian throat singing. In animal vocalizations, formant locking appears to be particularly common when the vocal tract is long and formants are closely spaced (my speculation). For example, elk bugles often contain a series of stepwise pitch jumps from one formant to the next, a bit like this:
s039 = soundgen(
sylLen = 1500, rolloff = -20,
pitch = c(500, 1000, 1800, 1700, 500),
formants = NULL, vocalTract = 55,
formantLocking = c(0, 1, 1, 1, 1, 0), # except the beginning and end
shortestEpoch = 200, # affects both subharmonics (if any) and formantLocking
noise = -20, # just to make formants visible in this example
temperature = .1,
samplingRate = 22000, pitchSamplingRate = 22000,
# samplingRate and pitchSamplingRate must be high enough relative to f0
# (reset internally to be >=10 * max(pitch))
play = playback, plot = TRUE, ylim = c(0, 8)
)For some purposes it may be useful to separate the generation of
glottal source (or another source of acoustic excitation) from its
spectral filtering. You may also need to add formants to an existing
waveform. To do so, you can call the helper function
addFormants(), which normally works under the hood in
soundgen. The algorithm is to take an STFT, multiply the resulting
spectrum by the filter, and then convert it back to time domain via
inverse STFT. The same function can theoretically be used to perform
inverse filtering - that is, to remove formants from a signal - as long
as you can provide a VERY accurate formant filter. See
?addFormants for more information.
TIP Bewildered by all these formants, antiformants, VTL, etc?
Good news: you can simply lift formants off a real recording and
“transplant” them onto your synthetic sound. See
?transplantFormants for more information and
examples.
Soundgen produces tonal sounds by means of generating a separate sine
wave for each harmonic. However, it is very tricky to choose the
appropriate strength of each harmonic. The simplest solution is to make
each higher harmonic slightly weaker than the previous one, say by
setting a fixed exponential decay rate from lower to higher harmonics.
The corresponding parameter in soundgen is rolloff (in the
app, “Source rolloff, dB/octave”). Unfortunately, this is often not
really good enough, necessitating several more control parameters.
Soundgen allows a lot of flexibility when specifying source spectrum.
You can change the basic rolloff of harmonics per octave, producing a
sharper or more gentle decline of energy over frequencies
(rolloffOct), or adjust rolloff depending on f0, so that
high-pitch sounds will have a steeper rolloff (rolloffKHz).
The relevant function is getRolloff. Its arguments are
well-documented: type ?getRolloff for help. Here is just a
single example:
# strong F0, rolloff with a "shoulder"
r = getRolloff(rolloff = c(-5, -20), # rolloff parameters are vectorized
pitch_per_gc = c(170, 340), plot = TRUE)
# to generate the corresponding sound:
s040 = soundgen(sylLen = 1000, rolloff = c(-5, -20),
pitch = c(170, 340), play = playback)In the app the relevant parameters are found in the tab “Source /
Rolloff”. To develop an intuition for source spectrum settings, I
recommend practicing with disabled formants in the app (set “Formants
prominence” under “Tract / Formants” to 0). This way you can isolate the
effects of source spectrum and use the preview plot for instant feedback
– it shows the rolloff for the lowest and the highest pitch in your
intonation contour. Rolloff parameters are vectorized, but this
functionality is only available from R console. However, rolloff also
varies over time if temperature is above zero (use
tempEffects$specDep to control the amount of stochastic
variation of rolloff and other spectral characteristics).
Apart from using rolloff and related parameters to
control the general shape of excitation spectrum, it is also possible to
control each harmonic individually. Say, you have performed inverse
filtering on a recording to estimate glottal source, giving you the
strength of each harmonic in glottal pulses, and now you wish to hear
the sound corresponding to this glottal source. To do so, use the
rolloffExact argument and supply a matrix of numeric values
on a scale of 0 to 1. Each row is one harmonic, columns are equidistant
time points, and the sound will contain only as many partials as there
are rows in rolloffExact:
rolloffExact = matrix(c(.1, .2, 1, .02, .2, # strength of H1-H5 at time 0
1, .2, .01, .1, .4), # strength of H1-H5 at time 1000
ncol = 2)
s041 = soundgen(sylLen = 1000, pitch = c(400, 430), formants = NULL,
rolloffExact = rolloffExact,
plot = TRUE, ylim = c(0, 4), play = playback)If you do not want source spectrum to change over time, a single vector instead of a matrix will do:
s042 = soundgen(sylLen = 1000, pitch = c(400, 430), formants = NULL,
rolloffExact = c(.1, .5, .25, 1, .25, .08, .05, .02),
plot = TRUE, ylim = c(0, 4), play = playback)If f0 is very low, as in vocal fry or some animal vocalizations like
crocodile roaring or elephant rumbling, individual glottal pulses can be
both seen on a spectrogram and perceived as distinct percussion-like
acoustic events separated by noticeable pauses. Soundgen can create such
sounds by adding pauses between “fundamental periods” (the synthesis is
actually continuous in time, but it’s the same principle as having a
closed phase in each glottal cycle). This is mostly justified for very
low-pitched sounds, since with higher pitch there will be too few points
per glottal cycle to sound convincing without increasing
samplingRate to astronomical values. Also note that some
spectral artifacts may appear. Example:
# Not a good idea: samplingRate is too low
s043 = soundgen(pitch = c(1500, 800),
glottis = 75, # % of open phase, values >100% are allowed
samplingRate = 16000, play = playback)# This sounds better but takes a long time to synthesize:
s044 = suppressWarnings(soundgen(
pitch = c(1500, 800), glottis = 75,
samplingRate = 80000, play = playback,
invalidArgAction = 'ignore'))
# NB: invalidArgAction = 'ignore' forces a "weird" samplingRate value
# to be accepted without question# Now this is what this feature is meant for: vocal fry
s045 = soundgen(
sylLen = 1500, pitch = c(70, 10), rolloff = -12,
glottis = c(0, 500),
jitterDep = 1, shimmerDep = 20,
# subharmonics not implemented with "glottis"
play = playback)
spectrogram(s045, samplingRate = 16000, heights = c(1, 1))Soundgen can add frequency jumps, subharmonics, sidebands
(implemented as a modification of subharmonics or as amplitude
modulation), and approximate deterministic chaos by adding strong jitter
and shimmer. These effects basically make the sound appear noisy / harsh
/ rough / unpredictable / etc. Jitter and shimmer are created by adding
random noise to the periods and amplitudes, respectively, of the
“glottal cycles”. Subharmonics could be created by adding rapid
amplitude and/or frequency modulation, but for maximum flexibility
soundgen uses a different - slightly hacky, but powerful - technique of
literally setting up an additional sine wave for each subharmonic. The
main limitation of this approach is that it is too computationally
costly to generate variable numbers of subharmonics for the entire bout.
The solution currently adopted in soundgen is to break longer sounds
into so-called “epochs” with a constant number of subharmonics in each.
The epochs are synthesized separately and then glued together with a
rapid crossFade(). This is suboptimal, since it shortens
the sound and may introduce artifacts at transitions between epochs.
shortestEpoch controls the approximate minimum length of
each epoch. Longer epochs minimize problems with transitions, but the
behavior of subharmonics then becomes less variable since their number
is constrained to be constant within each epoch. NB: with short
shortestEpoch, add ~20-30 ms per transition to the nominal
sound duration in order to compensate for cross-fading the epochs.
To add nonlinear effects stochastically, you can use
nonlinBalance, which regulates approximately what
proportion of the sound is affected. At temperature > 0,
nonlinBalance creates a random walk that divides each
syllable into epochs defined by their regime, using two thresholds to
determine when a new regime begins (see Fitch et al., 2002):
Regime 1: no nonlinear effects. If nonlinBalance = 0%, the whole syllable is in regime 1.
Regime 2: subharmonics only.
Regime 3: subharmonics, jitter, and shimmer. If nonlinBalance = 100%, the whole syllable is in regime 3.
To see any effect, you have to set jitterDep, shimmerDep, and subRatio/subDep to some positive values. With nonlinBalance < 100%, the result is a stochastic combination of the three regimes (tonal, subharmonics, subharmonics + jitter + shimmer):
s046 = soundgen(
sylLen = 1500, pitch = c(170, 420, 400, 190),
nonlinBalance = 60,
subDep = 10, jitterDep = 1.5, shimmerDep = 25,
play = playback, plot = TRUE, ylim = c(0, 5))To add nonlinearities non-stochastically (exactly where you want
them), keep nonlinBalance at the default value of 100% and
specify the nature and timing of nonlinearities manually. To add a
single subharmonic between each pair of f-harmonics (period doubling),
set subRatio = 2, for period tripling,
subRatio = 3, etc. This number of subharmonics will be
added regardless of pitch changes. The amplitude (loudness) of
subharmonics is controlled by subDep (“Depth of
subharmonics”). Both parameters can vary within a syllable:
s047 = soundgen(subRatio = c(2, 3), subDep = c(5, 20),
sylLen = 800, pitch = c(700, 1300), formants = NULL,
play = playback, plot = TRUE, ylim = c(0, 3))Sidebands can be created by adding rapid frequency modulation or amplitude modulation (AM) - see Anikin & Herbst, 2025. Perfectly sinusoidal AM creates a simple pair of extra harmonics, while non-sinusoidal AM creates sidebands:
s050 = soundgen(
sylLen = 800,
pitch = list(time=c(0, .3, .9, 1),
value = c(1200, 1547, 1487, 1154)),
rolloff = -3, rolloffKHz = 0,
# gradually increasing width of sidebands at 0-600 ms
amFreq = 75, amShape = .1,
amDep = list(time = c(0, 600, 650, 800),
value = c(0, 100, 0, 0)),
vocalTract = 12, mouth = c(.1, .8, .1),
temperature = .001,
pitchSamplingRate = 22050, samplingRate = 22050,
play = playback, plot = TRUE, ylim = c(0, 5))TIP The parameters regulating nonlinear effects are vectorized,
so you can write subDep = c(0, 130), jitterDep = c(0, 1),
etc., or use the “anchor format” as above (console only, not available
in the app)
As for jitter in regime 3, it wiggles both f0 and g0 harmonic stacks,
blurring the spectrum. Parameter jitterDep (“Jitter depth,
semitones” in the app) defines how much the pitch fluctuates, while
jitterLen (“Jitter period, ms”) defines how rapid these
fluctuations are. Slow jitter with a period of ~50 ms produces the
effect of a shaky, unsteady voice. It may sound similar to a vibrato,
but jitter is irregular. Rapid jitter with a period of ~1 ms, especially
in combination with subharmonics, may be used to imitate deterministic
chaos, which is found in voiced but highly irregular animal sounds such
as barks, roars, noisy screams, etc. This works best for high-pitched
sounds like screams. Shimmer is similar to jitter, except that it
defines random fluctuations of the amplitude rather than frequency. It
is controlled by two arguments, shimmerDep (percent) and
shimmerLen (ms).
s051 = soundgen(jitterLen = 40, jitterDep = 1, # shaky voice
shimmerLen = 30, shimmerDep = 30,
sylLen = 1000, pitch = c(150, 170),
play = playback, plot = TRUE, ylim = c(0, 3))s052 = soundgen(jitterLen = 1, jitterDep = 1, # harsh voice
shimmerLen = 1, shimmerDep = 10,
sylLen = 1000, pitch = c(150, 170),
play = playback, plot = TRUE, ylim = c(0, 3))Jitter + shimmer + subharmonics work well together. For example, barks of a small, annoying dog can be roughly approximated with this minimal code (ignoring respiration to keep things simple):
s053 = soundgen(repeatBout = 2, sylLen = 140, pauseLen = 100,
vocalTract = 8, formants = NULL, rolloff = -6,
pitch = c(1100, 1600, 1100), mouth = c(0, 0.5, 0),
jitterDep = 1, subDep = 60, play = playback) Note that jitter is random variation around a target value, but
soundgen also has relatively slow random pitch drift, which is
implemented as a random walk and can thus wander into values quite far
removed from the target. Random pitch drift is added whenever
temperature > 0. Use tempEffects to regulate its amount
and frequency:
# slight and slow (slightly unsteady voice)
s054 = soundgen(
sylLen = 1500, pitch = 300,
tempEffects = list(pitchDriftDep = 1, pitchDriftFreq = .5),
play = playback, plot = TRUE, ylim = c(0, 2))# strong and rapid (trembling voice, similar to jitter)
s055 = soundgen(
sylLen = 1500, pitch = 300,
tempEffects = list(pitchDriftDep = 5, pitchDriftFreq = 5),
play = playback, plot = TRUE, ylim = c(0, 2))# both drift and jitter (trembling voice ending with some "chaos")
s056 = soundgen(
sylLen = 1500, pitch = 300,
tempEffects = list(pitchDriftDep = 5, pitchDriftFreq = 5),
jitterDep = c(0, 0, 0, 2),
play = playback, plot = TRUE, ylim = c(0, 2))There is no way to synthesize true deterministic chaos with residual harmonic structure in soundgen. However, there are several roundabout ways to achieve a comparable effect. As already mentioned, strong jitter and shimmer create harsh sounds that are perceptually similar to deterministic chaos, especially for higher f0 values:
s057 = soundgen(
sylLen = 1200,
pitch = list(
time = c(0, 110, 111, 180, 350, 940, 941, 1100, 1200),
value = c(700, 1150, 1550, 2000, 2240, 1940, 1180, 900, 500)),
temperature = 0.05, tempEffects = list(pitchDep = 0),
jitterDep = list(time = c(0, 200, 201, 900, 901, 1200),
value = c(0, 0, 1.7, 1.2, 0, 0)),
formants = c(900, 1300, 3300, 4300),
attackLen = c(10, 200),
samplingRate = 44100, play = playback, plot = TRUE, ylim = c(0, 5))## pitchSamplingRate should be much higher than the highest pitch; resetting to 22400 Hz
Another method is to encode very rapid pitch jumps, say between f0 and a formant or between harmonically related values, like this:
s058 = soundgen(
sylLen = 1200,
pitch = list(
time = c(0, 80, 81, 230, 231, 385,
# 500 time anchors here - an episode of "chaos"
seq(385, 850, length.out = 500),
851, 1020, 1021, 1085),
value = c(700, 1130, 1000, 1200, 1860, 1840,
# random f0 jumps b/w 1.2 & 1.8 KHz
sample(c(1200, 1800), size = 500, replace = TRUE),
1620, 1540, 1220, 900)),
smoothing = list(interpol = 'linear'),
temperature = 0.05,
tempEffects = list(pitchDep = 0),
jitterDep = .3,
rolloffKHz = 0, rolloff = -8, formants = c(900, 1300, 3300, 4300),
samplingRate = 44100, play = playback, plot = TRUE, ylim = c(0, 5))## pitchSamplingRate should be much higher than the highest pitch; resetting to 18600 Hz
Incidentally, you can use similar tricks for introducing variation in
any soundgen parameter. For example, you can use runif() or
rnorm() to randomly vary things like mouth opening, pitch,
amplitude. That’s the best part of working in R!
Sometimes it may be necessary to control precisely the timing of each
nonlinear regime. For example, in an experiment a sound containing
nonlinear effects may need to be synthesized repeatedly, varying one
parameter and preserving everything else, including nonlinear regimes.
To control the timing of nonlinear effects manually, set
nonlinBalance to 100 (the entire vocalization) and vary the
strength of nonlinear effects with their vectorized “depth”
settings:
s060 = soundgen(
# nonlinear settings
jitterDep = c(0, 0, 1.5, .5), shimmerDep = c(0, 0, 15, 5),
# settings for high precision
temperature = .001, dynamicRange = 120,
samplingRate = 22050, pitchSamplingRate = 22050,
# other settings
sylLen = 1000, pitch = c(240, 200),
rolloff = c(-20, -18, -23, -28) + 4, vibratoDep = .2,
formants = c(800, 1400, 2500, 3700, 5000, 6800),
noise = list(time = c(0, 340, 900, 1000),
value = c(-60, -45, -60, -80) + 25),
rolloffNoise = -6,
mouth = c(.55, .5, .45, .6),
play = playback, plot = TRUE, ylim = c(0, 4)
)TIP For analytical-precision work, set
pitchSamplingRate to the same (high) value as
samplingRate, say 22050. By default,
pitchSamplingRate is much lower to speed up the synthesis,
but sound duration can vary considerably depending on nonlinear regimes
with a low pitchSamplingRate, especially in sounds like
screams with highly variable pitch. Increase it when precise timing is
required.
In the example above jitterDep = c(0, 0, 1.5, .5) means
that there is no jitter roughly in the first half of the voiced
fragment, then a jitter of 1.5 semitones, and then .5 semitones towards
the end. For more precision, use the “anchor format”. The same goes for
all vectorized parameters: jitterLen, shimmerDep, shimmerLen, subDep,
rolloff settings, etc. For example, to turn on jitter abruptly at 300 ms
and turn it off again at 500 ms, and to have shimmer only between 600
and 800 ms, we can modify the code as follows (it still won’t be precise
down to a millisecond, though):
s061 = soundgen(
# nonlinear settings
jitterDep = list(
time = c(0, 300, 301, 500, 501, 1000),
value = c(0, 0, 1.5, 1.5, 0, 0)
),
shimmerDep = list(
time = c(0, 600, 601, 800, 801, 1000),
value = c(0, 0, 40, 40, 0, 0)
),
# settings for high precision
temperature = .001, dynamicRange = 120,
samplingRate = 22050, pitchSamplingRate = 22050,
# other settings
smoothing = list(interpol = 'approx'),
# "approx" aka "linear" interpol to avoid overshooting (rapid transitions)
addSilence = 0, # easier to check timing
sylLen = 1000, pitch = c(240, 200),
rolloff = c(-20, -18, -23, -28), vibratoDep = .2,
formants = c(800, 1400, 2500, 3700, 5000, 6800),
noise = list(time = c(0, 340, 900, 1000),
value = c(-60, -45, -60, -80) + 30),
rolloffNoise = -8,
mouth = c(.55, .5, .45, .6),
play = playback, plot = TRUE, ylim = c(0, 4)
)Here is another method of controlling the timing of nonlinear
phenomena. When nonlinBalance < 100, soundgen divides a
sound into different nonlinear regimes by generating a random walk,
which also controls the drift of some other control parameters. Setting
temperature at nearly zero (say, at 0.001) removes random variation in
most control parameters, but the random walk for nonlinear effects still
remains random. To standardize that random walk as well, use
nonlinRandomWalk. nonlinRandomWalk should be a
vector containing 0, 1, and 2, where 0 = no nonlinearities, 1 =
subharmonics, and 2 = subharmonics + jitter + shimmer. The number and
order of 0/1/2 determines which nonlinear regime is active at which
time. For example, this will make a sound with no effect in the first
third, subharmonics in the second third, and jitter in the final third
of the total duration:
rw_bin = c(rep(0, 100), rep(1, 100), rep(2, 100))
s062 = soundgen(sylLen = 800, pitch = 300, temperature = 0.001,
subDep = 70, jitterDep = 1,
nonlinRandomWalk = rw_bin,
play = playback, plot = TRUE, ylim = c(0, 4))Make nonlinRandomWalk a fairly long vector for greater
precision, i.e. not just c(0, 1) - because of the way
approx works, that will NOT split the sound into 50% with
no nonlinear effects and 50% with subharmonics. Instead, write
c(rep(0, 50), rep(1, 50)) or some such.
You can also generate an actual random walk and then use it in several sounds to make sure their nonlinear effects have exactly the same timing. For example, here are two sounds with different pitch levels, but identical otherwise, including identical nonlinear regimes:
# set up a random walk (repeat until satisfied with the contour)
rw = soundgen:::getRandomWalk(len = 1000, rw_range = 100,
trend = c(0.5, -0.5), rw_smoothing = .95)
rw_bin = soundgen:::getIntegerRandomWalk(rw, minLength = 100, plot = TRUE)
# synthesize two sounds with identical nonlinear effects but different f0
s063 = soundgen(sylLen = 800, pitch = 300, temperature = 0.001,
subDep = 20, jitterDep = 1,
nonlinRandomWalk = rw_bin,
play = playback, plot = TRUE, ylim = c(0, 4))s064 = soundgen(sylLen = 800, pitch = 500, temperature = 0.001,
subDep = 20, jitterDep = 1,
nonlinRandomWalk = rw_bin,
play = playback, plot = TRUE, ylim = c(0, 4))In addition to the periodic (harmonic, voiced) component, which is synthesized as a stack of harmonics (sine waves), soundgen produces aperiodic noise. This noise can be added to the voiced component to create breathing, sniffing, snuffling, hissing, gargling, etc. It is often appropriate to include at least some noise in synthetic vocalizations, if only because it is more natural to have noise instead of harmonics in the upper part of the source spectrum.
The perceptual quality of turbulent noise depends on its spectral
composition, which is controlled by three soundgen arguments:
formantsNoise, rolloffNoise, and
rolloffNoiseExp (in the app, use “Tract / Noise type”).
Noise is generated as white noise with spectral rolloff given by
rolloffNoise (“Noise rolloff, dB/kHz” in the app) above a
certain cutoff value (noiseFlatSpec, the default is
currently 1200 Hz). The timing of the noise relative to the periodic
component is controlled by the argument noise, which is
discussed in the next section. There are two basic types of turbulent
noise in soundgen:
rolloffNoise setting,
while formantsNoise is NULL or
NA. This is useful for adding noise that originates deep in
the throat, close to the vocal cords. To generate breathing, specify
noise, but leave formantsNoise blank (NA,
which is its default value). Soundgen then assumes that the noise
component should have the same formant structure as the voiced
component.s065 = soundgen(
sylLen = 500,
noise = list(time = c(0, 800), value = c(-20, -10)),
formantsNoise = NA, # breathing - same formants as for voiced
play = playback, plot = TRUE)formantsNoise) manually in exactly the
same format as for the voiced component (formants).s066 = soundgen(
sylLen = 200, pitch = c(150, 120),
noise = list(time = c(180, 250, 400), value = c(-20, -10, -50)),
# specify noise filter ≠ voiced filter to get ~[s]
formantsNoise = list(f1 = list(freq = 7000, amp = 40, width = 1500)),
rolloffNoise = 0,
play = playback, plot = TRUE)TIP: pitch = NA or NULL removes the
voiced component, so that only turbulent noise is synthesized. Voiced
component is also not generated if min(noise$value) >= 40 dB. In the
app, untick the box Intonation / Intonation syllable / “Generate voiced
component?”
If formantsNoise = NA or NULL (i.e., if
this is aspiration noise), formant structure is calculated based on
vocal tract length, and then extra stochastic formants are added as
usual. For example, to create simple sighs, you can just specify the
length of your creature’s vocal tract:
s067 = soundgen(
vocalTract = 15.5, # ~human throat (15.5 cm)
formants = NULL,
noise = 0, pitch = NULL,
attackLen = 200, play = playback)s068 = soundgen(
vocalTract = 30, # a large animal
formants = NULL,
noise = 0, pitch = NULL,
attackLen = 200, play = playback)s069 = soundgen(
vocalTract = 100, # a whale
formants = NULL,
noise = 0, pitch = NULL,
attackLen = 200, play = playback) In contrast, if formantsNoise is specified explicitly
(i.e., if this is not aspiration noise), breathing noise is by default
NOT enriched with stochastically added formants. To avoid losing all
high-frequency energy in your noise, make sure you add a sufficient
number of formants in formantsNoise - ideally, all the way
up to Nyquist frequency (half the sampling rate). Alternatively, you can
explicitly specify vocalTract, and then extra formants will
be added to the noise component. Compare:
# only two specified formants
s070 = soundgen(pitch = NULL,
formantsNoise = c(1000, 2000),
noise = 40, sylLen = 800,
play = playback, plot = TRUE)