Goa or psytrance music has several subgenres and some of them are difficult to tell apart. I want to build a classifier to predict genres based on music features. First, I need an expert opinion to use for supervised classification. For that, I’ll use the absolutely fantastic http://psytranceguide.com/ (Made by @DanielLesden) that lists 20 psytrance genres and fortunately, also has example playlists that can be scraped and analyzed.

Highlights

  • Webscraping using rvest

  • Using the spotifyr package to get music features

  • Exploring the music features

  • Building a random forest classifier on bootstrap resamples using tidymodels

  • Estimating the importance of each feature using the vip package

  • Creating a network of subgenres from the confusion matrix

  • Misc: using tidyverse for data wrangling, rectangling, and visualization.

# Load packages
library(tidyverse)
library(rvest)
library(spotifyr)
library(tidytext)
library(tidymodels)
library(vip)
library(igraph)
library(ggraph)

# Default ggplot theme
theme_set(theme_light())

# Setup parallel procesing
doParallel::registerDoParallel()

# Authenticate the spotify API through spotifyr.
spotify_id <- read_csv(here::here("spotify_client_id.txt"))

Sys.setenv(SPOTIFY_CLIENT_ID = spotify_id$client_id)
Sys.setenv(SPOTIFY_CLIENT_SECRET = spotify_id$secret)

access_token <- get_spotify_access_token()

Create the dataset

The first task is to scrape the data that we can use for machine learning. Get data from http://psytranceguide.com/ . There are genres here with examples, and what is more, links lead to spotify playlists. The spotifyr package makes it possible to retrieve track features for each playlist that we can use for classification.

Create a dataset that contains genre data and track features.

# Scrape the homepage.
psytrance_link <- "http://psytranceguide.com/"
psytrance_page <- read_html(psytrance_link)

# Get the urls and other data from the psytrance guide
# I used the selectorGadget to find the proper css tags
goa_data <- 
  psytrance_page %>% 
  html_nodes("h2 a , .jouele-info-control-text")

# Create a table for all data (genres + playlist links)
goa_genres <-
  tibble(genre = map_chr(goa_data, html_text),
         playlist_url = html_attr(goa_data, "href")) %>% 
  extract(playlist_url, 
          into = "playlist_id", 
          regex = ".*playlist/(.*)\\?si.*") 

# Get all music features for all tracks for each playlist
features <- map_dfr(goa_genres$playlist_id, 
                    ~get_playlist_audio_features(playlist_uris = .x))


# Put playlist information and features together
goa <- 
  left_join(goa_genres, features, by = "playlist_id") %>% 
  hoist(track.album.artists, "name", .remove = TRUE) %>% 
  select(genre:playlist_name, 
         track_id = track.id, track_name = track.name, 
         artist_names = name,
         danceability:tempo) %>% 
  rowwise() %>% 
  mutate(artist_names = str_c(artist_names, collapse = ", ")) %>% 
  ungroup()

# Write data 
write_csv(goa, here::here("data/goa_tracks.csv"))

Explore goa data

First let’s see if there is enough variability in the features. I’m using a trick to calculate confidence intervals for the means by using a one-sample t.test with 0 as reference (could be any number as we are not interested in the p value).

# Use the saved dataset so we don't have to reassemle the data.
goa <- 
  read_csv(here::here("data/goa_tracks.csv")) %>% 
  select(genre, track_name, danceability:tempo) %>% 
  mutate(genre = factor(genre))

# Balance the data by sampling exactly 10 tracks from each genre
set.seed(1)
goa <-
  goa %>% 
  group_by(genre) %>% 
  sample_n(10) %>% 
  ungroup()

# Put data into long format, calculate confidence intervals
goa_long <- 
  goa %>% 
  pivot_longer(danceability:tempo,
               names_to = "feature") %>% 
  group_nest(genre, feature) %>% 
  mutate(t_test = map(data, 
                      ~t.test(.x$value) %>% 
                      tidy())) %>% 
  unnest(c(t_test, data))
  
# Distributions
goa_long %>% 
  ggplot() +
  aes(x = value, fill = feature) +
  geom_histogram(show.legend = FALSE) +
  facet_wrap(~feature, scales = "free")

Let’s order genres by features. Some features seem legit on face validity, although I have some difficulty in telling apart psyctrance genres. Also, I’m not fully aware of the meaning of all features.

goa_long %>% 
  mutate(genre = reorder_within(genre, estimate, feature)) %>%
  ggplot() +
  aes(x = estimate, y = genre, 
      xmin = conf.low, xmax = conf.high,
      color = feature) %>% 
  geom_pointrange(size = 0.5, show.legend = FALSE) +
  scale_y_reordered() +
  facet_wrap(~feature, scales = "free") +
  labs(title = "Average feature values by genre (95%CI)",
       subtitle = "Several features have high variability across genres that make features plausible to classify genres.",
       x = NULL, y = NULL)

Classify goa subgenres using random forest

Create a random forest model for finding the defining features for the different subgenres. As we have a very little dataset, we are not splitting it into training and test set, but we use bootstraps to make the model more robust.

Bootstrap data, specify model and workflow.

# Create bootstraps
set.seed(123)
goa_boot <- bootstraps(goa)

# Specify random forest model
rf_spec <- 
  rand_forest(trees = 1000) %>%
  set_mode("classification") %>%
  set_engine("ranger", importance = "permutation")

