This tutorial walks the user through the creation of spectrographic images of .wav files of birdsong using R.

Preparing your RStudio Workspace

1. Set working directory

Make a folder on your desktop titled “spectro” and place your .wav files there. Then open RStudio and set your working directory to that folder:

setwd("C:/Users/Shelby Palmer/Desktop/spectro")

2. Install required packages

Next, you need to install the packages we will use to handle the sound files and create the spectrograms. I commented out these lines of code (added # symbols at the beginning so that they would not run) because these packages are already installed on my computer. For your purposes, remove the # symbols to run the code.

# install.packages("seewave")
# install.packages("tuneR")

3. Load the newly-installed packages

library(tuneR)
library(seewave)

We are now ready to begin working with the sound files.

Preparing the Sound Files

1. Read in a sound file

First you need to import one of the sound files into your R environment to begin working with it. This is done using the function readWave() and the name of one of your sound files, as shown below:

readWave("XC350304_Black-capped-chickadee.wav")
## 
## Wave Object
##  Number of Samples:      1530938
##  Duration (seconds):     34.72
##  Samplingrate (Hertz):   44100
##  Channels (Mono/Stereo): Stereo
##  PCM (integer format):   TRUE
##  Bit (8/16/24/32/64):    16

Shortcut: By running this line of code

list.files(pattern = ".wav")
## [1] "XC350304_Black-capped-chickadee.wav" "XC742516_White-eyed-vireo.wav"      
## [3] "XC814764_Indigo-bunting.wav"

you can get an output of all of the file names in your working directory that are .wav files. Then you can just copy-paste one into the parentheses of the readWave() function. Or, if you want to be more elegant, running the line of code below

readWave(list.files(pattern = ".wav")[1])
## 
## Wave Object
##  Number of Samples:      1530938
##  Duration (seconds):     34.72
##  Samplingrate (Hertz):   44100
##  Channels (Mono/Stereo): Stereo
##  PCM (integer format):   TRUE
##  Bit (8/16/24/32/64):    16

will give you the same result as copy-pasting the first file name (because the [1] after list.files() grabs just the first entry in the list of file names).

2. Create an object

We’ve successfully read a sound file into R now, but we can’t manipulate it unless we give it its own name in R. We need to make it into an R object. We can do that using the <- symbol, and by giving the .wav file any name we want (I’m going to go with the abbreviated name of the Black-capped chickadee).

bcch <- readWave("XC350304_Black-capped-chickadee.wav")

We will now use bcch anytime we want to do something with this object in R. To make sure it is the right type of file, we can run

class(bcch)
## [1] "Wave"
## attr(,"package")
## [1] "tuneR"

We want it to be an object of class "Wave".

3. Filter out unwanted frequencies

Most sound files have some amount of low-level ambient noise that we may want to get rid of so it doesn’t end up in our spectrogram. We can filter out certain frequencies of sound that we know don’t include our target sound using the function fir(). Most functions require that you provide certain specifications (called “arguments”) so that they can customize their functionality to your needs. You can figure out what arguments fir() requires by running ?fir in your console, which will pull up the documentation page (basically a mini how-to manual).

We will need to give fir()

  • the R name of our sound file (bcch)
  • the lower and upper frequency boundaries that we want to keep, in Hz (these will vary based on what kind of sound you’re dealing with; for the Black-capped chickadee, I know they never sing below 2000 Hz or above 6000 Hz, so we’ll use those)
  • instruction to apply a bandpass filter (a bandstop filter will remove the frequencies within the lower and upper thresholds we specify, which we don’t want)
  • the proper output, which is “Wave”
bcch <- fir(wave = bcch,
            from = 2000, # lower bound frequency in Hz
            to = 6000, # upper bound frequency in Hz
            bandpass = TRUE,
            output = "Wave")

Notice that we named the filtered version of bcch also bcch. This overwrites the original unfiltered bcch recording, since we don’t need it anymore. Some people advise against this, but I’d rather do this than clutter up my workspace with bcch2, bcch3, etc. Personally I hate having a million R objects that I can’t remember all the names of. To each their own.

Working with Spectrograms

1. Create a spectrogram

Now we are ready to start actually looking at our sound file using the function spectro(). Let’s begin by running spectro() on our sound file without specifying any arguments and seeing what happens.

spectro(wave = bcch)

2. Customize your spectrogram

Cool, there are all the Black-capped chickadee songs. But they are kind of tiny and hard to see. The first thing you may notice is that the frequency (y) axis goes all the way up to 20000 Hz/20 kHz, way higher than we need for this species. We can fix that by adding an argument flim in spectro(). And let’s also get rid of the amplitude scale bar while we’re at it (because I think it’s unsightly).

spectro(wave = bcch,
        flim = c(0,8), # frequency limits in kHz
        scale = FALSE)

  • (Quick aside: Notice that the frequency scale argument we use above works similarly to how we set the frequency boundaries when we were filtering the recording with fir(), but the wording required by these two different functions is extremely different: from = [X], to = [Y] in fir(), and flim = c([X],[Y]) in spectro(). That’s one of those pain-in-the-ass things about R: different functions don’t always agree on how to ask you, the R user, for essentially the same thing…which can result in cryptic errors and unnecessary stress. The fix for this is to always use ?[function name] to make sure you’re getting the wording of the arguments right.)

Now we have a more sensible frequency axis, but we can’t get a very good idea of the shape of the notes because they’re so squished on the time (x) axis. What if we looked at just 3 of the songs? We can see from looking at the previous spectrogram that the third song ends at probably 8-ish seconds, so let’s use the tlim() argument to trim it at 10 seconds.

spectro(wave = bcch,
        flim = c(0,8), # frequency limits in kHz
        tlim = c(0,10), # time limits in seconds
        scale = FALSE)

And now let’s go down to a single song

spectro(wave = bcch,
        flim = c(0,8), # frequency limits in kHz
        tlim = c(0,2), # time limits in seconds
        scale = FALSE)

There are a lot of other arguments in spectro() that you can change to your liking. Mess with them if you want until you get an image you really like. It’s especially fun to mess with the color of the signal traces using the argument palette. The options are:
temp.colors
reverse.gray.colors.1
reverse.gray.colors.2
reverse.heat.colors
reverse.terrain.colors
reverse.topo.colors
reverse.cm.colors
terrain.colors
topo.colors
cm.colors

And here are my personal favorite settings to use for publication-quality images.

spectro(bcch,
        wl = 512, # window length (determines frequency resolution)
        ovlp = 90, # percentage window overlap
        collevels = seq(-30,0,5), # sets amplitude threshold and gradient levels for color
        flim = c(0,8), # will change based on species
        tlim = c(0,2), # will change based on species
        scale = F, # get rid of scale
        colgrid = "gray", # makes the background grid gray
        palette=reverse.gray.colors.2 # makes the signal traces black & white
        )

Exporting Your Spectrogram

To export your spectrogram to your working directory (the “spectros” folder), you need to use the function png(). It’ll ask you for an output file name, a width measurement, and a height measurement (the measurement unit default is pixels, but you can change that to inches or centimeters if you prefer. I use inches, which then requires you to provide a resolution argument, for which I use 1000 for a nice crisp image). Then you can copy-paste your spectro() function and all its arguments below the png() line. And then below that, you’ll put the function dev.off(). What happens when you run these three lines sequentially–png() opens an empty .png object in R, spectro() creates an image to fill it, and dev.off() basically kicks the image out of your R environment, so it ends up getting saved to your working directory. Don’t ask me for any more detail than that!

png(filename = "bcch_spectro.png",
    width = 12,
    height = 8,
    units = "in",
    res = 1000)
spectro(bcch,
        wl = 512, 
        ovlp = 90, 
        collevels = seq(-30,0,5), 
        flim = c(0,8), 
        tlim = c(0,2), 
        scale = F, 
        colgrid = "gray", 
        palette=reverse.gray.colors.2 
        )
dev.off()
## png 
##   2

And that’s it!! Check the “spectros” folder for your image; it should be in there, and if it’s not the way you wanted it, mess with the arguments in png() and spectro() until it’s to your liking. I’ve also given you songs of the White-eyed vireo and the Indigo bunting to mess around with if you want. Hope you enjoyed!!