Chapter 3 Audio features — Spotify

Spotify has developed a number of metrics that assess the sonic characteristics of any given songs. These features are described in their API documentation. The R package spotifyr provides a great interface to access these features.

In order to use spotifyr, you must have API credentials. See the README for more instructions. Below I reference an .Renvironfile I created that hosts my Spotify API credentials.

library(spotifyr)

access_token <- get_spotify_access_token(
  client_id = Sys.getenv("SPOTIFY_CLIENT_ID"),
  client_secret = Sys.getenv("SPOTIFY_CLIENT_SECRET")
)

One current limitation of the API is that it requires a song’s identification number to retrieve audio features. In an effort to simplify the process of retrieving the audio features for each song, I created a function that we can use with map() inside of a mutate() call. Note that this is a very useful pattern commit to memory.

track_audio_features <- function(artist, title, type = "track") {
  search_results <- search_spotify(paste(artist, title), type = type)
  
  track_audio_feats <- get_track_audio_features(search_results$id[[1]]) %>%
    dplyr::select(-id, -uri, -track_href, -analysis_url)
  
  return(track_audio_feats)
}

Let’s take a moment to undersand what is happening in this function. First, we take the artist and title arguments and create a search string to provide to search_spotify(). This returns a data frame object for the top search results. Next, we take the track ID from the first search result (this works under the assumption that the spotify_search is robust enough to identify the track based on the artist and song name), and is fed to the get_track_audio_features() function.

Let’s see how this works on it’s own by search spotify for a single song. Below I look for the song Nocturne: Lost Faith by the metalcore band Invent, Animate.

ia <- search_spotify("Invent, Animate Nocturne: Lost Faith", type = "track")
ia
## # A tibble: 1 x 29
##   artists available_marke… disc_number duration_ms explicit href  id   
##   <list>  <list>                 <int>       <int> <lgl>    <chr> <chr>
## 1 <df[,6… <chr [78]>                 1      206900 FALSE    http… 0tTe…
## # … with 22 more variables: is_local <lgl>, name <chr>, popularity <int>,
## #   preview_url <chr>, track_number <int>, type <chr>, uri <chr>,
## #   album.album_type <chr>, album.artists <list>,
## #   album.available_markets <list>, album.href <chr>, album.id <chr>,
## #   album.images <list>, album.name <chr>, album.release_date <chr>,
## #   album.release_date_precision <chr>, album.total_tracks <int>,
## #   album.type <chr>, album.uri <chr>, album.external_urls.spotify <chr>,
## #   external_ids.isrc <chr>, external_urls.spotify <chr>

Now we can take the ID column and feed it to get_track_audio_features(). Note that there will often be more than one search result, hence the subsetting within the function.

get_track_audio_features(ia$id)
## # A tibble: 1 x 18
##   danceability energy   key loudness  mode speechiness acousticness
##          <dbl>  <dbl> <int>    <dbl> <int>       <dbl>        <dbl>
## 1        0.463  0.988     8    -3.39     0       0.156    0.0000924
## # … with 11 more variables: instrumentalness <dbl>, liveness <dbl>,
## #   valence <dbl>, tempo <dbl>, type <chr>, id <chr>, uri <chr>,
## #   track_href <chr>, analysis_url <chr>, duration_ms <int>,
## #   time_signature <int>

How does the self-defined function perform?

track_audio_features("Invent, Animate", "Nocturne: Lost Faith")
## # A tibble: 1 x 14
##   danceability energy   key loudness  mode speechiness acousticness
##          <dbl>  <dbl> <int>    <dbl> <int>       <dbl>        <dbl>
## 1        0.463  0.988     8    -3.39     0       0.156    0.0000924
## # … with 7 more variables: instrumentalness <dbl>, liveness <dbl>,
## #   valence <dbl>, tempo <dbl>, type <chr>, duration_ms <int>,
## #   time_signature <int>

What happens if a song is not found?

track_audio_features("definiely not", "in spotify")

We are returned a very uninformative error. As we will iterate through our data frame of 600 songs, there is a good chance that at least one track will not be found in Spotify. To cope with this we can use the possibly() function from purrr which allows functions to fail gracefully. By wrapping our function in possibly(), we can specify what is returned in the case of an error.

possible_feats <- possibly(track_audio_features, otherwise = tibble())

Here we define another function possible_feats() which, when it encounters an error, will return an empty tibble. The empty tibble is important as we will later have to unnest the data frame.

Now we are prepared to iterate through all of the tracks and fetch the audio features. Note this is not a quick operation creating a great opportunity to pour yourself another cup of coffee.

chart_analysis <- charts %>%
  mutate(audio_features = map2(artist, title, possible_feats)) %>%
  unnest() %>% 
  as_tibble()
glimpse(chart_analysis)
## Observations: 596
## Variables: 20
## $ rank             <int> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, …
## $ year             <int> 2016, 2016, 2016, 2016, 2016, 2016, 2016, 2016,…
## $ chart            <chr> "Hot Country Songs", "Hot Country Songs", "Hot …
## $ artist           <chr> "Florida Georgia Line", "Thomas Rhett", "Tim Mc…
## $ featured_artist  <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,…
## $ title            <chr> "H.O.L.Y.", "Die A Happy Man", "Humble And Kind…
## $ danceability     <dbl> 0.525, 0.660, 0.355, 0.573, 0.557, 0.531, 0.579…
## $ energy           <dbl> 0.685, 0.383, 0.480, 0.635, 0.676, 0.746, 0.776…
## $ key              <int> 7, 2, 11, 7, 7, 11, 8, 9, 1, 1, 0, 9, 6, 3, 4, …
## $ loudness         <dbl> -3.954, -9.377, -7.310, -6.621, -4.457, -4.228,…
## $ mode             <int> 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 0, 1, 1,…
## $ speechiness      <dbl> 0.0351, 0.0304, 0.0282, 0.0275, 0.0270, 0.0329,…
## $ acousticness     <dbl> 0.385000, 0.381000, 0.679000, 0.000598, 0.06610…
## $ instrumentalness <dbl> 0.00e+00, 0.00e+00, 2.28e-06, 0.00e+00, 0.00e+0…
## $ liveness         <dbl> 0.0731, 0.1190, 0.1200, 0.0845, 0.0871, 0.1020,…
## $ valence          <dbl> 0.540, 0.381, 0.137, 0.447, 0.678, 0.304, 0.434…
## $ tempo            <dbl> 78.003, 83.096, 100.956, 144.031, 108.016, 139.…
## $ type             <chr> "audio_features", "audio_features", "audio_feat…
## $ duration_ms      <int> 194187, 227427, 259267, 197120, 202347, 226307,…
## $ time_signature   <int> 4, 4, 3, 4, 4, 4, 4, 4, 4, 4, 4, 3, 4, 4, 4, 4,…