# Create workflow
goa_wf <- 
  workflow() %>%
  add_formula(genre ~ . -track_name) %>% 
  add_model(rf_spec)

Train model and evaluate. I seems like accuracy is poor, and sensitivity is also not great, that is a characteristic of multiclass classifications (too many categories to choose from). In overall, the model seems to have a 90% ROC AUC.

goa_rs <- 
  goa_wf %>% 
  fit_resamples(resamples = goa_boot,
                metrics = metric_set(roc_auc, accuracy,
                                     specificity, sensitivity),
                control = control_resamples(verbose = TRUE,
                                            save_pred = TRUE,
                                            save_workflow = TRUE))

goa_rs %>% 
  collect_metrics()
## # A tibble: 4 x 5
##   .metric  .estimator  mean     n  std_err
##   <chr>    <chr>      <dbl> <int>    <dbl>
## 1 accuracy multiclass 0.317    25 0.0102  
## 2 roc_auc  hand_till  0.900    25 0.00326 
## 3 sens     macro      0.372    25 0.0117  
## 4 spec     macro      0.964    25 0.000517

Which goa genres are the most difficult to classify?

It seems like some styles are more difficult than others to classify. As the individual ROC curve show Goa Trance, Pssybreaks, Psytechno can be easily confused with other styles. In contrast, Chillout, Hi-Tech, and Offbeat are easier to identify.

goa_rs %>% 
  collect_predictions() %>%
  roc_curve(genre, .pred_Chillout:.pred_Tribal) %>% 
  ggplot() +
  aes(x = 1 - specificity, y = sensitivity, color = .level) +
  geom_abline(lty = 2, color = "gray80", size = 1) +
  geom_path(show.legend = FALSE, alpha = 0.6, size = 1.2) +
  facet_wrap(~.level, ncol = 5) +
  scale_color_viridis_d(option = "plasma") +
  scale_x_continuous(labels = scales::percent_format()) +
  scale_y_continuous(labels = scales::percent_format()) +
  labs(title = "Individual ROC curves for each goa genre.",
       subtitle = "Some genres are easier to classify than others.")

Importance of the features

We can evaluate the importance of the features. It seems like tempo is by far the most important feature, followed by danceability and energy. Mode is not especially helpful (I’m not even sure what it means).

rf_spec %>% 
  set_engine("ranger", importance = "permutation") %>%
  fit(genre ~ ., data = goa) %>% 
  vip(geom = "col") +
  aes(fill = Variable) +
  scale_fill_viridis_d(option = "viridis") +
  labs(title = "Variable importance for classification")

Explore which genres are easier to confuse with each other

Because of the several classes and the small dataset, the confusion matrix shows that there are only a few genres that are correctly classified at least half of the time. It also shows what genres are the easiest to confuse with each other.

goa_confusion <-
  goa_rs %>%
  collect_predictions() %>% 
  conf_mat(genre, .pred_class) 

goa_conf_perc <-
  goa_confusion$table %>% 
  as_tibble() %>% 
  group_by(Truth) %>% 
  mutate(all = sum(n),
         perc = n/all) 

goa_conf_perc %>% 
  ggplot() +
  aes(x = Truth, y = Prediction, fill = perc, 
      label = scales::percent(perc, accuracy = 1)) +
  geom_tile(show.legend = FALSE) +
  geom_text(size = 3) +
  scale_fill_viridis_c(option = "plasma") +
  labs(title = "Confusion matrix for the random forest model",
       subtitle = "Percent of correct classifications in the genre",
       x = "Truth") +
  theme(axis.text.x = element_text(angle = 90, vjust = 0.5, hjust=1))

Let’s visualize how easy it is to confuse certain genres with others by building a network graph!

goa_graph <-
  goa_conf_perc %>% 
  filter(Prediction != Truth & n > 0) %>% 
  select(Prediction, Truth, perc) %>% 
  graph_from_data_frame(directed = FALSE)

goa_graph %>% 
  ggraph() +
  geom_edge_link(aes(edge_width = perc),
                 alpha = .4) +
  scale_edge_width(range = c(.01, 3)) +
  geom_node_text(aes(label = name, 
                     size = betweenness(goa_graph),
                     color = name),
                 show.legend = FALSE) +
  scale_size(range = c(3,6)) +
  scale_color_viridis_d(option = "plasma") +
  guides(fill = FALSE, alpha = FALSE, edge_alpha = FALSE, edge_width = FALSE) +
  theme_graph() +
  labs(title = "Confusion map of psytrance genres",
       subtitle = "The wider the connection between genre names, the easier to confuse.\nThe size of the node shows the centrality (betweenness)")

That’s all!

LS0tDQp0aXRsZTogIlBzeXRyYW5jZSBnZW5yZSBjbGFzc2lmaWVyIg0KYXV0aG9yOiAiVGFtYXMgTmFneSINCmRhdGU6ICIxMC8yNC8yMDIwIg0Kb3V0cHV0OiANCiAgaHRtbF9kb2N1bWVudDoNCiAgIHRoZW1lOiBwYXBlcg0KICAgY29kZV9kb3dubG9hZDogdHJ1ZQ0KICAgdG9jOiB0cnVlDQogICB0b2NfZmxvYXQ6IHRydWUNCmVkaXRvcl9vcHRpb25zOiANCiAgY2h1bmtfb3V0cHV0X3R5cGU6IGNvbnNvbGUNCi0tLQ0KDQpHb2Egb3IgcHN5dHJhbmNlIG11c2ljIGhhcyBzZXZlcmFsIHN1YmdlbnJlcyBhbmQgc29tZSBvZiB0aGVtIGFyZSBkaWZmaWN1bHQgdG8gdGVsbCBhcGFydC4gSSB3YW50IHRvIGJ1aWxkIGEgY2xhc3NpZmllciB0byBwcmVkaWN0IGdlbnJlcyBiYXNlZCBvbiBtdXNpYyBmZWF0dXJlcy4gRmlyc3QsIEkgbmVlZCBhbiBleHBlcnQgb3BpbmlvbiB0byB1c2UgZm9yIHN1cGVydmlzZWQgY2xhc3NpZmljYXRpb24uIEZvciB0aGF0LCBJJ2xsIHVzZSB0aGUgYWJzb2x1dGVseSBmYW50YXN0aWMgPGh0dHA6Ly9wc3l0cmFuY2VndWlkZS5jb20vPiAoTWFkZSBieSBcQERhbmllbExlc2RlbikgdGhhdCBsaXN0cyAyMCBwc3l0cmFuY2UgZ2VucmVzIGFuZCBmb3J0dW5hdGVseSwgYWxzbyBoYXMgZXhhbXBsZSBwbGF5bGlzdHMgdGhhdCBjYW4gYmUgc2NyYXBlZCBhbmQgYW5hbHl6ZWQuDQoNCiFbXShodHRwczovL21lZGlhLmdpcGh5LmNvbS9tZWRpYS9DbkdiQ2p4STY4QTh3L2dpcGh5LmdpZikNCg0KIyMgSGlnaGxpZ2h0cw0KDQotICAgV2Vic2NyYXBpbmcgdXNpbmcgcnZlc3QNCg0KLSAgIFVzaW5nIHRoZSBzcG90aWZ5ciBwYWNrYWdlIHRvIGdldCBtdXNpYyBmZWF0dXJlcw0KDQotICAgRXhwbG9yaW5nIHRoZSBtdXNpYyBmZWF0dXJlcw0KDQotICAgQnVpbGRpbmcgYSByYW5kb20gZm9yZXN0IGNsYXNzaWZpZXIgb24gYm9vdHN0cmFwIHJlc2FtcGxlcyB1c2luZyB0aWR5bW9kZWxzDQoNCi0gICBFc3RpbWF0aW5nIHRoZSBpbXBvcnRhbmNlIG9mIGVhY2ggZmVhdHVyZSB1c2luZyB0aGUgdmlwIHBhY2thZ2UNCg0KLSAgIENyZWF0aW5nIGEgbmV0d29yayBvZiBzdWJnZW5yZXMgZnJvbSB0aGUgY29uZnVzaW9uIG1hdHJpeA0KDQotICAgTWlzYzogdXNpbmcgdGlkeXZlcnNlIGZvciBkYXRhIHdyYW5nbGluZywgcmVjdGFuZ2xpbmcsIGFuZCB2aXN1YWxpemF0aW9uLg0KDQpgYGB7ciBzZXR1cCwJZWNobyA9IFRSVUUsIG1lc3NhZ2UgPSBGQUxTRSwgd2FybmluZyA9IEZBTFNFfQ0KIyBMb2FkIHBhY2thZ2VzDQpsaWJyYXJ5KHRpZHl2ZXJzZSkNCmxpYnJhcnkocnZlc3QpDQpsaWJyYXJ5KHNwb3RpZnlyKQ0KbGlicmFyeSh0aWR5dGV4dCkNCmxpYnJhcnkodGlkeW1vZGVscykNCmxpYnJhcnkodmlwKQ0KbGlicmFyeShpZ3JhcGgpDQpsaWJyYXJ5KGdncmFwaCkNCg0KIyBEZWZhdWx0IGdncGxvdCB0aGVtZQ0KdGhlbWVfc2V0KHRoZW1lX2xpZ2h0KCkpDQoNCiMgU2V0dXAgcGFyYWxsZWwgcHJvY2VzaW5nDQpkb1BhcmFsbGVsOjpyZWdpc3RlckRvUGFyYWxsZWwoKQ0KDQojIEF1dGhlbnRpY2F0ZSB0aGUgc3BvdGlmeSBBUEkgdGhyb3VnaCBzcG90aWZ5ci4NCnNwb3RpZnlfaWQgPC0gcmVhZF9jc3YoaGVyZTo6aGVyZSgic3BvdGlmeV9jbGllbnRfaWQudHh0IikpDQoNClN5cy5zZXRlbnYoU1BPVElGWV9DTElFTlRfSUQgPSBzcG90aWZ5X2lkJGNsaWVudF9pZCkNClN5cy5zZXRlbnYoU1BPVElGWV9DTElFTlRfU0VDUkVUID0gc3BvdGlmeV9pZCRzZWNyZXQpDQoNCmFjY2Vzc190b2tlbiA8LSBnZXRfc3BvdGlmeV9hY2Nlc3NfdG9rZW4oKQ0KDQpgYGANCg0KIyMgQ3JlYXRlIHRoZSBkYXRhc2V0DQoNClRoZSBmaXJzdCB0YXNrIGlzIHRvIHNjcmFwZSB0aGUgZGF0YSB0aGF0IHdlIGNhbiB1c2UgZm9yIG1hY2hpbmUgbGVhcm5pbmcuIEdldCBkYXRhIGZyb20gPGh0dHA6Ly9wc3l0cmFuY2VndWlkZS5jb20vPiAuIFRoZXJlIGFyZSBnZW5yZXMgaGVyZSB3aXRoIGV4YW1wbGVzLCBhbmQgd2hhdCBpcyBtb3JlLCBsaW5rcyBsZWFkIHRvIHNwb3RpZnkgcGxheWxpc3RzLiBUaGUgc3BvdGlmeXIgcGFja2FnZSBtYWtlcyBpdCBwb3NzaWJsZSB0byByZXRyaWV2ZSB0cmFjayBmZWF0dXJlcyBmb3IgZWFjaCBwbGF5bGlzdCB0aGF0IHdlIGNhbiB1c2UgZm9yIGNsYXNzaWZpY2F0aW9uLg0KDQpDcmVhdGUgYSBkYXRhc2V0IHRoYXQgY29udGFpbnMgZ2VucmUgZGF0YSBhbmQgdHJhY2sgZmVhdHVyZXMuDQoNCmBgYHtyLCBjYWNoZSA9IFRSVUV9DQojIFNjcmFwZSB0aGUgaG9tZXBhZ2UuDQpwc3l0cmFuY2VfbGluayA8LSAiaHR0cDovL3BzeXRyYW5jZWd1aWRlLmNvbS8iDQpwc3l0cmFuY2VfcGFnZSA8LSByZWFkX2h0bWwocHN5dHJhbmNlX2xpbmspDQoNCiMgR2V0IHRoZSB1cmxzIGFuZCBvdGhlciBkYXRhIGZyb20gdGhlIHBzeXRyYW5jZSBndWlkZQ0KIyBJIHVzZWQgdGhlIHNlbGVjdG9yR2FkZ2V0IHRvIGZpbmQgdGhlIHByb3BlciBjc3MgdGFncw0KZ29hX2RhdGEgPC0gDQogIHBzeXRyYW5jZV9wYWdlICU+JSANCiAgaHRtbF9ub2RlcygiaDIgYSAsIC5qb3VlbGUtaW5mby1jb250cm9sLXRleHQiKQ0KDQojIENyZWF0ZSBhIHRhYmxlIGZvciBhbGwgZGF0YSAoZ2VucmVzICsgcGxheWxpc3QgbGlua3MpDQpnb2FfZ2VucmVzIDwtDQogIHRpYmJsZShnZW5yZSA9IG1hcF9jaHIoZ29hX2RhdGEsIGh0bWxfdGV4dCksDQogICAgICAgICBwbGF5bGlzdF91cmwgPSBodG1sX2F0dHIoZ29hX2RhdGEsICJocmVmIikpICU+JSANCiAgZXh0cmFjdChwbGF5bGlzdF91cmwsIA0KICAgICAgICAgIGludG8gPSAicGxheWxpc3RfaWQiLCANCiAgICAgICAgICByZWdleCA9ICIuKnBsYXlsaXN0LyguKilcXD9zaS4qIikgDQoNCiMgR2V0IGFsbCBtdXNpYyBmZWF0dXJlcyBmb3IgYWxsIHRyYWNrcyBmb3IgZWFjaCBwbGF5bGlzdA0KZmVhdHVyZXMgPC0gbWFwX2Rmcihnb2FfZ2VucmVzJHBsYXlsaXN0X2lkLCANCiAgICAgICAgICAgICAgICAgICAgfmdldF9wbGF5bGlzdF9hdWRpb19mZWF0dXJlcyhwbGF5bGlzdF91cmlzID0gLngpKQ0KDQoNCiMgUHV0IHBsYXlsaXN0IGluZm9ybWF0aW9uIGFuZCBmZWF0dXJlcyB0b2dldGhlcg0KZ29hIDwtIA0KICBsZWZ0X2pvaW4oZ29hX2dlbnJlcywgZmVhdHVyZXMsIGJ5ID0gInBsYXlsaXN0X2lkIikgJT4lIA0KICBob2lzdCh0cmFjay5hbGJ1bS5hcnRpc3RzLCAibmFtZSIsIC5yZW1vdmUgPSBUUlVFKSAlPiUgDQogIHNlbGVjdChnZW5yZTpwbGF5bGlzdF9uYW1lLCANCiAgICAgICAgIHRyYWNrX2lkID0gdHJhY2suaWQsIHRyYWNrX25hbWUgPSB0cmFjay5uYW1lLCANCiAgICAgICAgIGFydGlzdF9uYW1lcyA9IG5hbWUsDQogICAgICAgICBkYW5jZWFiaWxpdHk6dGVtcG8pICU+JSANCiAgcm93d2lzZSgpICU+JSANCiAgbXV0YXRlKGFydGlzdF9uYW1lcyA9IHN0cl9jKGFydGlzdF9uYW1lcywgY29sbGFwc2UgPSAiLCAiKSkgJT4lIA0KICB1bmdyb3VwKCkNCg0KIyBXcml0ZSBkYXRhIA0Kd3JpdGVfY3N2KGdvYSwgaGVyZTo6aGVyZSgiZGF0YS9nb2FfdHJhY2tzLmNzdiIpKQ0KDQpgYGANCg0KIyMgRXhwbG9yZSBnb2EgZGF0YQ0KDQpGaXJzdCBsZXQncyBzZWUgaWYgdGhlcmUgaXMgZW5vdWdoIHZhcmlhYmlsaXR5IGluIHRoZSBmZWF0dXJlcy4gSSdtIHVzaW5nIGEgdHJpY2sgdG8gY2FsY3VsYXRlIGNvbmZpZGVuY2UgaW50ZXJ2YWxzIGZvciB0aGUgbWVhbnMgYnkgdXNpbmcgYSBvbmUtc2FtcGxlIHQudGVzdCB3aXRoIDAgYXMgcmVmZXJlbmNlIChjb3VsZCBiZSBhbnkgbnVtYmVyIGFzIHdlIGFyZSBub3QgaW50ZXJlc3RlZCBpbiB0aGUgcCB2YWx1ZSkuDQoNCmBgYHtyIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0V9DQojIFVzZSB0aGUgc2F2ZWQgZGF0YXNldCBzbyB3ZSBkb24ndCBoYXZlIHRvIHJlYXNzZW1sZSB0aGUgZGF0YS4NCmdvYSA8LSANCiAgcmVhZF9jc3YoaGVyZTo6aGVyZSgiZGF0YS9nb2FfdHJhY2tzLmNzdiIpKSAlPiUgDQogIHNlbGVjdChnZW5yZSwgdHJhY2tfbmFtZSwgZGFuY2VhYmlsaXR5OnRlbXBvKSAlPiUgDQogIG11dGF0ZShnZW5yZSA9IGZhY3RvcihnZW5yZSkpDQoNCiMgQmFsYW5jZSB0aGUgZGF0YSBieSBzYW1wbGluZyBleGFjdGx5IDEwIHRyYWNrcyBmcm9tIGVhY2ggZ2VucmUNCnNldC5zZWVkKDEpDQpnb2EgPC0NCiAgZ29hICU+JSANCiAgZ3JvdXBfYnkoZ2VucmUpICU+JSANCiAgc2FtcGxlX24oMTApICU+JSANCiAgdW5ncm91cCgpDQoNCiMgUHV0IGRhdGEgaW50byBsb25nIGZvcm1hdCwgY2FsY3VsYXRlIGNvbmZpZGVuY2UgaW50ZXJ2YWxzDQpnb2FfbG9uZyA8LSANCiAgZ29hICU+JSANCiAgcGl2b3RfbG9uZ2VyKGRhbmNlYWJpbGl0eTp0ZW1wbywNCiAgICAgICAgICAgICAgIG5hbWVzX3RvID0gImZlYXR1cmUiKSAlPiUgDQogIGdyb3VwX25lc3QoZ2VucmUsIGZlYXR1cmUpICU+JSANCiAgbXV0YXRlKHRfdGVzdCA9IG1hcChkYXRhLCANCiAgICAgICAgICAgICAgICAgICAgICB+dC50ZXN0KC54JHZhbHVlKSAlPiUgDQogICAgICAgICAgICAgICAgICAgICAgdGlkeSgpKSkgJT4lIA0KICB1bm5lc3QoYyh0X3Rlc3QsIGRhdGEpKQ0KICANCiMgRGlzdHJpYnV0aW9ucw0KZ29hX2xvbmcgJT4lIA0KICBnZ3Bsb3QoKSArDQogIGFlcyh4ID0gdmFsdWUsIGZpbGwgPSBmZWF0dXJlKSArDQogIGdlb21faGlzdG9ncmFtKHNob3cubGVnZW5kID0gRkFMU0UpICsNCiAgZmFjZXRfd3JhcCh+ZmVhdHVyZSwgc2NhbGVzID0gImZyZWUiKQ0KDQpgYGANCg0KTGV0J3Mgb3JkZXIgZ2VucmVzIGJ5IGZlYXR1cmVzLiBTb21lIGZlYXR1cmVzIHNlZW0gbGVnaXQgb24gZmFjZSB2YWxpZGl0eSwgYWx0aG91Z2ggSSBoYXZlIHNvbWUgZGlmZmljdWx0eSBpbiB0ZWxsaW5nIGFwYXJ0IHBzeWN0cmFuY2UgZ2VucmVzLiBBbHNvLCBJJ20gbm90IGZ1bGx5IGF3YXJlIG9mIHRoZSBtZWFuaW5nIG9mIGFsbCBmZWF0dXJlcy4NCg0KYGBge3IgZmlnLmhlaWdodD0xNSwgZmlnLndpZHRoPTEwfQ0KZ29hX2xvbmcgJT4lIA0KICBtdXRhdGUoZ2VucmUgPSByZW9yZGVyX3dpdGhpbihnZW5yZSwgZXN0aW1hdGUsIGZlYXR1cmUpKSAlPiUNCiAgZ2dwbG90KCkgKw0KICBhZXMoeCA9IGVzdGltYXRlLCB5ID0gZ2VucmUsIA0KICAgICAgeG1pbiA9IGNvbmYubG93LCB4bWF4ID0gY29uZi5oaWdoLA0KICAgICAgY29sb3IgPSBmZWF0dXJlKSAlPiUgDQogIGdlb21fcG9pbnRyYW5nZShzaXplID0gMC41LCBzaG93LmxlZ2VuZCA9IEZBTFNFKSArDQogIHNjYWxlX3lfcmVvcmRlcmVkKCkgKw0KICBmYWNldF93cmFwKH5mZWF0dXJlLCBzY2FsZXMgPSAiZnJlZSIpICsNCiAgbGFicyh0aXRsZSA9ICJBdmVyYWdlIGZlYXR1cmUgdmFsdWVzIGJ5IGdlbnJlICg5NSVDSSkiLA0KICAgICAgIHN1YnRpdGxlID0gIlNldmVyYWwgZmVhdHVyZXMgaGF2ZSBoaWdoIHZhcmlhYmlsaXR5IGFjcm9zcyBnZW5yZXMgdGhhdCBtYWtlIGZlYXR1cmVzIHBsYXVzaWJsZSB0byBjbGFzc2lmeSBnZW5yZXMuIiwNCiAgICAgICB4ID0gTlVMTCwgeSA9IE5VTEwpDQoNCmBgYA0KDQojIyBDbGFzc2lmeSBnb2Egc3ViZ2VucmVzIHVzaW5nIHJhbmRvbSBmb3Jlc3QNCg0KQ3JlYXRlIGEgcmFuZG9tIGZvcmVzdCBtb2RlbCBmb3IgZmluZGluZyB0aGUgZGVmaW5pbmcgZmVhdHVyZXMgZm9yIHRoZSBkaWZmZXJlbnQgc3ViZ2VucmVzLiBBcyB3ZSBoYXZlIGEgdmVyeSBsaXR0bGUgZGF0YXNldCwgd2UgYXJlIG5vdCBzcGxpdHRpbmcgaXQgaW50byB0cmFpbmluZyBhbmQgdGVzdCBzZXQsIGJ1dCB3ZSB1c2UgYm9vdHN0cmFwcyB0byBtYWtlIHRoZSBtb2RlbCBtb3JlIHJvYnVzdC4NCg0KQm9vdHN0cmFwIGRhdGEsIHNwZWNpZnkgbW9kZWwgYW5kIHdvcmtmbG93Lg0KDQpgYGB7cn0NCiMgQ3JlYXRlIGJvb3RzdHJhcHMNCnNldC5zZWVkKDEyMykNCmdvYV9ib290IDwtIGJvb3RzdHJhcHMoZ29hKQ0KDQojIFNwZWNpZnkgcmFuZG9tIGZvcmVzdCBtb2RlbA0KcmZfc3BlYyA8LSANCiAgcmFuZF9mb3Jlc3QodHJlZXMgPSAxMDAwKSAlPiUNCiAgc2V0X21vZGUoImNsYXNzaWZpY2F0aW9uIikgJT4lDQogIHNldF9lbmdpbmUoInJhbmdlciIsIGltcG9ydGFuY2UgPSAicGVybXV0YXRpb24iKQ0KDQojIENyZWF0ZSB3b3JrZmxvdw0KZ29hX3dmIDwtIA0KICB3b3JrZmxvdygpICU+JQ0KICBhZGRfZm9ybXVsYShnZW5yZSB+IC4gLXRyYWNrX25hbWUpICU+JSANCiAgYWRkX21vZGVsKHJmX3NwZWMpDQoNCmBgYA0KDQpUcmFpbiBtb2RlbCBhbmQgZXZhbHVhdGUuIEkgc2VlbXMgbGlrZSBhY2N1cmFjeSBpcyBwb29yLCBhbmQgc2Vuc2l0aXZpdHkgaXMgYWxzbyBub3QgZ3JlYXQsIHRoYXQgaXMgYSBjaGFyYWN0ZXJpc3RpYyBvZiBtdWx0aWNsYXNzIGNsYXNzaWZpY2F0aW9ucyAodG9vIG1hbnkgY2F0ZWdvcmllcyB0byBjaG9vc2UgZnJvbSkuIEluIG92ZXJhbGwsIHRoZSBtb2RlbCBzZWVtcyB0byBoYXZlIGEgOTAlIFJPQyBBVUMuDQoNCmBgYHtyLCBjYWNoZSA9IFRSVUV9DQpnb2FfcnMgPC0gDQogIGdvYV93ZiAlPiUgDQogIGZpdF9yZXNhbXBsZXMocmVzYW1wbGVzID0gZ29hX2Jvb3QsDQogICAgICAgICAgICAgICAgbWV0cmljcyA9IG1ldHJpY19zZXQocm9jX2F1YywgYWNjdXJhY3ksDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc3BlY2lmaWNpdHksIHNlbnNpdGl2aXR5KSwNCiAgICAgICAgICAgICAgICBjb250cm9sID0gY29udHJvbF9yZXNhbXBsZXModmVyYm9zZSA9IFRSVUUsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHNhdmVfcHJlZCA9IFRSVUUsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHNhdmVfd29ya2Zsb3cgPSBUUlVFKSkNCg0KZ29hX3JzICU+JSANCiAgY29sbGVjdF9tZXRyaWNzKCkNCmBgYA0KDQojIyBXaGljaCBnb2EgZ2VucmVzIGFyZSB0aGUgbW9zdCBkaWZmaWN1bHQgdG8gY2xhc3NpZnk/DQoNCkl0IHNlZW1zIGxpa2Ugc29tZSBzdHlsZXMgYXJlIG1vcmUgZGlmZmljdWx0IHRoYW4gb3RoZXJzIHRvIGNsYXNzaWZ5LiBBcyB0aGUgaW5kaXZpZHVhbCBST0MgY3VydmUgc2hvdyBHb2EgVHJhbmNlLCBQc3N5YnJlYWtzLCBQc3l0ZWNobm8gY2FuIGJlIGVhc2lseSBjb25mdXNlZCB3aXRoIG90aGVyIHN0eWxlcy4gSW4gY29udHJhc3QsIENoaWxsb3V0LCBIaS1UZWNoLCBhbmQgT2ZmYmVhdCBhcmUgZWFzaWVyIHRvIGlkZW50aWZ5Lg0KDQpgYGB7cn0NCmdvYV9ycyAlPiUgDQogIGNvbGxlY3RfcHJlZGljdGlvbnMoKSAlPiUNCiAgcm9jX2N1cnZlKGdlbnJlLCAucHJlZF9DaGlsbG91dDoucHJlZF9UcmliYWwpICU+JSANCiAgZ2dwbG90KCkgKw0KICBhZXMoeCA9IDEgLSBzcGVjaWZpY2l0eSwgeSA9IHNlbnNpdGl2aXR5LCBjb2xvciA9IC5sZXZlbCkgKw0KICBnZW9tX2FibGluZShsdHkgPSAyLCBjb2xvciA9ICJncmF5ODAiLCBzaXplID0gMSkgKw0KICBnZW9tX3BhdGgoc2hvdy5sZWdlbmQgPSBGQUxTRSwgYWxwaGEgPSAwLjYsIHNpemUgPSAxLjIpICsNCiAgZmFjZXRfd3JhcCh+LmxldmVsLCBuY29sID0gNSkgKw0KICBzY2FsZV9jb2xvcl92aXJpZGlzX2Qob3B0aW9uID0gInBsYXNtYSIpICsNCiAgc2NhbGVfeF9jb250aW51b3VzKGxhYmVscyA9IHNjYWxlczo6cGVyY2VudF9mb3JtYXQoKSkgKw0KICBzY2FsZV95X2NvbnRpbnVvdXMobGFiZWxzID0gc2NhbGVzOjpwZXJjZW50X2Zvcm1hdCgpKSArDQogIGxhYnModGl0bGUgPSAiSW5kaXZpZHVhbCBST0MgY3VydmVzIGZvciBlYWNoIGdvYSBnZW5yZS4iLA0KICAgICAgIHN1YnRpdGxlID0gIlNvbWUgZ2VucmVzIGFyZSBlYXNpZXIgdG8gY2xhc3NpZnkgdGhhbiBvdGhlcnMuIikNCmBgYA0KDQojIyBJbXBvcnRhbmNlIG9mIHRoZSBmZWF0dXJlcw0KDQpXZSBjYW4gZXZhbHVhdGUgdGhlIGltcG9ydGFuY2Ugb2YgdGhlIGZlYXR1cmVzLiBJdCBzZWVtcyBsaWtlIHRlbXBvIGlzIGJ5IGZhciB0aGUgbW9zdCBpbXBvcnRhbnQgZmVhdHVyZSwgZm9sbG93ZWQgYnkgZGFuY2VhYmlsaXR5IGFuZCBlbmVyZ3kuIE1vZGUgaXMgbm90IGVzcGVjaWFsbHkgaGVscGZ1bCAoSSdtIG5vdCBldmVuIHN1cmUgd2hhdCBpdCBtZWFucykuDQoNCmBgYHtyfQ0KcmZfc3BlYyAlPiUgDQogIHNldF9lbmdpbmUoInJhbmdlciIsIGltcG9ydGFuY2UgPSAicGVybXV0YXRpb24iKSAlPiUNCiAgZml0KGdlbnJlIH4gLiwgZGF0YSA9IGdvYSkgJT4lIA0KICB2aXAoZ2VvbSA9ICJjb2wiKSArDQogIGFlcyhmaWxsID0gVmFyaWFibGUpICsNCiAgc2NhbGVfZmlsbF92aXJpZGlzX2Qob3B0aW9uID0gInZpcmlkaXMiKSArDQogIGxhYnModGl0bGUgPSAiVmFyaWFibGUgaW1wb3J0YW5jZSBmb3IgY2xhc3NpZmljYXRpb24iKQ0KYGBgDQoNCiMjIEV4cGxvcmUgd2hpY2ggZ2VucmVzIGFyZSBlYXNpZXIgdG8gY29uZnVzZSB3aXRoIGVhY2ggb3RoZXINCg0KQmVjYXVzZSBvZiB0aGUgc2V2ZXJhbCBjbGFzc2VzIGFuZCB0aGUgc21hbGwgZGF0YXNldCwgdGhlIGNvbmZ1c2lvbiBtYXRyaXggc2hvd3MgdGhhdCB0aGVyZSBhcmUgb25seSBhIGZldyBnZW5yZXMgdGhhdCBhcmUgY29ycmVjdGx5IGNsYXNzaWZpZWQgYXQgbGVhc3QgaGFsZiBvZiB0aGUgdGltZS4gSXQgYWxzbyBzaG93cyB3aGF0IGdlbnJlcyBhcmUgdGhlIGVhc2llc3QgdG8gY29uZnVzZSB3aXRoIGVhY2ggb3RoZXIuDQoNCmBgYHtyfQ0KDQpnb2FfY29uZnVzaW9uIDwtDQogIGdvYV9ycyAlPiUNCiAgY29sbGVjdF9wcmVkaWN0aW9ucygpICU+JSANCiAgY29uZl9tYXQoZ2VucmUsIC5wcmVkX2NsYXNzKSANCg0KZ29hX2NvbmZfcGVyYyA8LQ0KICBnb2FfY29uZnVzaW9uJHRhYmxlICU+JSANCiAgYXNfdGliYmxlKCkgJT4lIA0KICBncm91cF9ieShUcnV0aCkgJT4lIA0KICBtdXRhdGUoYWxsID0gc3VtKG4pLA0KICAgICAgICAgcGVyYyA9IG4vYWxsKSANCg0KZ29hX2NvbmZfcGVyYyAlPiUgDQogIGdncGxvdCgpICsNCiAgYWVzKHggPSBUcnV0aCwgeSA9IFByZWRpY3Rpb24sIGZpbGwgPSBwZXJjLCANCiAgICAgIGxhYmVsID0gc2NhbGVzOjpwZXJjZW50KHBlcmMsIGFjY3VyYWN5ID0gMSkpICsNCiAgZ2VvbV90aWxlKHNob3cubGVnZW5kID0gRkFMU0UpICsNCiAgZ2VvbV90ZXh0KHNpemUgPSAzKSArDQogIHNjYWxlX2ZpbGxfdmlyaWRpc19jKG9wdGlvbiA9ICJwbGFzbWEiKSArDQogIGxhYnModGl0bGUgPSAiQ29uZnVzaW9uIG1hdHJpeCBmb3IgdGhlIHJhbmRvbSBmb3Jlc3QgbW9kZWwiLA0KICAgICAgIHN1YnRpdGxlID0gIlBlcmNlbnQgb2YgY29ycmVjdCBjbGFzc2lmaWNhdGlvbnMgaW4gdGhlIGdlbnJlIiwNCiAgICAgICB4ID0gIlRydXRoIikgKw0KICB0aGVtZShheGlzLnRleHQueCA9IGVsZW1lbnRfdGV4dChhbmdsZSA9IDkwLCB2anVzdCA9IDAuNSwgaGp1c3Q9MSkpDQoNCmBgYA0KDQpMZXQncyB2aXN1YWxpemUgaG93IGVhc3kgaXQgaXMgdG8gY29uZnVzZSBjZXJ0YWluIGdlbnJlcyB3aXRoIG90aGVycyBieSBidWlsZGluZyBhIG5ldHdvcmsgZ3JhcGghDQoNCmBgYHtyIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0V9DQoNCmdvYV9ncmFwaCA8LQ0KICBnb2FfY29uZl9wZXJjICU+JSANCiAgZmlsdGVyKFByZWRpY3Rpb24gIT0gVHJ1dGggJiBuID4gMCkgJT4lIA0KICBzZWxlY3QoUHJlZGljdGlvbiwgVHJ1dGgsIHBlcmMpICU+JSANCiAgZ3JhcGhfZnJvbV9kYXRhX2ZyYW1lKGRpcmVjdGVkID0gRkFMU0UpDQoNCmdvYV9ncmFwaCAlPiUgDQogIGdncmFwaCgpICsNCiAgZ2VvbV9lZGdlX2xpbmsoYWVzKGVkZ2Vfd2lkdGggPSBwZXJjKSwNCiAgICAgICAgICAgICAgICAgYWxwaGEgPSAuNCkgKw0KICBzY2FsZV9lZGdlX3dpZHRoKHJhbmdlID0gYyguMDEsIDMpKSArDQogIGdlb21fbm9kZV90ZXh0KGFlcyhsYWJlbCA9IG5hbWUsIA0KICAgICAgICAgICAgICAgICAgICAgc2l6ZSA9IGJldHdlZW5uZXNzKGdvYV9ncmFwaCksDQogICAgICAgICAgICAgICAgICAgICBjb2xvciA9IG5hbWUpLA0KICAgICAgICAgICAgICAgICBzaG93LmxlZ2VuZCA9IEZBTFNFKSArDQogIHNjYWxlX3NpemUocmFuZ2UgPSBjKDMsNikpICsNCiAgc2NhbGVfY29sb3JfdmlyaWRpc19kKG9wdGlvbiA9ICJwbGFzbWEiKSArDQogIGd1aWRlcyhmaWxsID0gRkFMU0UsIGFscGhhID0gRkFMU0UsIGVkZ2VfYWxwaGEgPSBGQUxTRSwgZWRnZV93aWR0aCA9IEZBTFNFKSArDQogIHRoZW1lX2dyYXBoKCkgKw0KICBsYWJzKHRpdGxlID0gIkNvbmZ1c2lvbiBtYXAgb2YgcHN5dHJhbmNlIGdlbnJlcyIsDQogICAgICAgc3VidGl0bGUgPSAiVGhlIHdpZGVyIHRoZSBjb25uZWN0aW9uIGJldHdlZW4gZ2VucmUgbmFtZXMsIHRoZSBlYXNpZXIgdG8gY29uZnVzZS5cblRoZSBzaXplIG9mIHRoZSBub2RlIHNob3dzIHRoZSBjZW50cmFsaXR5IChiZXR3ZWVubmVzcykiKQ0KDQpgYGANCg0KVGhhdCdzIGFsbCENCg0KIVtdKGh0dHBzOi8vbWVkaWEuZ2lwaHkuY29tL21lZGlhL2xjV2pIeEtvbTVTd00vZ2lwaHkuZ2lmKXt3aWR0aD0iMjQwIn0NCg==