From 1ef67ab1bb07d6f6a991f9e87253881fdcffca0b Mon Sep 17 00:00:00 2001 From: Peter Harrison Date: Tue, 23 Jan 2024 14:46:41 +0000 Subject: [PATCH 01/24] Rename files --- R/{har-2019.R => model-har-2019.R} | 0 R/{huron-1994.R => model-huron-1994.R} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename R/{har-2019.R => model-har-2019.R} (100%) rename R/{huron-1994.R => model-huron-1994.R} (100%) diff --git a/R/har-2019.R b/R/model-har-2019.R similarity index 100% rename from R/har-2019.R rename to R/model-har-2019.R diff --git a/R/huron-1994.R b/R/model-huron-1994.R similarity index 100% rename from R/huron-1994.R rename to R/model-huron-1994.R From 851c0d1f637b3f8789625d38f043700242988d30 Mon Sep 17 00:00:00 2001 From: Peter Harrison Date: Tue, 23 Jan 2024 21:55:00 +0000 Subject: [PATCH 02/24] Remove old packages --- DESCRIPTION | 23 +++-------------------- 1 file changed, 3 insertions(+), 20 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index c21aba1..2bbb928 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -12,15 +12,7 @@ LazyData: true Imports: Rdpack (>= 0.11.0), hrep (>= 0.10.0.9001), - bowl18 (>= 0.1.0), - corpdiss (>= 0.1.0.9000), - dycon (>= 0.2.0), - har18 (>= 0.1.0), - jl12 (>= 0.1.0), - parn88 (>= 0.2.0), - parn94 (>= 0.2.3), - stolz15 (>= 0.1.0), - wang13 (>= 0.1.1), + hht, checkmate (>= 1.9.4), magrittr (>= 1.5), purrr (>= 0.3.2), @@ -35,17 +27,8 @@ Suggests: knitr (>= 1.23), DT (>= 0.5), covr (>= 3.2.1) -RoxygenNote: 6.1.1 +RoxygenNote: 7.3.1 Remotes: - pmcharrison/hrep, - pmcharrison/bowl18, - pmcharrison/corpdiss, - pmcharrison/dycon, - pmcharrison/har18, - pmcharrison/jl12, - pmcharrison/parn88, - pmcharrison/parn94, - pmcharrison/stolz15, - pmcharrison/wang13 + pmcharrison/hrep VignetteBuilder: knitr Byte-Compile: yes From 99c933a5d7beba5ee254aa688afd1e4cf12cce97 Mon Sep 17 00:00:00 2001 From: Peter Harrison Date: Tue, 23 Jan 2024 21:55:16 +0000 Subject: [PATCH 03/24] Update exports --- NAMESPACE | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/NAMESPACE b/NAMESPACE index 9ea3e41..556871b 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1,9 +1,22 @@ # Generated by roxygen2: do not edit by hand +S3method(huron_1994,default) +S3method(huron_1994,int_vec) +S3method(huron_1994,pc_set) +S3method(roughness_wang,default) +S3method(roughness_wang,sparse_fr_spectrum) +export(demo_wang) export(har_19_composite_coef) +export(huron_1994) +export(huron_1994_weights) export(incon) export(incon_models) export(list_models) +export(roughness_wang) importFrom(magrittr,"%>%") +importFrom(stats,"approx") +importFrom(stats,"cor") +importFrom(stats,"fft") importFrom(tibble,tibble) +importFrom(utils,"capture.output") importFrom(zeallot,"%<-%") From fe272d717eb71ed09715098de09aa2535b467314 Mon Sep 17 00:00:00 2001 From: Peter Harrison Date: Tue, 23 Jan 2024 21:55:28 +0000 Subject: [PATCH 04/24] Rebuild doc --- man/har_19_composite.Rd | 5 ++--- man/har_19_composite_coef.Rd | 6 ++++-- man/incon.Rd | 10 ++++++++-- man/incon_models.Rd | 4 +++- 4 files changed, 17 insertions(+), 8 deletions(-) diff --git a/man/har_19_composite.Rd b/man/har_19_composite.Rd index 19e3461..f43afd6 100644 --- a/man/har_19_composite.Rd +++ b/man/har_19_composite.Rd @@ -1,11 +1,10 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/har-2019.R +% Please edit documentation in R/model-har-2019.R \name{har_19_composite} \alias{har_19_composite} \title{Composite consonance model} \usage{ -har_19_composite(x, chord_size = FALSE, num_harmonics = 11L, - roll_off = 1) +har_19_composite(x, chord_size = FALSE, num_harmonics = 11L, roll_off = 1) } \arguments{ \item{x}{Chord to analyse; passed to \code{\link{incon}}. diff --git a/man/har_19_composite_coef.Rd b/man/har_19_composite_coef.Rd index 08fe991..5f4afa7 100644 --- a/man/har_19_composite_coef.Rd +++ b/man/har_19_composite_coef.Rd @@ -1,10 +1,12 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/har-2019.R +% Please edit documentation in R/model-har-2019.R \docType{data} \name{har_19_composite_coef} \alias{har_19_composite_coef} \title{Regression coefficients} -\format{An object of class \code{tbl_df} (inherits from \code{tbl}, \code{data.frame}) with 5 rows and 2 columns.} +\format{ +An object of class \code{tbl_df} (inherits from \code{tbl}, \code{data.frame}) with 5 rows and 2 columns. +} \usage{ har_19_composite_coef } diff --git a/man/incon.Rd b/man/incon.Rd index a9abc5e..784b3ac 100644 --- a/man/incon.Rd +++ b/man/incon.Rd @@ -4,8 +4,14 @@ \alias{incon} \title{Simultaneous consonance} \usage{ -incon(x, model = "hutch_78_roughness", x_int = NULL, - num_harmonics = 11L, roll_off = 1, par = list()) +incon( + x, + model = "hutch_78_roughness", + x_int = NULL, + num_harmonics = 11L, + roll_off = 1, + par = list() +) } \arguments{ \item{x}{Chord to analyse. diff --git a/man/incon_models.Rd b/man/incon_models.Rd index ff38191..05a3d7d 100644 --- a/man/incon_models.Rd +++ b/man/incon_models.Rd @@ -4,7 +4,9 @@ \name{incon_models} \alias{incon_models} \title{Simultaneous consonance models} -\format{An object of class \code{tbl_df} (inherits from \code{tbl}, \code{data.frame}) with 17 rows and 8 columns.} +\format{ +An object of class \code{tbl_df} (inherits from \code{tbl}, \code{data.frame}) with 17 rows and 8 columns. +} \usage{ incon_models } From aa0bf23877503ac81340f36b6a2cbdf277cd5ba6 Mon Sep 17 00:00:00 2001 From: Peter Harrison Date: Tue, 23 Jan 2024 21:55:35 +0000 Subject: [PATCH 05/24] Update Huron1994 doc --- R/model-huron-1994.R | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/R/model-huron-1994.R b/R/model-huron-1994.R index 24bdb69..ba86c6d 100644 --- a/R/model-huron-1994.R +++ b/R/model-huron-1994.R @@ -1,21 +1,26 @@ +#' @export huron_1994 <- function(x) { UseMethod("huron_1994") } +#' @export huron_1994.default <- function(x) { x <- hrep::pc_set(x) huron_1994.pc_set(x) } +#' @export huron_1994.pc_set <- function(x) { x <- hrep::int_vec(x) huron_1994.int_vec(x) } +#' @export huron_1994.int_vec <- function(x) { sum(x * huron_1994_weights) } +#' @export huron_1994_weights <- c(- 1.428, - 0.582, 0.594, From 64d96118d97e2f368e9a622f5001092d0915063f Mon Sep 17 00:00:00 2001 From: Peter Harrison Date: Tue, 23 Jan 2024 21:55:42 +0000 Subject: [PATCH 06/24] Update imports --- R/imports.R | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/R/imports.R b/R/imports.R index bc27478..974c066 100644 --- a/R/imports.R +++ b/R/imports.R @@ -3,3 +3,11 @@ NULL #' @importFrom zeallot "%<-%" NULL + +#' @importFrom stats "approx" "cor" "fft" +NULL + +#' @importFrom utils "capture.output" +NULL + +`.` <- NULL From d91426a8485338f27fb0ee2f523a023c2f7f018c Mon Sep 17 00:00:00 2001 From: Peter Harrison Date: Tue, 23 Jan 2024 21:55:51 +0000 Subject: [PATCH 07/24] Add Wang13 --- R/model-wang13.R | 1038 ++++++++++++++++++++++++++++++++++ inst/REFERENCES.bib | 15 + man/demo_wang.Rd | 26 + man/roughness_wang.Rd | 95 ++++ tests/testthat/test-wang13.R | 27 + 5 files changed, 1201 insertions(+) create mode 100644 R/model-wang13.R create mode 100644 man/demo_wang.Rd create mode 100644 man/roughness_wang.Rd create mode 100644 tests/testthat/test-wang13.R diff --git a/R/model-wang13.R b/R/model-wang13.R new file mode 100644 index 0000000..5200a84 --- /dev/null +++ b/R/model-wang13.R @@ -0,0 +1,1038 @@ +approx2 <- function(x, y, new_x) { + assertthat::assert_that( + length(x) == length(y), + length(x) > 1 + ) + y <- y[order(x)] + x <- x[order(x)] + n <- length(x) + ifelse( + new_x < x[1], + y[1] + (new_x - x[1]) * (y[2] - y[1]) / (x[2] - x[1]), + ifelse( + new_x > x[n], + y[n] + (new_x - x[n]) * (y[n] - y[n - 1]) / (x[n] - x[n - 1]), + approx(x, y, xout = new_x)$y + ) + ) +} + +compile_detail <- function(x) { + x$spectrum_input <- data.frame(frequency_Hz = x$frequency_Hz, + level_dB = x$level_dB) + x$spectrum_after_ear_filtering <- data.frame(frequency_Hz = x$frequency_Hz, + level_dB = x$level_dB_filtered) + x[c( + "spectrum_input", + "spectrum_after_ear_filtering", + "channel_wave_forms", + "channel_envelopes", + "filtered_channel_envelopes", + "modulation_indices", + "phase_impact_factors", + "specific_roughnesses", + "total_roughness" + )] +} + +get_channel_sound_excitation_levels <- function(frequency_Hz, + level_dB_filtered) { + frequency_bark <- convert_Hz_to_bark(frequency_Hz) + lapply( + seq_len(47), + function(channel_num) { + get_channel_sound_excitation_level( + freq_Hz = frequency_Hz, + freq_bark = frequency_bark, + sound_intensity_level = level_dB_filtered, + channel_num = channel_num) + } + ) +} + +# @param freq_Hz Frequency in Hz (numeric vector) +# @param freq_bark Corresponding frequency in barks (numeric vector) +# @param sound_intensity_level Sound intensity level (numeric vector, matches with freq_Hz) +# @param channel_num Critical band filter number (should be a scalar integer between 1 and 47) +# @return Numeric vector giving the excitation level for the respective channel for each frequency listed in \code{freq_Hz} +get_channel_sound_excitation_level <- function(freq_Hz, + freq_bark, + sound_intensity_level, + channel_num) { + assertthat::assert_that( + is.numeric(freq_bark), is.numeric(sound_intensity_level), + length(freq_bark) == length(sound_intensity_level), + assertthat::is.scalar(channel_num), is.numeric(channel_num) + ) + channel_centre_bark <- 0.5 * channel_num + df <- data.frame( + freq_Hz = freq_Hz, + freq_bark = freq_bark, + sound_intensity_level = sound_intensity_level, + region = NA + ) + # Assign components to regions + df$region[0 <= df$freq_bark & freq_bark < channel_centre_bark - 0.5] <- "upper_slope" + df$region[channel_centre_bark - 0.5 <= freq_bark & + freq_bark <= channel_centre_bark + 0.5] <- "centre" + df$region[channel_centre_bark + 0.5 < freq_bark & + freq_bark <= 47] <- "lower_slope" + # Calculate SELs for components within the critical band + df$sound_excitation_level[df$region == "centre"] <- + df[df$region == "centre", ] %>% + (function(df) df$sound_intensity_level) + # ... and for components below the critical band... + df$sound_excitation_level[df$region == "upper_slope"] <- + df[df$region == "upper_slope", ] %>% + (function(df) { + df$sound_intensity_level + + ((channel_centre_bark + 0.5) - df$freq_bark) * + (- 24 - (230 / df$freq_Hz) + 0.2 * df$sound_intensity_level) + }) + # ... and for components above the critical band + df$sound_excitation_level[df$region == "lower_slope"] <- + df[df$region == "lower_slope", ] %>% + (function(df) { + df$sound_intensity_level + + ((channel_centre_bark - 0.5) - df$freq_bark) * 27 + }) + df$sound_excitation_level +} + +get_channel_wave_form <- function(excitation_levels, + frequency_Hz) { + # Excitation levels are in decibels, so we need to convert back + # to a linear scale. The units for this linear scale don't matter, + # because later on we normalise by the RMS of the waveform. + assertthat::assert_that( + all(frequency_Hz < 44000), + all(frequency_Hz >= 0.5) + ) + # This bit is inefficient for dense spectra + # (i.e. when frequency_Hz is long) + amplitudes <- 10 ^ (excitation_levels / 20) + x <- rep(0, times = 44000) + for (i in seq_along(frequency_Hz)) { + tmp_freq <- round(frequency_Hz[i]) + tmp_amp <- amplitudes[i] + x[tmp_freq] <- hrep::sum_amplitudes( + x[tmp_freq], + tmp_amp, + coherent = FALSE + ) + } + Re(fft(x, inverse = TRUE) / length(x)) +} + +get_channel_envelope <- function(x) { + hht::HilbertEnvelope(hht::HilbertTransform(x)) %>% + (function(x) x - mean(x)) # center it +} + +filter_channel_envelope <- function(i, channel_envelope) { + ft <- fft(channel_envelope) + ft.coef <- Mod(ft) + ft.freq <- seq(from = 0, to = 44000 - 1) + ft.freq <- ifelse(ft.freq >= 22000, # Nyquist frequency + 44000 - ft.freq, + ft.freq) + ft.coef_new <- ft.coef * envelope_weight(freq_Hz = abs(ft.freq), + channel_num = i) + waveform <- Re(fft(ft.coef_new, inverse = TRUE) / length(ft.coef_new)) + waveform <- waveform - mean(waveform) + waveform +} + +get_modulation_index <- function(filtered_channel_envelope, + channel_wave_form) { + sqrt(mean(filtered_channel_envelope ^ 2)) / + sqrt(mean(channel_wave_form ^ 2)) +} + +get_phase_impact_factor <- function(i, filtered_channel_envelopes) { + if (i == 1) { + cor( + filtered_channel_envelopes[[1]], + filtered_channel_envelopes[[2]] + ) ^ 2 + } else if (i == 47) { + cor( + filtered_channel_envelopes[[46]], + filtered_channel_envelopes[[47]] + ) ^ 2 + } else { + cor( + filtered_channel_envelopes[[i - 1]], + filtered_channel_envelopes[[i]] + ) * + cor( + filtered_channel_envelopes[[i]], + filtered_channel_envelopes[[i + 1]] + ) + } +} + +get_channel_weight <- function(channel_num) { + assertthat::assert_that( + is.numeric(channel_num), + all(round(channel_num) == channel_num), + all(channel_num > 0), + all(channel_num < 48) + ) + approx( + x = c(1, 5, 10, 17, 19, 24, 27, 47), + y = c(0.41, 0.82, 0.83, 1.12, 1.12, 1, 0.8, 0.58), + xout = channel_num + )$y +} + +# Get ear transmission coefficients +# +# Gets ear transmission coefficients according to Figure 3 of Wang et al. (2013). +# This is a linear approximation to the graph provided in the paper +# (the paper does not provide an equation for the curve, unfortunately). +# @param freq Numeric vector of frequencies +# @return Numeric vector of ear transmission coefficients. +# These coefficients describe a filter that can be applied to incoming spectra +# to simulate the filtering of the ear. +# \insertRef{Wang2013}{incon} +ear_transmission <- function(freq) { + log_freq <- log(freq, base = 10) + ifelse( + log_freq < 3.1, + 0, + ifelse( + log_freq > 4.25, + 30 + (log_freq - 4.25) * 100, + approx( + x = c(3.1, 3.4, 3.5, 4, 4.25), + y = c(0, -5, -5, 5, 30), + method = "linear", rule = 2, + xout = log_freq + )$y + ) + ) +} + +convert_Hz_to_bark <- function(freq_Hz) { + freq_kHz <- freq_Hz / 1000 + ifelse( + freq_kHz <= 1.5, + 11.82 * atan(1.21 * freq_kHz), + 5 * log(freq_kHz / 1.5) + 12.61 + ) +} + +get_critical_bandwidth <- function(freq_Hz) { + ifelse(freq_Hz < 500, 100, freq_Hz / 5) +} + +# Figure 5 of Wang et al. +envelope_weight <- function(freq_Hz, channel_num) { + assertthat::assert_that( + is.numeric(freq_Hz), + is.numeric(channel_num), assertthat::is.scalar(channel_num), + channel_num > 0, channel_num < 48, + round(channel_num) == channel_num + ) + if (channel_num < 5) { + approx2( + x = c(0, 20, 30, 150, 250, 350), + y = c(0, 0.8, 1, 0.475, 0.2, 0.02), + new_x = freq_Hz + ) + } else if (channel_num < 16) { + approx2( + x = c(0, 30, 55, 150, 400), + y = c(0, 0.8, 1, 0.675, 0.1), + new_x = freq_Hz + ) + } else if (channel_num < 21) { + approx2( + x = c(0, 50, 77, 165, 250, 400), + y = c(0, 0.85, 1, 0.82, 0.48, 0.225), + new_x = freq_Hz + ) + } else if (channel_num < 42) { + approx2( + x = c(0, 50, 77, 100, 250, 400), + y = c(0, 0.9, 1, 0.95, 0.48, 0.225), + new_x = freq_Hz + ) + } else { + approx2( + x = c(0, 50, 70, 85, 140, 400), + y = c(0, 0.95, 1, 0.955, 0.69, 0.225), + new_x = freq_Hz + ) + } +} + +get_specific_roughness <- function(channel_weight, + phase_impact_factor, + modulation_index, + include_phase_impact_factors) { + if (include_phase_impact_factors) { + (channel_weight * phase_impact_factor * modulation_index) ^ 2 + } else { + (channel_weight * modulation_index) ^ 2 + } +} + +play_channel_wave_forms <- function(channel_wave_forms, + channel_num, + scale_to_other_channels = TRUE, + ...) { + if (!requireNamespace("tuneR")) + stop("tuneR must be installed before continuing") + peak <- if (scale_to_other_channels) { + channel_wave_forms %>% + vapply(function(x) max(abs(x)), numeric(1)) %>% + max + } else { + max(abs(channel_wave_forms[[channel_num]])) + } + play_wave_form(x = channel_wave_forms[[channel_num]], + peak = peak, + ...) +} + + +play_wave_form <- function(x, + sample_rate = 44e3, + fade_samples = 1e3, + bit = 16, + peak = max(abs(x))) { + if (!requireNamespace("tuneR")) + stop("tuneR must be installed before continuing") + + if (length(x) > 2 * fade_samples) { + ind_1 <- seq(from = 1, length.out = fade_samples) + ind_2 <- seq(to = length(x), length.out = fade_samples) + x[ind_1] <- x[ind_1] * seq(from = 0, to = 1, length.out = fade_samples) + x[ind_2] <- x[ind_2] * seq(from = 1, to = 0, length.out = fade_samples) + } + + u <- x %>% + magrittr::divide_by(peak * 1.5) %>% + magrittr::multiply_by(2 ^ (bit - 1) - 1) %>% + round + tuneR::play(tuneR::Wave(u, samp.rate = sample_rate, bit = bit), "play") +} + + +play_sparse_spectrum <- function( + frequency, + amplitude, + seconds = 1, + sample_rate = 44e3, + ... +) { + sparse_spectrum_to_waveform(frequency, amplitude, seconds, sample_rate) %>% + play_wave_form(sample_rate = sample_rate, ...) +} + +# Note: this could be done more efficiently with ifft +sparse_spectrum_to_waveform <- function( + frequency, + amplitude, + seconds, + sample_rate +) { + x <- seq(from = 0, to = seconds, length.out = sample_rate * seconds) + y <- mapply( + function(freq, amplitude) { + sin(2 * pi * freq * x) * amplitude + }, + frequency, + amplitude + ) %>% rowSums +} + +plot_modulation_indices_wang <- function(modulation_indices, + theme = cowplot::theme_cowplot()) { + if (!requireNamespace("ggplot2")) + stop("ggplot2 must be installed before continuing") + assertthat::assert_that( + is.numeric(modulation_indices), + length(modulation_indices) == 47 + ) + df <- data.frame( + x = seq_along(modulation_indices), + y = modulation_indices + ) + ggplot2::ggplot(df, ggplot2::aes_string(x = "x", y = "y")) + + ggplot2::geom_line() + + ggplot2::scale_x_continuous("Channel number") + + ggplot2::scale_y_continuous("Modulation index") + + theme + + ggplot2::theme(aspect.ratio = 1) +} + +plot_phase_impact_factors_wang <- function(phase_impact_factors, + theme = cowplot::theme_cowplot()) { + if (!requireNamespace("ggplot2")) + stop("ggplot2 must be installed before continuing") + assertthat::assert_that( + is.numeric(phase_impact_factors), + length(phase_impact_factors) == 47 + ) + df <- data.frame( + x = seq_along(phase_impact_factors), + y = phase_impact_factors + ) + ggplot2::ggplot(df, ggplot2::aes_string(x = "x", y = "y")) + + ggplot2::geom_line() + + ggplot2::scale_x_continuous("Channel number") + + ggplot2::scale_y_continuous("Phase impact factor") + + theme + + ggplot2::theme(aspect.ratio = 1) +} + +plot_specific_roughnesses_wang <- function(specific_roughnesses, + theme = cowplot::theme_cowplot()) { + if (!requireNamespace("ggplot2")) + stop("ggplot2 must be installed before continuing") + assertthat::assert_that( + is.numeric(specific_roughnesses), + length(specific_roughnesses) == 47 + ) + df <- data.frame( + x = seq_along(specific_roughnesses), + y = specific_roughnesses + ) + ggplot2::ggplot(df, ggplot2::aes_string(x = "x", y = "y")) + + ggplot2::geom_line() + + ggplot2::scale_x_continuous("Channel number") + + ggplot2::scale_y_continuous("Specific roughness") + + theme + + ggplot2::theme(aspect.ratio = 1) +} + +plot_waveform <- function( + x, + sample_rate = 44000, + range_sec = c(0, 0.2), + theme = cowplot::theme_cowplot() +) { + if (!requireNamespace("ggplot2")) + stop("ggplot2 must be installed before continuing") + df <- data.frame( + t = seq(from = 0, by = 1 / sample_rate, length.out = length(x)), + x = x + ) %>% + (function(df) df[df$t >= range_sec[1] & df$t <= range_sec[2], ]) + ggplot2::ggplot(df, ggplot2::aes_string(x = "t", y = "x")) + + ggplot2::geom_line(alpha = 0.5) + + ggplot2::scale_x_continuous("Time (sec)") + + ggplot2::scale_y_continuous("Instantaneous amplitude") + + theme + + ggplot2::theme(aspect.ratio = 1) +} + +plot_filtered_input_spectrum <- function(analysis) { + frequency <- analysis$spectrum_after_ear_filtering$frequency_Hz + amplitude <- hrep::dB_to_amplitude( + analysis$spectrum_after_ear_filtering$level_dB, + 60 + ) + plot_sparse_spectrum(frequency, amplitude, range_Hz = NULL) +} + +plot_sparse_spectrum <- function( + frequency, + amplitude, + resolution_Hz = 1, + range_Hz = NULL, + theme = cowplot::theme_cowplot() +) { + if (!requireNamespace("ggplot2")) + stop("ggplot2 must be installed before continuing") + if (is.null(range_Hz)) range_Hz <- c(0, max(frequency)) + df <- data.frame(freq = round(frequency), + amp = amplitude) %>% + (function(df) df[df$freq >= range_Hz[1] & + df$freq <= range_Hz[2], ]) %>% + rbind( + data.frame(freq = seq(from = range_Hz[1], + to = range_Hz[2], + by = resolution_Hz), + amp = 0) + ) %>% + (function(df) { + df[order(df$freq), ] + }) + ggplot2::ggplot(df, ggplot2::aes_string(x = "freq", y = "amp")) + + ggplot2::geom_line() + + ggplot2::scale_x_continuous("Frequency (Hz)") + + ggplot2::scale_y_continuous("Amplitude") + + theme + + ggplot2::theme(aspect.ratio = 1) +} + +format_chord <- function(x) { + paste(x, collapse = " ") +} + +enter_new_chord <- function(text, state, num_harmonics) { + tryCatch({ + set_chord(hrep::pc_chord(text), + state) + }, error = function(e){ + message("Call: ", capture.output(e$call)) + message("Message: ", e$message) + shinyjs::alert("Invalid chord") + }) +} + +set_chord <- function(chord, state) { + state$chord <- chord + state$chord_img_src <- get_chord_url(chord) +} + +plot_input_spectrum <- function(analysis) { + frequency <- analysis$spectrum_input$frequency_Hz + amplitude <- hrep::dB_to_amplitude(analysis$spectrum_input$level_dB, 60) + plot_sparse_spectrum(frequency, amplitude, range_Hz = NULL) +} + +play_input_spectrum <- function(analysis) { + frequency <- analysis$spectrum_input$frequency_Hz + amplitude <- hrep::dB_to_amplitude(analysis$spectrum_input$level_dB, 60) + play_sparse_spectrum(frequency, amplitude) +} + +play_filtered_input_spectrum <- function(analysis) { + frequency <- analysis$spectrum_after_ear_filtering$frequency_Hz + amplitude <- hrep::dB_to_amplitude( + analysis$spectrum_after_ear_filtering$level_dB, 60 + ) + play_sparse_spectrum(frequency, amplitude) +} + +get_chord_url <- function(chord, type = "png") { + assertthat::assert_that( + assertthat::is.scalar(type), + is.character(type), + type %in% c("png", "mp3", "midi") + ) + pitch <- as.integer(hrep::pi_chord(chord)) + label <- paste(pitch, collapse = "_") + src <- "http://research.pmcharrison.com/studies/HarmonyDissonance/chords/piano/%s/%s.%s" %>% + sprintf(label, label, type) + src +} + +analyse_chord <- function(x, + include_phase_impact_factors, + fundamental_dB, + num_harmonics) { + spectrum <- hrep::sparse_fr_spectrum(hrep::pi_chord(x), + num_harmonics = num_harmonics) + + shiny::withProgress( + roughness_wang( + x = spectrum, + include_phase_impact_factors = include_phase_impact_factors, + detail = TRUE, + msg = function(n, N, msg) shiny::setProgress(n, msg) + ), + min = 0, max = 7 + ) +} + +shiny_ui_sidebar <- function() { + shinydashboard::dashboardSidebar( + shinyjs::useShinyjs(), + shiny::h4("Wang et al. (2013)", + style = "padding-left: 20px"), + shinydashboard::sidebarMenu( + lapply(c( + "About", + "Input spectrum", + "Filtered spectrum", + "Channel waveforms", + "Channel envelopes", + "Filtered channel envelopes", + "Modulation indices", + "Phase impact factors", + "Specific roughnesses" + ), function(title) { + shinydashboard::menuItem(title, + tabName = gsub(" ", "_", tolower(title)), + icon = NULL) + }), + shiny::uiOutput("total_roughness") + ) + ) +} + +shiny_ui_body <- function(opt) { + shinydashboard::dashboardBody( + shiny::fluidRow( + shiny::column(6, shiny_ui_tabs(opt)), + shiny::column(6, shiny_ui_input(opt)) + ) + ) +} + +shiny_ui_tabs <- function(opt) { + shinydashboard::tabItems( + shiny_ui_tab_0(), + shiny_ui_tab_1(opt), + shiny_ui_tab_2(opt), + shiny_ui_tab_3(opt), + shiny_ui_tab_4(), + shiny_ui_tab_5(), + shiny_ui_tab_6(), + shiny_ui_tab_7(), + shiny_ui_tab_8() + ) +} + +shiny_ui_tab_0 <- function() { + shinydashboard::tabItem( + tabName = "about", + shinydashboard::box( + # title = "Input spectrum", + title = "About", + status = "primary", + width = 12, + shiny::tags$p("This interactive app analyses the roughness of musical chords", + "using Wang et al.'s (2013) algorithm.", + "Use the tabs on the left-hand side of the screen", + "to navigate through the successive stages of the model."), + shiny::tags$p("Wang, Y. S., Shen, G. Q., Guo, H., Tang, X. L., & Hamade, T. (2013).", + "Roughness modelling based on human auditory perception for", + "sound quality evaluation of vehicle interior noise.", + shiny::tags$em("Journal of Sound and Vibration"), + "332(16), 3893-3904.", + shiny::tags$a(href = "https://doi.org/10.1016/j.jsv.2013.02.030") + ) + ) + ) +} + +shiny_ui_tab_1 <- function(opt) { + shinydashboard::tabItem( + tabName = "input_spectrum", + shinydashboard::box( + # title = "Input spectrum", + title = NULL, + status = "primary", + width = 12, + shiny::tags$p("The input to the model is an acoustic spectrum."), + shiny::div(shiny::plotOutput("plot_input_spectrum"), + style = "max-width: 100%"), + if (opt$audio) shiny::actionButton("play_input_spectrum", "Play") + ) + ) +} + +shiny_ui_tab_2 <- function(opt) { + shinydashboard::tabItem( + tabName = "filtered_spectrum", + shinydashboard::box( + # title = "Filtered spectrum", + title = NULL, + status = "primary", + width = 12, + shiny::tags$p("The spectrum is filtered by the outer and middle ear."), + shiny::plotOutput("plot_filtered_input_spectrum"), + if (opt$audio) shiny::actionButton("play_filtered_input_spectrum", "Play") + ) + ) +} + +shiny_ui_tab_3 <- function(opt) { + shinydashboard::tabItem( + tabName = "channel_waveforms", + shinydashboard::box( + title = NULL, + status = "primary", + width = 12, + shiny::tags$p("Different cochlear channels selectively filter", + "for different frequency ranges."), + shiny::plotOutput("plot_channel_wave_form"), + shiny::sliderInput("channel_wave_forms_channel_num", + "Channel number", + min = 1, max = 47, value = 25, step = 1), + if (opt$audio) shiny::fluidRow( + shiny::column(3, shiny::actionButton("play_channel_wave_form", "Play", + style = "text-align: center")), + shiny::column(9, shiny::checkboxInput("normalise_volume_across_channels", + "Normalise volume across channels?", + value = TRUE)) + ) + )) +} + +shiny_ui_tab_4 <- function() { + shinydashboard::tabItem( + tabName = "channel_envelopes", + shinydashboard::box( + # title = "Channel envelopes", + title = NULL, + status = "primary", + width = 12, + shiny::tags$p("Waveform envelopes are extracted", + "for each channel."), + shiny::plotOutput("plot_channel_envelope"), + shiny::sliderInput("channel_envelopes_channel_num", "Channel number", + min = 1, max = 47, value = 25, step = 1) + )) +} + +shiny_ui_tab_5 <- function() { + shinydashboard::tabItem( + tabName = "filtered_channel_envelopes", + shinydashboard::box( + # title = "Filtered channel envelopes", + title = NULL, + status = "primary", + width = 12, + shiny::tags$p("These amplitude modulations are filtered to prioritise", + "the modulation frequencies that contribute most towards roughness."), + shiny::plotOutput("plot_filtered_channel_envelope"), + shiny::sliderInput("filtered_channel_envelopes_channel_num", "Channel number", + min = 1, max = 47, value = 25, step = 1) + )) +} + +shiny_ui_tab_6 <- function() { + shinydashboard::tabItem( + tabName = "modulation_indices", + shinydashboard::box( + # title = "Modulation indices", + title = NULL, + status = "primary", + width = 12, + shiny::tags$p("The modulation index captures the magnitude of", + "amplitude modulation for each channel."), + shiny::plotOutput("plot_modulation_indices") + )) +} + +shiny_ui_tab_7 <- function() { + shinydashboard::tabItem( + tabName = "phase_impact_factors", + shinydashboard::box( + # title = "Phase impact factors", + title = NULL, + status = "primary", + width = 12, + shiny::tags$p("Phase impact factors capture the correlation between", + "the envelopes of adjacent channels.", + "According to Wang et al. (2013), higher correlations", + "yield greater roughness."), + shiny::plotOutput("plot_phase_impact_factors") + )) +} + +shiny_ui_tab_8 <- function() { + shinydashboard::tabItem( + tabName = "specific_roughnesses", + shinydashboard::box( + # title = "Specific roughnesses", + title = NULL, + status = "primary", + width = 12, + shiny::tags$p("The roughness contribution of each critical band is", + "estimated as a function of modulation index,", + "phase impact factor, and pitch height of the critical band.", + "These are then summed to give the total roughness value."), + shiny::plotOutput("plot_specific_roughnesses") + )) +} + + +shiny_ui_input <- function(opt) { + shinydashboard::box( + shiny::p("Enter a pitch-class set to analyse.", + "The first pitch class will be taken as the bass note."), + shiny::textInput("chord", label = NULL, placeholder = "e.g. 4 0 7"), + shiny::actionButton("enter_new_chord", "Enter"), + shiny::uiOutput("current_chord_text"), + shiny::uiOutput("current_chord_image", height = "auto"), + shiny::checkboxInput("include_phase_impact_factors", + "Include phase impact factors", + value = opt$default_include_phase_impact_factors), + shiny::sliderInput("num_harmonics", "Number of harmonics", + value = opt$default_num_harmonics, + min = 1, max = 15, step = 1), + style = "text-align: center" + ) +} + +#' Wang et al. (2013) interactive demo +#' +#' Launches an interactive demo of Wang et al.'s (2013) roughness model. +#' This function requires a few additional packages to be installed; +#' you will be notified if any of these packages are missing +#' once you run \code{demo_wang()}. +#' @param audio (Scalar logical) Whether to enable playback controls +#' (currently the playback controls don't work when the app is hosted +#' on a remote server). +#' @note The demo takes the form of an Shiny app +#' (\url{https://shiny.rstudio.com/}). +#' @references +#' \insertRef{Wang2013}{wang13} +#' @export +demo_wang <- function(audio = TRUE) { + pkg_suggest <- c("cowplot", "shiny", "shinyjs", "shinydashboard") + purrr::map_lgl(pkg_suggest, requireNamespace) %>% + { + if (any(!.)) { + stop("the following packages need to be installed before continuing: ", + paste(pkg_suggest[!.], collapse = ", ")) + } + } + + opt <- list( + default_chord = hrep::pc_chord(c(4, 0, 7)), + default_num_harmonics = 11, + default_include_phase_impact_factors = FALSE, + fundamental_dB = 60, + audio = audio + ) + + ui <- shinydashboard::dashboardPage( + shinydashboard::dashboardHeader(title = "Wang et al. (2013)", + disable = FALSE), + shiny_ui_sidebar(), + shiny_ui_body(opt) + ) + + server <- function(input, output) { + state <- shiny::reactiveValues( + chord = opt$default_chord, + analysis = NULL + ) + output$current_chord_text <- shiny::renderUI( + shiny::tags$p("Current chord: ", + shiny::tags$strong(as.character(state$chord))) + ) + output$current_chord_image <- shiny::renderUI( + shiny::img(src = get_chord_url(state$chord), + contentType = 'image/png', + alt = "Current chord", + style = paste("max-width: 150px;", + "border-style: solid;", + "border-width: 5px;", + "border-color: white")) + ) + shiny::observeEvent( + input$enter_new_chord, + enter_new_chord( + input$chord, + state) + ) + shiny::observe({ + state$analysis <- analyse_chord(x = state$chord, + input$include_phase_impact_factors, + opt$fundamental_dB, + input$num_harmonics) + }) + shiny::observeEvent(input$play_input_spectrum, { + play_input_spectrum(state$analysis) + }) + shiny::observeEvent(input$play_filtered_input_spectrum, { + play_filtered_input_spectrum(state$analysis) + }) + shiny::observeEvent(input$play_channel_wave_form, { + play_channel_wave_forms( + channel_wave_forms = state$analysis$channel_wave_forms, + channel_num = input$channel_wave_forms_channel_num, + scale_to_other_channels = input$normalise_volume_across_channels + ) + }) + + output$plot_input_spectrum <- shiny::renderPlot( + plot_input_spectrum(state$analysis)) + + output$plot_filtered_input_spectrum <- shiny::renderPlot( + plot_filtered_input_spectrum(state$analysis)) + + output$plot_channel_wave_form <- shiny::renderPlot( + plot_waveform( + state$analysis$channel_wave_forms[[input$channel_wave_forms_channel_num]], + range_sec = c(0, 0.05) + )) + output$plot_channel_envelope <- shiny::renderPlot( + plot_waveform( + state$analysis$channel_envelopes[[input$channel_envelopes_channel_num]], + range_sec = c(0, 0.05) + )) + output$plot_filtered_channel_envelope <- shiny::renderPlot( + plot_waveform( + state$analysis$filtered_channel_envelopes[[input$filtered_channel_envelopes_channel_num]], + range_sec = c(0, 0.05) + )) + output$plot_modulation_indices <- shiny::renderPlot({ + plot_modulation_indices_wang(state$analysis$modulation_indices) + }) + + output$plot_phase_impact_factors <- shiny::renderPlot( + plot_phase_impact_factors_wang(state$analysis$phase_impact_factors)) + + output$plot_specific_roughnesses <- shiny::renderPlot( + plot_specific_roughnesses_wang(state$analysis$specific_roughnesses)) + + output$total_roughness <- shiny::renderUI({ + x <- round(state$analysis$total_roughness, digits = 5) + shiny::showNotification(shiny::p("Roughness:", shiny::strong(x)), + type = "message", + duration = 120) + shiny::p("Output:", + shiny::strong(x), + style = "padding: 15px; font-size: 12pt") + }) + } + + # Run the application + shiny::shinyApp(ui = ui, server = server) +} + +#' Wang et al.'s (2013) roughness model +#' +#' Gets the roughness of a sonority according to the model of Wang et al. (2013). +#' @param x Object to analyse, +#' which will be coerced to an object of class +#' \code{\link[hrep]{sparse_fr_spectrum}}. +#' Various input types are possible: +#' * Numeric vectors will be treated as vectors of MIDI note numbers, +#' which will be expanded into their implied harmonics. +#' * A two-element list can be used to define a harmonic spectrum. +#' The first element should be a vector of frequencies in Hz, +#' the second a vector of amplitudes. +#' * The function also accepts classes from the \code{hrep} package, +#' such as produced by \code{\link[hrep]{pi_chord}()} and +#' \code{\link[hrep]{sparse_fr_spectrum}()}. +#' @param detail (Logical scalar) Whether to return detailed output information. +#' @param include_phase_impact_factors (Logical scalar) +#' Whether to include phase impact factors in roughness computation. +#' Set to \code{TRUE} to recover the original specifications of Wang et al. (2013). +#' However, disabling this feature (by leaving the parameter at \code{FALSE}) +#' seems to result in better estimation of perceptual consonance. +#' @param unit_amplitude_in_dB (Numeric scalar) +#' Determines the decibel level of a partial with amplitude 1. +#' When the input is a musical chord, +#' this will correspond to the decibel level of the fundamental frequencies +#' of each chord tone. +#' @param msg Function to be called to give progress updates. +#' This function should accept three arguments: +#' \code{n}, an integer identifying the current position in the pipeline, +#' \code{N}, an integer identifying the length of the pipeline, +#' and \code{msg}, a string providing a longer-format description +#' of the current position in the pipeline. +#' Pass \code{NULL} to disable progress updates. +#' @param ... Additional parameters to pass to +#' \code{\link[hrep]{sparse_fr_spectrum}}. +#' * \code{num_harmonics}: Number of harmonics to use when expanding +#' chord tones into their implied harmonics. +#' * \code{roll_off}: Rate of amplitude roll-off for the harmonics. +#' @return If \code{detail == FALSE}, a numeric vector of roughnesses, +#' otherwise a list containing detailed algorithm output. +#' @references +#' \insertRef{Wang2013}{wang13} +#' @note +#' This implementation is designed for sparse input spectra, that is, +#' spectra containing only a few (< 100) components. +#' @md +#' @rdname roughness_wang +#' @export +roughness_wang <- function( + x, + detail = FALSE, + include_phase_impact_factors = FALSE, + unit_amplitude_in_dB = 60, + msg = function(n, N, msg) + if (interactive()) + message(n, "/", N, ": ", msg), + ... +) { + UseMethod("roughness_wang") +} + +#' @rdname roughness_wang +#' @export +roughness_wang.default <- function( + x, + detail = FALSE, + include_phase_impact_factors = FALSE, + unit_amplitude_in_dB = 60, + msg = function(n, N, msg) + if (interactive()) + message(n, "/", N, ": ", msg), + ... +) { + x <- hrep::sparse_fr_spectrum(x, ...) + do.call(roughness_wang, as.list(environment())) +} + +#' @rdname roughness_wang +#' @export +roughness_wang.sparse_fr_spectrum <- function( + x, + detail = FALSE, + include_phase_impact_factors = FALSE, + unit_amplitude_in_dB = 60, + msg = function(n, N, msg) + if (interactive()) + message(n, "/", N, ": ", msg), + ... +) { + frequency_Hz <- hrep::freq(x) + level_dB <- hrep::amplitude_to_dB(hrep::amp(x), unit_amplitude_in_dB) + if (is.null(msg)) msg <- function(...) NULL + + msg(1, 6, "Ear transmission...") + level_dB_filtered <- level_dB - ear_transmission(frequency_Hz) + + msg(2, 6, "Channel excitation levels...") + channel_sound_excitation_levels <- get_channel_sound_excitation_levels( + frequency_Hz = frequency_Hz, + level_dB_filtered = level_dB_filtered + ) + + # is a list of numeric vectors corresponding to the + # y values of the waveforms for each channel for time in [0s, 1s]. + # The units of y is amplitude ratios relative to the reference sound amplitude. + msg(3, 6, "Channel waveforms...") + channel_wave_forms <- purrr::map(.x = channel_sound_excitation_levels, + .f = get_channel_wave_form, + frequency_Hz) + + # These are waveforms corresponding to the signal envelopes + msg(4, 6, "Channel envelopes...") + channel_envelopes <- purrr::map(.x = channel_wave_forms, + .f = get_channel_envelope) + + # The channel envelopes are filtered to account for the different roughness + # contributions of different modulation frequencies + msg(5, 6, "Filtering channel envelopes...") + filtered_channel_envelopes <- purrr::map2(.x = seq_along(channel_envelopes), + .y = channel_envelopes, + .f = filter_channel_envelope) + + msg(6, 6, "Computing roughness...") + modulation_indices <- purrr::map2_dbl(.x = filtered_channel_envelopes, + .y = channel_wave_forms, + .f = get_modulation_index) + + phase_impact_factors <- purrr::map_dbl(.x = seq_len(47), + .f = get_phase_impact_factor, + filtered_channel_envelopes) + + specific_roughnesses <- purrr::pmap_dbl(.l = list(get_channel_weight(seq_len(47)), + phase_impact_factors, + modulation_indices), + .f = get_specific_roughness, + include_phase_impact_factors) + + total_roughness <- 0.25 * sum(specific_roughnesses) + + if (detail) + compile_detail(as.list(environment())) else + total_roughness +} diff --git a/inst/REFERENCES.bib b/inst/REFERENCES.bib index 2c65852..33f2323 100644 --- a/inst/REFERENCES.bib +++ b/inst/REFERENCES.bib @@ -16,3 +16,18 @@ @article{Harrison2019 title = {{Instantaneous consonance in the perception and composition of Western music}}, year = {2019} } + +@article{Wang2013, +abstract = {In this paper, a roughness model, which is based on human auditory perception (HAP) and known as HAP-RM, is developed for the sound quality evaluation (SQE) of vehicle noise. First, the interior noise signals are measured for a sample vehicle and prepared for roughness modelling. The HAP-RM model is based on the process of sound transfer and perception in the human auditory system by combining the structural filtering function and nonlinear perception characteristics of the ear. The HAP-RM model is applied to the measured vehicle interior noise signals by considering the factors that affect hearing, such as the modulation and carrier frequencies, the time and frequency maskings and the correlations of the critical bands. The HAP-RM model is validated by jury tests. An anchor-scaled scoring method (ASM) is used for subjective evaluations in the jury tests. The verification results show that the novel developed model can accurately calculate vehicle noise roughness below 0.6 asper. Further investigation shows that the total roughness of the vehicle interior noise can mainly be attributed to frequency components below 12 Bark. The time masking effects of the modelling procedure enable the application of the HAP-RM model to stationary and nonstationary vehicle noise signals and the SQE of other sound-related signals in engineering problems.}, +author = {Wang, Y.S. and Shen, G.Q. and Guo, H. and Tang, X.L. and Hamade, T.}, +doi = {10.1016/j.jsv.2013.02.030}, +file = {:Users/peter/Dropbox/Academic/literature/Wang et al/Wang et al. - 2013 - Roughness modelling based on human auditory perception for sound quality evaluation of vehicle interior noise.pdf:pdf}, +journal = {Journal of Sound and Vibration}, +mendeley-groups = {Dissonance models/Roughness}, +number = {16}, +pages = {3893--3904}, +publisher = {Elsevier}, +title = {{Roughness modelling based on human auditory perception for sound quality evaluation of vehicle interior noise}}, +volume = {332}, +year = {2013} +} diff --git a/man/demo_wang.Rd b/man/demo_wang.Rd new file mode 100644 index 0000000..007e5a0 --- /dev/null +++ b/man/demo_wang.Rd @@ -0,0 +1,26 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/model-wang13.R +\name{demo_wang} +\alias{demo_wang} +\title{Wang et al. (2013) interactive demo} +\usage{ +demo_wang(audio = TRUE) +} +\arguments{ +\item{audio}{(Scalar logical) Whether to enable playback controls +(currently the playback controls don't work when the app is hosted +on a remote server).} +} +\description{ +Launches an interactive demo of Wang et al.'s (2013) roughness model. +This function requires a few additional packages to be installed; +you will be notified if any of these packages are missing +once you run \code{demo_wang()}. +} +\note{ +The demo takes the form of an Shiny app +(\url{https://shiny.rstudio.com/}). +} +\references{ +\insertRef{Wang2013}{wang13} +} diff --git a/man/roughness_wang.Rd b/man/roughness_wang.Rd new file mode 100644 index 0000000..a4a1d69 --- /dev/null +++ b/man/roughness_wang.Rd @@ -0,0 +1,95 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/model-wang13.R +\name{roughness_wang} +\alias{roughness_wang} +\alias{roughness_wang.default} +\alias{roughness_wang.sparse_fr_spectrum} +\title{Wang et al.'s (2013) roughness model} +\usage{ +roughness_wang( + x, + detail = FALSE, + include_phase_impact_factors = FALSE, + unit_amplitude_in_dB = 60, + msg = function(n, N, msg) if (interactive()) message(n, "/", N, ": ", msg), + ... +) + +\method{roughness_wang}{default}( + x, + detail = FALSE, + include_phase_impact_factors = FALSE, + unit_amplitude_in_dB = 60, + msg = function(n, N, msg) if (interactive()) message(n, "/", N, ": ", msg), + ... +) + +\method{roughness_wang}{sparse_fr_spectrum}( + x, + detail = FALSE, + include_phase_impact_factors = FALSE, + unit_amplitude_in_dB = 60, + msg = function(n, N, msg) if (interactive()) message(n, "/", N, ": ", msg), + ... +) +} +\arguments{ +\item{x}{Object to analyse, +which will be coerced to an object of class +\code{\link[hrep]{sparse_fr_spectrum}}. +Various input types are possible: +\itemize{ +\item Numeric vectors will be treated as vectors of MIDI note numbers, +which will be expanded into their implied harmonics. +\item A two-element list can be used to define a harmonic spectrum. +The first element should be a vector of frequencies in Hz, +the second a vector of amplitudes. +\item The function also accepts classes from the \code{hrep} package, +such as produced by \code{\link[hrep]{pi_chord}()} and +\code{\link[hrep]{sparse_fr_spectrum}()}. +}} + +\item{detail}{(Logical scalar) Whether to return detailed output information.} + +\item{include_phase_impact_factors}{(Logical scalar) +Whether to include phase impact factors in roughness computation. +Set to \code{TRUE} to recover the original specifications of Wang et al. (2013). +However, disabling this feature (by leaving the parameter at \code{FALSE}) +seems to result in better estimation of perceptual consonance.} + +\item{unit_amplitude_in_dB}{(Numeric scalar) +Determines the decibel level of a partial with amplitude 1. +When the input is a musical chord, +this will correspond to the decibel level of the fundamental frequencies +of each chord tone.} + +\item{msg}{Function to be called to give progress updates. +This function should accept three arguments: +\code{n}, an integer identifying the current position in the pipeline, +\code{N}, an integer identifying the length of the pipeline, +and \code{msg}, a string providing a longer-format description +of the current position in the pipeline. +Pass \code{NULL} to disable progress updates.} + +\item{...}{Additional parameters to pass to +\code{\link[hrep]{sparse_fr_spectrum}}. +\itemize{ +\item \code{num_harmonics}: Number of harmonics to use when expanding +chord tones into their implied harmonics. +\item \code{roll_off}: Rate of amplitude roll-off for the harmonics. +}} +} +\value{ +If \code{detail == FALSE}, a numeric vector of roughnesses, +otherwise a list containing detailed algorithm output. +} +\description{ +Gets the roughness of a sonority according to the model of Wang et al. (2013). +} +\note{ +This implementation is designed for sparse input spectra, that is, +spectra containing only a few (< 100) components. +} +\references{ +\insertRef{Wang2013}{wang13} +} diff --git a/tests/testthat/test-wang13.R b/tests/testthat/test-wang13.R new file mode 100644 index 0000000..6b32d27 --- /dev/null +++ b/tests/testthat/test-wang13.R @@ -0,0 +1,27 @@ +context("test-wang13") + +test_that("from legacy implementation", { + # C major + expect_equal( + roughness_wang(c(48, 64, 67)), + 0.67, + tolerance = 1e-2 + ) + expect_equal( + roughness_wang(c(48, 64, 67), include_phase_impact_factors = TRUE), + 0.11, + tolerance = 1e-2 + ) + + # C dim + expect_equal( + roughness_wang(c(48, 63, 66)), + 0.82, + tolerance = 1e-2 + ) + expect_equal( + roughness_wang(c(48, 63, 66), include_phase_impact_factors = TRUE), + 0.10, + tolerance = 1e-2 + ) +}) From e0dddf9efd442616bad4296144404622755cb18a Mon Sep 17 00:00:00 2001 From: Peter Harrison Date: Tue, 23 Jan 2024 22:08:48 +0000 Subject: [PATCH 08/24] Add bowl18 --- .gitignore | 1 + NAMESPACE | 5 + R/imports.R | 3 + R/model-bowl18.R | 227 +++++++++++++++++++++++++++ R/model-har-2019.R | 2 +- R/model-wang13.R | 4 +- R/models.R | 20 +-- R/top-level.R | 10 +- README.Rmd | 27 ---- README.md | 4 +- inst/bowling-data/README.txt | 2 + inst/bowling-data/dyads.csv | 13 ++ inst/bowling-data/tetrads.csv | 221 ++++++++++++++++++++++++++ inst/bowling-data/triads.csv | 67 ++++++++ man/bowl18_min_freq_dist.Rd | 41 +++++ man/demo_wang.Rd | 2 +- man/gill09_harmonicity.Rd | 49 ++++++ man/har_19_composite.Rd | 2 +- man/incon.Rd | 10 +- man/rational_scale.Rd | 23 +++ man/roughness_wang.Rd | 2 +- tests/testthat/test-bowl18.R | 164 +++++++++++++++++++ tests/testthat/test-par.R | 4 +- tests/testthat/testthat-problems.rds | Bin 0 -> 23477 bytes 24 files changed, 846 insertions(+), 57 deletions(-) create mode 100644 R/model-bowl18.R create mode 100644 inst/bowling-data/README.txt create mode 100644 inst/bowling-data/dyads.csv create mode 100644 inst/bowling-data/tetrads.csv create mode 100644 inst/bowling-data/triads.csv create mode 100644 man/bowl18_min_freq_dist.Rd create mode 100644 man/gill09_harmonicity.Rd create mode 100644 man/rational_scale.Rd create mode 100644 tests/testthat/test-bowl18.R create mode 100644 tests/testthat/testthat-problems.rds diff --git a/.gitignore b/.gitignore index c69928d..9f4e178 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ tests/testthat/cache/* www inst/shiny/cache .cache +.DS_Store diff --git a/NAMESPACE b/NAMESPACE index 556871b..99fcdad 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1,11 +1,15 @@ # Generated by roxygen2: do not edit by hand +S3method(bowl18_min_freq_dist,default) +S3method(bowl18_min_freq_dist,fr_chord) S3method(huron_1994,default) S3method(huron_1994,int_vec) S3method(huron_1994,pc_set) S3method(roughness_wang,default) S3method(roughness_wang,sparse_fr_spectrum) +export(bowl18_min_freq_dist) export(demo_wang) +export(gill09_harmonicity) export(har_19_composite_coef) export(huron_1994) export(huron_1994_weights) @@ -14,6 +18,7 @@ export(incon_models) export(list_models) export(roughness_wang) importFrom(magrittr,"%>%") +importFrom(methods,"is") importFrom(stats,"approx") importFrom(stats,"cor") importFrom(stats,"fft") diff --git a/R/imports.R b/R/imports.R index 974c066..5143559 100644 --- a/R/imports.R +++ b/R/imports.R @@ -4,6 +4,9 @@ NULL #' @importFrom zeallot "%<-%" NULL +#' @importFrom methods "is" +NULL + #' @importFrom stats "approx" "cor" "fft" NULL diff --git a/R/model-bowl18.R b/R/model-bowl18.R new file mode 100644 index 0000000..8172cb3 --- /dev/null +++ b/R/model-bowl18.R @@ -0,0 +1,227 @@ +#' Harmonicity +#' +#' Computes the harmonicity of a chord using the +#' percentage similarity measure of Gill & Purves (2009). +#' +#' @details +#' This percentage similarity measure corresponds to the percentage of the harmonics +#' that the chord holds in common with a harmonic series rooted on +#' the chord's fundamental frequency. +#' +#' While \insertCite{Gill2009;textual}{incon} originally presented this measure +#' to quantify the harmonicity of two-note chords (dyads) +#' \insertCite{Bowling2018;textual}{incon} subsequently demonstrated the application +#' of this measure to chords of arbitrary sizes. +#' +#' @note +#' The algorithm assumes that chord pitches are precisely +#' tuned to the just-tuned scale provided by Bowling et al. (2018). +#' This scale can be found at \code{\link{rational_scale}}. +#' +#' @param x Chord to analyse, +#' expressed as an integerish vector of MIDI note numbers, +#' or more generally as any valid input to \code{\link[hrep:pi_chord]{pi_chord}}. +#' @param tonic (Integerish scalar, default = 0L) +#' Tonic to use when defining the just-tuned scale. +#' @return (Numeric scalar) +#' The chord's harmonicity, defined as the proportion of harmonics +#' of the chord's fundamental frequency that coincide with the +#' harmonics of the chord tones. +#' @references +#' \insertRef{Bowling2018}{incon} +#' @examples +#' gill09_harmonicity(c(60, 64, 67)) +#' gill09_harmonicity("60 64 67") +#' gill09_harmonicity(hrep::pi_chord(c(60, 64, 67))) +#' @export +gill09_harmonicity <- function(x, tonic = 0L) { + checkmate::qassert(tonic, "X1") + x <- hrep::pi_chord(x) + chord <- rationalise_chord(x, tonic) + fundamental <- gcd(chord) + max_freq <- lcm(chord) + chord_harmonics <- expand_harmonics(chord, max_freq) + fundamental_harmonics <- expand_harmonics(fundamental, max_freq) + mean(fundamental_harmonics %in% chord_harmonics) +} + +#' Minimum frequency distance +#' +#' This function returns the minimum distance between +#' the fundamental frequencies of a chord, +#' after Bowling et al. (2018). +#' It makes no assumptions about the chord's tuning. +#' @param x Chord to analyse. +#' The default method assumes that the chord is expressed +#' as a numeric vector of frequencies. +#' Representations from the hrep package +#' (e.g. \code{\link[hrep]{pi_chord}()}) +#' can be used to analyse chords expressed as MIDI note numbers. +#' @return (Numeric scalar) +#' The minimum distance between the fundamental frequencies of the chord, +#' in Hz. +#' @references +#' \insertRef{Bowling2018}{incon} +#' @rdname bowl18_min_freq_dist +#' @examples +#' bowl18_min_freq_dist(c(220, 440, 560)) # processed as frequencies +#' bowl18_min_freq_dist(hrep::fr_chord(c(220, 440, 560))) # same as above +#' bowl18_min_freq_dist(hrep::pi_chord(c(60, 64, 67))) # MIDI note numbers +#' @export +bowl18_min_freq_dist <- function(x) { + UseMethod("bowl18_min_freq_dist") +} + +#' @rdname bowl18_min_freq_dist +#' @export +bowl18_min_freq_dist.default <- function(x) { + bowl18_min_freq_dist(hrep::fr_chord(x)) +} + +#' @rdname bowl18_min_freq_dist +#' @export +bowl18_min_freq_dist.fr_chord <- function(x) { + if (length(x) < 2L) + as.numeric(NA) else + min(diff(as.numeric(x))) +} + +expand_harmonics <- function(x, max_freq) { + UseMethod("expand_harmonics") +} + +expand_harmonics.rational_chord <- function(x, max_freq) { + purrr::map(seq_len(ncol(x)), + ~ expand_harmonics(fraction(x[, .]), max_freq)) %>% + Reduce(union, .) +} + +expand_harmonics.fraction <- function(x, max_freq) { + stopifnot(is(max_freq, "fraction")) + n <- 0L + res <- new.env() + while (TRUE) { + n <- n + 1L + harmonic <- phonTools::reduce.fraction(x * c(n, 1L)) + if ((harmonic[1] / harmonic[2]) > + (max_freq[1] / max_freq[2])) break + res[[as.character(n)]] <- as.character(harmonic) + } + res <- as.character(as.list(res)) + names(res) <- NULL + res +} + +fraction <- function(x, ...) { + UseMethod("fraction") +} + +fraction.numeric <- function(x, ...) { + checkmate::qassert(x, "X2") + class(x) <- "fraction" + x +} + +as.character.fraction <- function(x, ...) { + paste0(x[1], "/", x[2]) +} + +half <- function(x) { + UseMethod("half") +} + +half.fraction <- function(x) { + if (x[1] %% 2L == 0L) x[1] <- x[1] / 2L else x[2] <- x[2] * 2L + x +} + +double <- function(x) { + UseMethod("double") +} + +double.fraction <- function(x) { + if (x[2] %% 2L == 0L) x[2] <- x[2] / 2L else x[1] <- x[1] * 2L + x +} + +gcd <- function(x) { + UseMethod("gcd") +} + +gcd.rational_chord <- function(x) { + y <- c(numbers::mGCD(x[1, ]), + numbers::mLCM(x[2, ])) + fraction(y) +} + +lcm <- function(x) { + UseMethod("lcm") +} + +lcm.rational_chord <- function(x) { + y <- c(numbers::mLCM(x[1, ]), + numbers::mGCD(x[2, ])) + fraction(y) +} + +# These functions assume that we are in the key of C +# (i.e. pitch-class 0 is the tonic). + +rational_chord <- function(x) { + stopifnot(is.matrix(x), is.numeric(x), nrow(x) == 2L) + class(x) <- "rational_chord" + x +} + +rationalise_chord <- function(x, tonic) { + UseMethod("rationalise_chord") +} + +rationalise_chord.pi_chord <- function(x, tonic) { + x <- hrep::tp(x, - tonic) + octave <- floor(hrep::get_bass_pi(x) / 12) + x <- hrep::tp(x, - 12 * octave) + sapply(x, rationalise_pitch) %>% rational_chord +} + +rationalise_pitch <- function(x) { + checkmate::qassert(x, "X1") + octave <- floor(x / 12) + pitch_class <- x %% 12 + fraction <- rationalise_pitch_class(pitch_class) + while (octave != 0) { + if (octave < 0) { + fraction <- half(fraction) + octave <- octave + 1L + } else if (octave > 0) { + fraction <- double(fraction) + octave <- octave - 1L + } + } + fraction +} + +rationalise_pitch_class <- function(pc) { + checkmate::qassert(pc, "X1[0,12)") + fraction(rational_scale[, pc + 1L]) +} + +#' Rational scale +#' +#' This defines the rational scale used when computing harmonicity with +#' \code{\link{gill09_harmonicity}}. +#' It is a matrix with 2 rows and 12 columns, +#' where the first row corresponds to fraction numerators, +#' and the second row corresponds to fraction denominators. +#' Column i identifes the interval of size (i - 1) semitones. +#' For example, column 8 identifies the perfect fifth +#' (7 semitones) as a 3:2 ratio. +#' +#' @docType data +#' @keywords data +rational_scale <- matrix( + c(1, 16, 9, 6, 5, 4, 7, 3, 8, 5, 9, 15, + 1, 15, 8, 5, 4, 3, 5, 2, 5, 3, 5, 8), + nrow = 2, + byrow = TRUE +) diff --git a/R/model-har-2019.R b/R/model-har-2019.R index 709a35c..a9f8d3c 100644 --- a/R/model-har-2019.R +++ b/R/model-har-2019.R @@ -36,7 +36,7 @@ har_19_composite_coef <- tibble::tribble( #' provided in \code{\link{har_19_composite_coef}}, with one caveat: #' by default, the chord size effect is disabled, #' because it's thought that this effect came from a confound -#' in the perceptual data of \insertCite{Bowling2018;textual}{bowl18}. +#' in the perceptual data of \insertCite{Bowling2018;textual}{incon}. #' #' @param x Chord to analyse; passed to \code{\link{incon}}. #' All chord pitches must be integer-valued. diff --git a/R/model-wang13.R b/R/model-wang13.R index 5200a84..dc35e27 100644 --- a/R/model-wang13.R +++ b/R/model-wang13.R @@ -776,7 +776,7 @@ shiny_ui_input <- function(opt) { #' @note The demo takes the form of an Shiny app #' (\url{https://shiny.rstudio.com/}). #' @references -#' \insertRef{Wang2013}{wang13} +#' \insertRef{Wang2013}{incon} #' @export demo_wang <- function(audio = TRUE) { pkg_suggest <- c("cowplot", "shiny", "shinyjs", "shinydashboard") @@ -934,7 +934,7 @@ demo_wang <- function(audio = TRUE) { #' @return If \code{detail == FALSE}, a numeric vector of roughnesses, #' otherwise a list containing detailed algorithm output. #' @references -#' \insertRef{Wang2013}{wang13} +#' \insertRef{Wang2013}{incon} #' @note #' This implementation is designed for sparse input spectra, that is, #' spectra containing only a few (< 100) components. diff --git a/R/models.R b/R/models.R index b55bae3..fc9fb8b 100644 --- a/R/models.R +++ b/R/models.R @@ -64,12 +64,12 @@ add_model <- function(label, add_model("gill_09_harmonicity", "Gill & Purves (2009)", "Periodicity/harmonicity", - "bowl18", + "incon", consonance = TRUE, spectrum_sensitive = FALSE, continuous_pitch = FALSE, f = function(x, num_harmonics, roll_off, ...) - bowl18::gill09_harmonicity(x, ...)) + gill09_harmonicity(x, ...)) add_model("har_18_harmonicity", "Harrison & Pearce (2018)", @@ -135,12 +135,12 @@ add_model("stolz_15_periodicity", add_model("bowl_18_min_freq_dist", "Bowling et al. (2018)", "Interference", - "bowl18", + "incon", consonance = TRUE, spectrum_sensitive = FALSE, continuous_pitch = TRUE, f = function(x, num_harmonics, roll_off, ...) - bowl18::bowl18_min_freq_dist(x, ...)) + bowl18_min_freq_dist(x, ...)) add_model("huron_94_dyadic", "Huron (1994)", @@ -207,16 +207,16 @@ add_model("vass_01_roughness", add_model("wang_13_roughness", "Wang et al. (2013)", "Interference", - "wang13", + "incon", consonance = FALSE, spectrum_sensitive = TRUE, continuous_pitch = TRUE, f = function(x, num_harmonics, roll_off, ...) - wang13::roughness_wang(x, - num_harmonics = num_harmonics, - roll_off = roll_off, - msg = NULL, - ...)) + roughness_wang(x, + num_harmonics = num_harmonics, + roll_off = roll_off, + msg = NULL, + ...)) add_model("jl_12_tonal", "Johnson-Laird et al. (2012)", diff --git a/R/top-level.R b/R/top-level.R index bece0b4..508f14f 100644 --- a/R/top-level.R +++ b/R/top-level.R @@ -58,8 +58,8 @@ #' @details #' The following models are available: #' * `gill_09_harmonicity`: -#' the harmonicity model of \insertCite{Gill2009;textual}{bowl18} -#' (see \code{bowl18::\link[bowl18]{gill09_harmonicity}}). +#' the harmonicity model of \insertCite{Gill2009;textual}{incon} +#' (see \code{incon::\link[incon]{gill09_harmonicity}}). #' * `har_18_harmonicity`: #' the harmonicity model of \insertCite{Harrison2018;textual}{har18} #' (see \code{har18::\link[har18]{pc_harmonicity}}). @@ -78,8 +78,8 @@ #' (see \code{stolz15::\link[stolz15]{smooth_log_periodicity}}). #' * `bowl_18_min_freq_dist`: #' the minimum frequency distance feature of -#' \insertCite{Bowling2018;textual}{bowl18} -#' (see \code{bowl18::\link[bowl18]{bowl18_min_freq_dist}}). +#' \insertCite{Bowling2018;textual}{incon} +#' (see \code{incon::\link[incon]{bowl18_min_freq_dist}}). #' * `huron_94_dyadic`: #' aggregate dyadic consonance, after \insertCite{Huron1994;textual}{incon}. #' * `hutch_78_roughness`: @@ -96,7 +96,7 @@ #' (see \code{dycon::\link[dycon]{roughness_vass}}). #' * `wang_13_roughness`: #' the roughness model of \insertCite{Wang2013;textual}{wang13} -#' (see \code{wang13::\link[wang13]{roughness_wang}}). +#' (see \code{incon::\link[incon]{roughness_wang}}). #' * `jl_12_tonal`: #' the tonal dissonance model of \insertCite{Johnson-Laird2012;textual}{jl12} #' (see \code{jl12::\link[jl12]{jl_tonal_dissonance}}). diff --git a/README.Rmd b/README.Rmd index 00de05b..e7219f3 100644 --- a/README.Rmd +++ b/README.Rmd @@ -86,33 +86,6 @@ incon::incon_models %>% See `?incon` for more details. -## Packages - -The functionality of `incon` is split between several low-level R packages, -listed below. - -```{r, results = "asis", echo = FALSE} -tribble( - ~ Package, ~ DOI, - "bowl18", "10.5281/zenodo.2545741", - "corpdiss", "10.5281/zenodo.2545748", - "dycon", "10.5281/zenodo.2545750", - "har18", "10.5281/zenodo.2545752", - "hcorp", "10.5281/zenodo.2545754", - "hrep", "10.5281/zenodo.2545770", - "jl12", "10.5281/zenodo.2545756", - "parn88", "10.5281/zenodo.1491909", - "parn94", "10.5281/zenodo.2545759", - "stolz15", "10.5281/zenodo.2545762", - "wang13", "10.5281/zenodo.2545764", -) %>% - mutate(GitHub = paste("https://github.com/pmcharrison/", - Package, - sep = ""), - DOI = sprintf("[%s](https://doi.org/%s)", DOI, DOI)) %>% - kable -``` - ## Spectral representations Certain `incon` models can be applied to full frequency spectra rather than just diff --git a/README.md b/README.md index 89b133a..d0e4131 100644 --- a/README.md +++ b/README.md @@ -61,13 +61,13 @@ implemented: | Label | Citation | Class | Package | | :------------------------ | :---------------------------- | :---------------------- | :------- | -| gill\_09\_harmonicity | Gill & Purves (2009) | Periodicity/harmonicity | bowl18 | +| gill\_09\_harmonicity | Gill & Purves (2009) | Periodicity/harmonicity | incon | | har\_18\_harmonicity | Harrison & Pearce (2018) | Periodicity/harmonicity | har18 | | milne\_13\_harmonicity | Milne (2013) | Periodicity/harmonicity | har18 | | parn\_88\_root\_ambig | Parncutt (1988) | Periodicity/harmonicity | parn88 | | parn\_94\_complex | Parncutt & Strasburger (1994) | Periodicity/harmonicity | parn94 | | stolz\_15\_periodicity | Stolzenburg (2015) | Periodicity/harmonicity | stolz15 | -| bowl\_18\_min\_freq\_dist | Bowling et al. (2018) | Interference | bowl18 | +| bowl\_18\_min\_freq\_dist | Bowling et al. (2018) | Interference | incon | | huron\_94\_dyadic | Huron (1994) | Interference | incon | | hutch\_78\_roughness | Hutchinson & Knopoff (1978) | Interference | dycon | | parn\_94\_pure | Parncutt & Strasburger (1994) | Interference | parn94 | diff --git a/inst/bowling-data/README.txt b/inst/bowling-data/README.txt new file mode 100644 index 0000000..fd0b0af --- /dev/null +++ b/inst/bowling-data/README.txt @@ -0,0 +1,2 @@ +These datasets were constructed by reformatting the supplementary materials +in Bowling et al.'s (2018) paper. diff --git a/inst/bowling-data/dyads.csv b/inst/bowling-data/dyads.csv new file mode 100644 index 0000000..8346f50 --- /dev/null +++ b/inst/bowling-data/dyads.csv @@ -0,0 +1,13 @@ +name,pc_1,pc_2,f_1,f_2,rating_rank,rating_mean,rating_sd,rating_se,bowling_harm_sim,bowling_min_freq_int +minor 2nd,0,1,253.19,270.07,11,1.317,0.542,0.099,0.125,16.88 +major 2nd,0,2,246.24,277.02,9,1.837,0.653,0.119,0.222,30.78 +minor 3rd,0,3,237.85,285.41,6,2.693,0.538,0.098,0.333,47.56 +major 3rd,0,4,232.56,290.7,3,3.173,0.61,0.111,0.4,58.14 +perfect 4th,0,5,224.25,299.01,4,2.95,0.598,0.109,0.5,74.76 +tritone,0,6,218.03,305.24,8,1.953,0.395,0.072,0.314,87.21 +perfect 5th,0,7,209.3,313.96,2,3.447,0.591,0.108,0.667,104.66 +minor 6th,0,8,201.25,322.01,7,2.477,0.577,0.105,0.3,120.76 +major 6th,0,9,196.22,327.04,5,2.713,0.686,0.125,0.467,130.82 +minor 7th,0,10,186.88,336.38,10,1.753,0.359,0.066,0.289,149.5 +major 7th,0,11,182,341.26,10,1.753,0.541,0.099,0.183,159.26 +octave,0,12,174.42,348.84,1,3.893,0.274,0.05,1,174.42 \ No newline at end of file diff --git a/inst/bowling-data/tetrads.csv b/inst/bowling-data/tetrads.csv new file mode 100644 index 0000000..e2a83a9 --- /dev/null +++ b/inst/bowling-data/tetrads.csv @@ -0,0 +1,221 @@ +name,pc_1,pc_2,pc_3,pc_4,f_1,f_2,f_3,f_4,rating_rank,rating_mean,rating_sd,rating_se,bowling_harm_sim,bowling_min_freq_int +,0,1,2,3,238.3,254.18,268.08,285.96,63,1.400,0.621,0.113,0.027,13.90 +,0,1,2,4,235.61,251.32,265.07,294.52,60,1.500,0.630,0.115,0.027,13.75 +,0,1,2,5,231.28,246.69,260.18,308.37,54,1.700,0.837,0.153,0.025,13.49 +,0,1,2,6,227.92,243.11,256.41,319.08,57,1.600,0.724,0.132,0.027,13.30 +suspended 2nd + m2,0,1,2,7,223.06,237.93,250.94,334.59,61,1.467,0.571,0.104,0.024,13.01 +,0,1,2,8,218.4,232.96,245.7,349.45,61,1.467,0.571,0.104,0.024,12.74 +,0,1,2,9,215.41,229.77,242.33,359.01,62,1.433,0.568,0.104,0.025,12.56 +,0,1,2,10,209.65,223.63,235.86,377.38,68,1.233,0.504,0.092,0.026,12.23 +,0,1,2,11,206.55,220.32,232.37,387.28,72,1.033,0.183,0.033,0.025,12.05 +,0,1,2,12,201.58,215.02,226.77,403.15,71,1.100,0.403,0.074,0.022,11.75 +,0,1,3,4,231.7,247.15,278.04,289.63,63,1.400,0.675,0.123,0.051,11.59 +,0,1,3,5,227.5,242.67,273.01,303.34,50,1.833,0.592,0.108,0.189,15.17 +,0,1,3,6,224.25,239.2,269.11,313.96,58,1.567,0.728,0.133,0.194,14.95 +minor triad (r) + m2,0,1,3,7,219.55,234.19,263.46,329.32,49,1.867,0.730,0.133,0.093,14.64 +,0,1,3,8,215.04,229.37,258.05,344.06,45,2.000,0.695,0.127,0.175,14.33 +,0,1,3,9,212.13,226.27,254.56,353.55,58,1.567,0.626,0.114,0.189,14.14 +,0,1,3,10,206.55,220.32,247.86,371.79,65,1.333,0.547,0.100,0.179,13.77 +,0,1,3,11,203.54,217.11,244.24,381.63,64,1.367,0.615,0.112,0.024,13.57 +,0,1,3,12,198.71,211.95,238.45,397.41,57,1.600,0.855,0.156,0.164,13.24 +,0,1,4,5,225.06,240.06,281.32,300.08,51,1.800,0.761,0.139,0.048,15.00 +,0,1,4,6,221.88,236.67,277.35,310.63,59,1.533,0.730,0.133,0.050,14.79 +major triad (r) + m2,0,1,4,7,217.27,231.76,271.59,325.91,53,1.733,0.785,0.143,0.046,14.49 +,0,1,4,8,212.85,227.04,266.06,340.56,53,1.733,0.828,0.151,0.045,14.19 +,0,1,4,9,210,224,262.51,350.01,54,1.700,0.596,0.109,0.048,14.00 +,0,1,4,10,204.53,218.17,255.66,368.16,56,1.633,0.615,0.112,0.048,13.64 +,0,1,4,11,201.58,215.02,251.97,377.96,59,1.533,0.629,0.115,0.023,13.44 +,0,1,4,12,196.84,209.96,246.05,393.68,50,1.833,0.747,0.136,0.041,13.12 +,0,1,5,6,218.03,232.56,290.7,305.24,64,1.367,0.615,0.112,0.186,14.53 +suspended 4th + m2,0,1,5,7,213.58,227.81,284.77,320.36,45,2.000,0.695,0.127,0.086,14.23 +,0,1,5,8,209.3,223.26,279.07,334.89,41,2.133,0.507,0.093,0.167,13.96 +,0,1,5,9,206.55,220.32,275.4,344.25,54,1.700,0.466,0.085,0.170,13.77 +,0,1,5,10,201.25,214.67,268.34,362.26,44,2.033,0.556,0.102,0.178,13.42 +,0,1,5,11,198.39,211.62,264.52,371.99,52,1.767,0.728,0.133,0.023,13.23 +power chord + m2,0,1,5,12,193.8,206.72,258.4,387.6,36,2.300,0.877,0.160,0.150,12.92 +,0,1,6,7,210.71,224.76,294.99,316.06,63,1.400,0.563,0.103,0.091,14.05 +,0,1,6,8,206.55,220.32,289.17,330.48,54,1.700,0.750,0.137,0.175,13.77 +,0,1,6,9,203.87,217.46,285.41,339.78,62,1.433,0.504,0.092,0.186,13.59 +,0,1,6,10,198.71,211.95,278.19,357.67,49,1.867,0.571,0.104,0.185,13.24 +,0,1,6,11,195.92,208.98,274.28,367.34,53,1.733,0.691,0.126,0.024,13.06 +,0,1,6,12,191.44,204.2,268.01,382.87,54,1.700,0.750,0.137,0.161,12.76 +,0,1,7,8,202.55,216.06,303.83,324.08,60,1.500,0.509,0.093,0.082,13.51 +,0,1,7,9,199.97,213.3,299.96,333.29,65,1.333,0.479,0.088,0.086,13.33 +,0,1,7,10,195,208,292.51,351.01,48,1.900,0.759,0.139,0.088,13.00 +,0,1,7,11,192.32,205.14,288.47,360.59,56,1.633,0.490,0.089,0.022,12.82 +power chord + m2,0,1,7,12,188,200.53,282,376,52,1.767,0.774,0.141,0.074,12.53 +,0,1,8,9,196.22,209.3,313.96,327.04,63,1.400,0.563,0.103,0.167,13.08 +,0,1,8,10,191.44,204.2,306.3,344.59,46,1.967,0.718,0.131,0.168,12.76 +,0,1,8,11,188.85,201.44,302.15,354.09,57,1.600,0.563,0.103,0.022,12.59 +,0,1,8,12,184.68,196.99,295.49,369.36,34,2.367,0.964,0.176,0.142,12.31 +,0,1,9,10,189.13,201.74,315.22,340.43,68,1.233,0.430,0.079,0.178,12.61 +,0,1,9,11,186.6,199.04,311,349.88,69,1.200,0.484,0.088,0.023,12.44 +,0,1,9,12,182.53,194.7,304.22,365.07,54,1.700,0.702,0.128,0.150,12.17 +,0,1,10,11,182.27,194.42,328.08,341.75,65,1.333,0.479,0.088,0.023,12.15 +,0,1,10,12,178.38,190.28,321.09,356.77,51,1.800,0.714,0.130,0.153,11.90 +,0,1,11,12,176.13,187.87,330.25,352.26,63,1.400,0.621,0.113,0.020,11.74 +,0,2,3,4,228.75,257.34,274.5,285.93,55,1.667,0.711,0.130,0.074,11.43 +,0,2,3,5,224.66,252.74,269.59,299.54,45,2.000,0.788,0.144,0.025,16.85 +,0,2,3,6,221.49,249.17,265.78,310.08,54,1.700,0.596,0.109,0.073,16.61 +minor triad (r) + M2,0,2,3,7,216.9,244.01,260.27,325.34,25,2.667,0.994,0.182,0.067,16.26 +,0,2,3,8,212.49,239.05,254.99,339.99,42,2.100,0.885,0.162,0.069,15.94 +,0,2,3,9,209.65,235.86,251.58,349.42,51,1.800,0.610,0.111,0.024,15.72 +,0,2,3,10,204.2,229.72,245.04,367.56,53,1.733,0.640,0.117,0.067,15.32 +,0,2,3,11,201.25,226.41,241.5,377.35,54,1.700,0.651,0.119,0.069,15.09 +,0,2,3,12,196.53,221.1,235.84,393.06,54,1.700,0.596,0.109,0.061,14.74 +,0,2,4,5,222.27,250.05,277.84,296.36,34,2.367,0.718,0.131,0.117,18.52 +,0,2,4,6,219.17,246.56,273.96,306.83,37,2.267,0.640,0.117,0.072,27.39 +major triad (r) + M2,0,2,4,7,214.67,241.5,268.34,322.01,4,3.533,0.571,0.104,0.311,26.83 +,0,2,4,8,210.36,236.65,262.94,336.57,42,2.100,0.662,0.121,0.070,26.29 +,0,2,4,9,207.57,233.52,259.47,345.96,35,2.333,0.661,0.121,0.113,25.95 +,0,2,4,10,202.23,227.5,252.78,364.01,49,1.867,0.629,0.115,0.069,25.27 +,0,2,4,11,199.34,224.25,249.17,373.76,39,2.200,0.805,0.147,0.311,24.91 +,0,2,4,12,194.7,219.04,243.38,389.4,29,2.533,0.776,0.142,0.289,24.34 +,0,2,5,6,215.41,242.33,287.21,301.57,56,1.633,0.615,0.112,0.024,14.36 +mu chord,0,2,5,7,211.06,237.45,281.42,316.59,26,2.633,0.669,0.122,0.104,26.39 +,0,2,5,8,206.89,232.75,275.85,331.02,38,2.233,0.728,0.133,0.023,25.86 +,0,2,5,9,204.2,229.72,272.27,340.33,23,2.767,0.774,0.141,0.107,25.52 +,0,2,5,10,199.02,223.9,265.36,358.24,32,2.433,0.898,0.164,0.023,24.88 +,0,2,5,11,196.22,220.75,261.63,367.92,35,2.333,0.758,0.138,0.108,24.53 +power chord + M2,0,2,5,12,191.73,215.69,255.64,383.46,29,2.533,0.776,0.142,0.095,23.96 +suspended 2nd + tt,0,2,6,7,208.26,234.3,291.57,312.39,35,2.333,0.711,0.130,0.064,20.82 +,0,2,6,8,204.2,229.72,285.88,326.72,50,1.833,0.747,0.136,0.069,25.52 +,0,2,6,9,201.58,226.77,282.21,335.96,21,2.833,0.747,0.136,0.023,25.19 +,0,2,6,10,196.53,221.1,275.14,353.75,48,1.900,0.607,0.111,0.068,24.57 +,0,2,6,11,193.8,218.03,271.32,363.38,31,2.467,0.629,0.115,0.067,24.23 +,0,2,6,12,189.42,213.09,265.18,378.83,23,2.767,0.626,0.114,0.059,23.67 +suspended 2nd + m6,0,2,7,8,200.29,225.33,300.44,320.47,50,1.833,0.747,0.136,0.063,20.03 +suspended 2nd + M6,0,2,7,9,197.77,222.49,296.65,329.61,33,2.400,0.894,0.163,0.100,24.72 +suspended 2nd + m7,0,2,7,10,192.91,217.02,289.36,347.23,34,2.367,0.850,0.155,0.061,24.11 +suspended 2nd + M7,0,2,7,11,190.28,214.06,285.41,356.77,20,2.867,0.819,0.150,0.283,23.78 +suspended 2nd + Oct,0,2,7,12,186.05,209.3,279.07,372.1,13,3.100,0.662,0.121,0.250,23.25 +,0,2,8,9,194.1,218.36,310.56,323.5,63,1.400,0.563,0.103,0.022,12.94 +,0,2,8,10,189.42,213.09,303.06,340.95,40,2.167,0.791,0.145,0.067,23.67 +,0,2,8,11,186.88,210.24,299.01,350.4,40,2.167,0.648,0.118,0.065,23.36 +,0,2,8,12,182.8,205.65,292.48,365.6,32,2.433,0.774,0.141,0.057,22.85 +,0,2,9,10,187.16,210.55,311.93,336.88,59,1.533,0.571,0.104,0.022,23.39 +,0,2,9,11,184.68,207.77,307.8,346.28,43,2.067,0.691,0.126,0.104,23.09 +,0,2,9,12,180.69,203.28,301.16,361.39,27,2.600,0.724,0.132,0.091,22.59 +,0,2,10,11,180.43,202.99,324.78,338.31,62,1.433,0.626,0.114,0.063,13.53 +,0,2,10,12,176.63,198.71,317.93,353.26,42,2.100,0.712,0.130,0.056,22.08 +,0,2,11,12,174.42,196.22,327.04,348.84,41,2.133,0.681,0.124,0.261,21.80 +,0,3,4,5,218.78,262.54,273.48,291.71,61,1.467,0.571,0.104,0.046,10.94 +,0,3,4,6,215.78,258.93,269.72,302.09,54,1.700,0.750,0.137,0.137,10.79 +major triad (r) + m3,0,3,4,7,211.42,253.7,264.27,317.13,51,1.800,0.714,0.130,0.127,10.57 +,0,3,4,8,207.23,248.68,259.04,331.57,41,2.133,0.776,0.142,0.130,10.36 +,0,3,4,9,204.53,245.44,255.66,340.89,55,1.667,0.711,0.130,0.044,10.22 +,0,3,4,10,199.34,239.2,249.17,358.81,50,1.833,0.699,0.128,0.124,9.97 +,0,3,4,11,196.53,235.84,245.66,368.49,58,1.567,0.568,0.104,0.063,9.82 +,0,3,4,12,192.02,230.43,240.03,384.04,59,1.533,0.629,0.115,0.113,9.60 +,0,3,5,6,212.13,254.56,282.84,296.99,52,1.767,0.774,0.141,0.176,14.15 +minor triad (r) + P4,0,3,5,7,207.92,249.5,277.22,311.88,18,2.933,0.640,0.117,0.083,27.72 +,0,3,5,8,203.87,244.64,271.82,326.19,19,2.900,0.607,0.111,0.167,27.18 +,0,3,5,9,201.25,241.5,268.34,335.42,18,2.933,0.828,0.151,0.164,26.84 +,0,3,5,10,196.22,235.47,261.63,353.2,29,2.533,0.730,0.133,0.159,26.16 +,0,3,5,11,193.5,232.2,258,362.82,50,1.833,0.648,0.118,0.022,25.80 +power chord + m3,0,3,5,12,189.13,226.96,252.17,378.26,28,2.567,0.679,0.124,0.144,25.21 +minor triad (r) + tt,0,3,6,7,205.2,246.24,287.28,307.8,44,2.033,0.765,0.140,0.248,20.52 +,0,3,6,8,201.25,241.5,281.76,322.01,16,3.000,0.830,0.152,0.486,40.25 +dimished 7th chord,0,3,6,9,198.71,238.45,278.19,331.18,53,1.733,0.640,0.117,0.170,39.74 +half diminished 7th chord,0,3,6,10,193.8,232.56,271.32,348.84,27,2.600,0.932,0.170,0.467,38.76 +diminished (r) + M7,0,3,6,11,191.15,229.37,267.6,358.4,49,1.867,0.629,0.115,0.065,38.22 +diminished triad + oct,0,3,6,12,186.88,224.25,261.63,373.76,20,2.867,0.937,0.171,0.429,37.37 +minor triad (r) + m6,0,3,7,8,197.46,236.95,296.18,315.93,22,2.800,0.887,0.162,0.233,19.75 +minor triad (r) + M6,0,3,7,9,195,234,292.51,325.01,31,2.467,0.860,0.157,0.080,32.50 +minor 7th chord,0,3,7,10,190.28,228.33,285.41,342.5,14,3.067,0.868,0.159,0.222,38.05 +minor (r) + M7,0,3,7,11,187.72,225.26,281.57,351.97,47,1.933,0.740,0.135,0.060,37.54 +minor triad (r) + oct ,0,3,7,12,183.6,220.32,275.4,367.2,6,3.467,0.681,0.124,0.200,36.72 +,0,3,8,9,191.44,229.72,306.3,319.06,54,1.700,0.750,0.137,0.160,12.76 +,0,3,8,10,186.88,224.25,299.01,336.38,11,3.200,0.761,0.139,0.444,37.37 +,0,3,8,11,184.41,221.29,295.05,345.77,34,2.367,0.765,0.140,0.062,36.88 +,0,3,8,12,180.43,216.52,288.7,360.87,3,3.767,0.504,0.092,0.400,36.09 +,0,3,9,10,184.68,221.62,307.8,332.42,53,1.733,0.691,0.126,0.153,24.62 +,0,3,9,11,182.27,218.72,303.78,341.75,44,2.033,0.669,0.122,0.021,36.45 +,0,3,9,12,178.38,214.06,297.31,356.77,32,2.433,0.817,0.149,0.138,35.68 +,0,3,10,11,178.13,213.76,320.64,334,60,1.500,0.572,0.104,0.059,13.36 +,0,3,10,12,174.42,209.3,313.96,348.84,13,3.100,0.607,0.111,0.378,34.88 +,0,3,11,12,172.27,206.72,323,344.53,52,1.767,0.626,0.114,0.053,21.53 +,0,4,5,6,210,262.51,280.01,294.01,56,1.633,0.718,0.131,0.045,14.00 +major triad (r) + P4,0,4,5,7,205.87,257.34,274.5,308.81,22,2.800,0.805,0.147,0.197,17.16 +,0,4,5,8,201.9,252.38,269.2,323.04,49,1.867,0.629,0.115,0.043,16.82 +,0,4,5,9,199.34,249.17,265.78,332.23,16,3.000,0.695,0.127,0.200,16.61 +,0,4,5,10,194.4,243,259.2,349.92,45,2.000,0.947,0.173,0.042,16.20 +,0,4,5,11,191.73,239.66,255.64,359.49,42,2.100,0.923,0.168,0.099,15.98 +power chord + M3,0,4,5,12,187.44,234.3,249.92,374.87,30,2.500,0.777,0.142,0.175,15.62 +major triad (r) + tt,0,4,6,7,203.21,254.01,284.49,304.81,38,2.233,0.679,0.124,0.122,20.32 +,0,4,6,8,199.34,249.17,279.07,318.94,38,2.233,0.679,0.124,0.130,29.90 +,0,4,6,9,196.84,246.05,275.57,328.06,28,2.567,0.679,0.124,0.043,29.52 +dominant 7th flat 5,0,4,6,10,192.02,240.03,268.83,345.64,50,1.833,0.531,0.097,0.128,28.80 +,0,4,6,11,189.42,236.77,265.18,355.15,32,2.433,0.817,0.149,0.061,28.41 +augmented triad + oct,0,4,6,12,185.22,231.53,259.31,370.45,24,2.733,0.583,0.106,0.109,27.78 +major triad (r) + m6,0,4,7,8,195.61,244.51,293.42,312.98,48,1.900,0.481,0.088,0.118,19.56 +major triad (r) + M6,0,4,7,9,193.2,241.5,289.81,322.01,17,2.967,0.765,0.140,0.189,32.20 +dominant 7th chord,0,4,7,10,188.56,235.7,282.84,339.41,29,2.533,0.860,0.157,0.116,47.14 +major 7th chord,0,4,7,11,186.05,232.56,279.07,348.84,9,3.300,0.837,0.153,0.267,46.51 +major triad (r) + oct,0,4,7,12,182,227.5,273.01,364.01,1,3.833,0.592,0.108,0.467,45.50 +augmented triad + M6,0,4,8,9,189.7,237.13,303.52,316.17,55,1.667,0.661,0.121,0.042,12.65 +augmented triad + m7,0,4,8,10,185.22,231.53,296.36,333.4,54,1.700,0.702,0.128,0.124,37.04 +augmented triad + M7,0,4,8,11,182.8,228.5,292.48,342.75,46,1.967,0.490,0.089,0.059,45.70 +augmented triad + oct,0,4,8,12,178.89,223.62,286.23,357.78,34,2.367,0.718,0.131,0.105,44.73 +,0,4,9,10,183.06,228.83,305.11,329.52,58,1.567,0.626,0.114,0.041,24.41 +,0,4,9,11,180.69,225.87,301.16,338.8,25,2.667,0.758,0.138,0.094,37.64 +,0,4,9,12,176.88,221.1,294.79,353.75,10,3.233,0.728,0.133,0.167,44.22 +,0,4,10,11,176.63,220.78,317.93,331.18,54,1.700,0.702,0.128,0.058,13.25 +,0,4,10,12,172.98,216.22,311.36,345.96,31,2.467,0.681,0.124,0.102,34.60 +,0,4,11,12,170.86,213.58,320.36,341.72,8,3.333,0.479,0.088,0.233,21.36 +suspended 4th + tt,0,5,6,7,199.97,266.63,279.96,299.96,62,1.433,0.568,0.104,0.080,13.33 +,0,5,6,8,196.22,261.63,274.71,313.96,52,1.767,0.626,0.114,0.167,13.08 +,0,5,6,9,193.8,258.4,271.32,323,38,2.233,0.935,0.171,0.158,12.92 +,0,5,6,10,189.13,252.17,264.78,340.43,43,2.067,0.691,0.126,0.163,12.61 +,0,5,6,11,186.6,248.8,261.24,349.88,50,1.833,0.531,0.097,0.021,12.44 +,0,5,6,12,182.53,243.38,255.55,365.07,41,2.133,0.973,0.178,0.138,12.17 +suspended 4th + m6,0,5,7,8,192.61,256.81,288.92,308.18,43,2.067,0.740,0.135,0.078,19.26 +suspended 4th + M6,0,5,7,9,190.28,253.7,285.41,317.13,10,3.233,0.898,0.164,0.356,31.71 +suspended 4th + m7,0,5,7,10,185.77,247.7,278.66,334.39,24,2.733,0.828,0.151,0.076,30.96 +suspended 4th + M7,0,5,7,11,183.33,244.44,275,343.75,26,2.633,0.718,0.131,0.093,30.56 +suspended 4th + oct,0,5,7,12,179.4,239.2,269.11,358.81,5,3.500,0.572,0.104,0.306,29.91 +,0,5,8,9,186.88,249.17,299.01,311.46,53,1.733,0.785,0.143,0.153,12.45 +,0,5,8,10,182.53,243.38,292.05,328.56,28,2.567,0.626,0.114,0.159,36.51 +,0,5,8,11,180.18,240.23,288.28,337.83,47,1.933,0.583,0.106,0.021,48.05 +,0,5,8,12,176.38,235.17,282.21,352.76,7,3.400,0.621,0.113,0.133,47.04 +,0,5,9,10,180.43,240.58,300.72,324.78,38,2.233,0.858,0.157,0.150,24.06 +,0,5,9,11,178.13,237.51,296.89,334,33,2.400,0.770,0.141,0.094,37.11 +,0,5,9,12,174.42,232.56,290.7,348.84,2,3.800,0.610,0.111,0.600,58.14 +,0,5,10,11,174.18,232.24,313.52,326.58,61,1.467,0.507,0.093,0.020,13.06 +power chord + m7,0,5,10,12,170.63,227.5,307.13,341.26,12,3.133,0.629,0.115,0.130,34.13 +,0,5,11,12,168.57,224.76,316.06,337.13,39,2.200,0.664,0.121,0.082,21.07 +,0,6,7,8,190.28,266.39,285.41,304.44,44,2.033,0.669,0.122,0.233,19.02 +,0,6,7,9,188,263.2,282,313.33,43,2.067,0.640,0.117,0.077,18.80 +,0,6,7,10,183.6,257.04,275.4,330.48,52,1.767,0.728,0.133,0.229,18.36 +,0,6,7,11,181.22,253.7,271.82,339.78,38,2.233,0.817,0.149,0.058,18.12 + ,0,6,7,12,177.38,248.33,266.06,354.75,31,2.467,0.819,0.150,0.190,17.73 +,0,6,8,9,184.68,258.55,295.49,307.8,63,1.400,0.621,0.113,0.160,12.31 +,0,6,8,10,180.43,252.61,288.7,324.78,39,2.200,0.761,0.139,0.467,36.08 +,0,6,8,11,178.13,249.38,285.01,334,46,1.967,0.556,0.102,0.062,35.63 +,0,6,8,12,174.42,244.19,279.07,348.84,14,3.067,0.640,0.117,0.400,34.88 +,0,6,9,10,178.38,249.74,297.31,321.09,58,1.567,0.679,0.124,0.157,23.78 +,0,6,9,11,176.13,246.59,293.55,330.25,38,2.233,0.626,0.114,0.020,36.70 +,0,6,9,12,172.5,241.5,287.51,345.01,23,2.767,0.774,0.141,0.131,46.01 +,0,6,10,11,172.27,241.17,310.08,323,62,1.433,0.679,0.124,0.060,12.92 +,0,6,10,12,168.79,236.31,303.83,337.59,45,2.000,0.830,0.152,0.390,33.76 +,0,6,11,12,166.78,233.49,312.71,333.55,60,1.500,0.630,0.115,0.051,20.84 +,0,7,8,9,181.48,272.22,290.36,302.46,66,1.300,0.535,0.098,0.074,12.10 +,0,7,8,10,177.38,266.06,283.8,319.28,42,2.100,0.607,0.111,0.222,17.74 +,0,7,8,11,175.15,262.72,280.24,328.41,46,1.967,0.718,0.131,0.056,17.52 +power chord + m6,0,7,8,12,171.56,257.34,274.5,343.12,22,2.800,0.887,0.162,0.183,17.16 +,0,7,9,10,175.39,263.09,292.32,315.71,50,1.833,0.648,0.118,0.073,23.39 +,0,7,9,11,173.22,259.83,288.7,324.78,39,2.200,0.805,0.147,0.089,28.87 +power chord + M6,0,7,9,12,169.71,254.56,282.84,339.41,15,3.033,0.964,0.176,0.289,28.28 +,0,7,10,11,169.48,254.22,305.06,317.77,58,1.567,0.679,0.124,0.054,12.71 +power chord + m7,0,7,10,12,166.11,249.17,299.01,332.23,32,2.433,0.774,0.141,0.178,33.22 +power chord added M7,0,7,11,12,164.16,246.24,307.8,328.32,37,2.267,0.785,0.143,0.217,20.52 +,0,8,9,10,172.5,276.01,287.51,310.51,68,1.233,0.430,0.079,0.153,11.50 +,0,8,9,11,170.4,272.63,283.99,319.49,61,1.467,0.629,0.115,0.020,11.36 +,0,8,9,12,167,267.2,278.33,334,65,1.333,0.479,0.088,0.127,11.13 +,0,8,10,11,166.78,266.84,300.2,312.71,67,1.267,0.450,0.082,0.059,12.51 +,0,8,10,12,163.52,261.63,294.33,327.04,38,2.233,0.774,0.141,0.378,32.70 +,0,8,11,12,161.62,258.6,303.05,323.25,60,1.500,0.572,0.104,0.049,20.20 +,0,9,10,11,165.02,275.04,297.04,309.42,70,1.167,0.379,0.069,0.019,12.38 +,0,9,10,12,161.83,269.72,291.3,323.67,55,1.667,0.802,0.146,0.123,21.58 +,0,9,11,12,159.98,266.63,299.96,319.96,49,1.867,0.776,0.142,0.078,20.00 +,0,10,11,12,156.78,282.21,293.97,313.56,65,1.333,0.661,0.121,0.048,11.76 diff --git a/inst/bowling-data/triads.csv b/inst/bowling-data/triads.csv new file mode 100644 index 0000000..1bf1b02 --- /dev/null +++ b/inst/bowling-data/triads.csv @@ -0,0 +1,67 @@ +name,pc_1,pc_2,pc_3,f_1,f_2,f_3,rating_rank,rating_mean,rating_sd,rating_se,bowling_harm_sim,bowling_min_freq_int +,0,1,2,245.92,262.31,276.66,46,1.066666667,0.253708132,0.046320556,0.022,14.35 +,0,1,3,240.27,256.29,288.33,34,1.666666667,0.758098044,0.138409133,0.164,16.02 +,0,1,4,236.65,252.43,295.81,38,1.5,0.572351471,0.104496604,0.041,15.78 +,0,1,5,230.85,246.24,307.8,26,2.1,0.661763579,0.120820947,0.150,15.39 +Viennese trichord,0,1,6,226.41,241.5,316.97,32,1.8,0.610257153,0.111417203,0.161,15.09 +,0,1,7,220.06,234.73,330.09,40,1.4,0.498272879,0.090971765,0.074,14.67 +,0,1,8,214.06,228.33,342.5,39,1.466666667,0.507416263,0.092641111,0.142,14.27 +,0,1,9,210.24,224.25,350.4,42,1.266666667,0.449776445,0.082117568,0.150,14.01 +,0,1,10,202.99,216.52,365.38,45,1.166666667,0.379049022,0.069204567,0.153,13.53 +,0,1,11,199.13,212.4,373.36,44,1.2,0.484234198,0.088408664,0.020,13.27 +,0,1,12,193.01,205.87,386.01,39,1.466666667,0.571346464,0.104313115,0.125,12.86 +,0,2,3,236.06,265.56,283.27,36,1.6,0.723973709,0.13217891,0.061,17.71 +,0,2,4,232.56,261.63,290.7,14,2.6,0.813676204,0.148556271,0.289,29.07 +,0,2,5,226.96,255.33,302.61,12,2.7,0.794376789,0.145032695,0.095,28.37 +,0,2,6,222.66,250.5,311.73,19,2.366666667,0.6149479,0.112273612,0.059,27.84 +suspended 2nd,0,2,7,216.52,243.59,324.78,10,2.933333333,0.691491807,0.126248554,0.250,27.07 +,0,2,8,210.71,237.05,337.13,33,1.766666667,0.678910554,0.123951542,0.057,26.34 +,0,2,9,207,232.88,345.01,30,1.9,0.803011573,0.146609184,0.091,25.88 +,0,2,10,199.97,224.97,359.95,32,1.8,0.610257153,0.111417203,0.056,25.00 +,0,2,11,196.22,220.75,367.92,27,2.033333333,0.808716878,0.147650826,0.261,24.53 +,0,2,12,190.28,214.06,380.55,23,2.2,0.71438423,0.130428119,0.222,23.78 +,0,3,4,227.5,273.01,284.38,44,1.2,0.406838102,0.074278135,0.113,11.37 +,0,3,5,222.14,266.57,296.18,18,2.4,0.723973709,0.13217891,0.144,29.61 +dimished triad (r) ,0,3,6,218.03,261.63,305.24,19,2.366666667,0.718395402,0.131160456,0.429,43.60 +minor triad (r) ,0,3,7,212.13,254.56,318.2,6,3.4,0.674664668,0.123176352,0.200,42.43 +major triad (1),0,3,8,206.55,247.86,330.48,4,3.633333333,0.6149479,0.112273612,0.400,41.31 +dimished triad (1),0,3,9,202.99,243.59,338.31,22,2.233333333,0.678910554,0.123951542,0.138,40.60 +,0,3,10,196.22,235.47,353.2,15,2.533333333,0.730296743,0.133333333,0.378,39.25 +,0,3,11,192.61,231.13,361.15,37,1.533333333,0.571346464,0.104313115,0.053,38.52 +,0,3,12,186.88,224.25,373.76,11,2.8,0.924755326,0.168836451,0.333,37.37 +,0,4,5,219.04,273.8,292.05,25,2.133333333,0.776079152,0.141692019,0.175,18.25 +,0,4,6,215.04,268.8,301.05,30,1.9,0.547722558,0.1,0.109,32.25 +major triad (r) ,0,4,7,209.3,261.63,313.96,2,3.8,0.406838102,0.074278135,0.467,52.33 +augmented triad ,0,4,8,203.87,254.83,326.19,29,1.933333333,0.63968383,0.116789754,0.105,50.96 +minor triad (1),0,4,9,200.4,250.5,334,9,3.033333333,0.808716878,0.147650826,0.167,50.10 +Italian 6th,0,4,10,193.8,242.25,348.84,17,2.433333333,0.773854363,0.14128583,0.102,48.45 +,0,4,11,190.28,237.85,356.77,14,2.6,0.621455466,0.113461726,0.233,47.57 +,0,4,12,184.68,230.85,369.36,5,3.533333333,0.571346464,0.104313115,0.400,46.17 +,0,5,6,210.24,280.32,294.33,41,1.366666667,0.668675135,0.122082818,0.138,14.01 +suspended 4th,0,5,7,204.75,273.01,307.13,8,3.133333333,0.819307249,0.149584354,0.306,34.12 +minor triad (2),0,5,8,199.55,266.06,319.28,11,2.8,0.664363839,0.121295687,0.133,53.22 +major triad (2),0,5,9,196.22,261.63,327.04,4,3.633333333,0.764890496,0.13964926,0.600,65.41 +,0,5,10,189.89,253.19,341.81,19,2.366666667,0.668675135,0.122082818,0.130,63.30 +,0,5,11,186.51,248.68,349.7,20,2.3,0.651258728,0.118903032,0.082,62.17 +power chord,0,5,12,181.13,241.5,362.26,3,3.666666667,0.479463301,0.087537622,0.500,60.37 +,0,6,7,201.25,281.76,301.88,34,1.666666667,0.606478435,0.110727306,0.190,20.12 +,0,6,8,196.22,274.71,313.96,13,2.633333333,0.668675135,0.122082818,0.400,39.25 +dimished triad (2),0,6,9,193.01,270.21,321.68,22,2.233333333,0.626062316,0.114302818,0.131,51.47 +,0,6,10,186.88,261.63,336.38,29,1.933333333,0.827681987,0.151113365,0.390,74.75 +,0,6,11,183.6,257.04,344.25,30,1.9,0.711966679,0.129986737,0.051,73.44 +,0,6,12,178.38,249.74,356.77,19,2.366666667,0.850287308,0.155240513,0.314,71.36 +,0,7,8,191.44,287.15,306.3,31,1.833333333,0.530668631,0.096886393,0.183,19.15 +,0,7,9,188.37,282.56,313.96,16,2.5,0.82000841,0.149712368,0.289,31.40 +,0,7,10,182.53,273.8,328.56,24,2.166666667,0.912870929,0.166666667,0.178,54.76 +,0,7,11,179.4,269.11,336.38,21,2.266666667,0.827681987,0.151113365,0.217,67.27 +power chord,0,7,12,174.42,261.63,348.84,1,3.833333333,0.379049022,0.069204567,0.667,87.21 +,0,8,9,183.96,294.33,306.6,44,1.2,0.406838102,0.074278135,0.127,12.27 +,0,8,10,178.38,285.41,321.09,28,1.966666667,0.718395402,0.131160456,0.378,35.68 +,0,8,11,175.39,280.63,328.86,35,1.633333333,0.556053417,0.101521,0.049,48.23 +,0,8,12,170.63,273.01,341.26,7,3.2,0.71438423,0.130428119,0.300,68.25 +,0,9,10,175.72,292.87,316.3,43,1.233333333,0.430183067,0.078540323,0.123,23.43 +,0,9,11,172.82,288.03,324.04,37,1.533333333,0.507416263,0.092641111,0.078,36.01 +,0,9,12,168.19,280.32,336.38,9,3.033333333,0.889917987,0.162476052,0.467,56.06 +,0,10,11,167.89,302.2,314.8,44,1.2,0.550861394,0.100573071,0.048,12.60 +,0,10,12,163.52,294.33,327.04,32,1.8,0.664363839,0.121295687,0.289,32.71 +,0,11,12,161,301.88,322.01,44,1.2,0.406838102,0.074278135,0.183,20.13 \ No newline at end of file diff --git a/man/bowl18_min_freq_dist.Rd b/man/bowl18_min_freq_dist.Rd new file mode 100644 index 0000000..78d23d8 --- /dev/null +++ b/man/bowl18_min_freq_dist.Rd @@ -0,0 +1,41 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/model-bowl18.R +\name{bowl18_min_freq_dist} +\alias{bowl18_min_freq_dist} +\alias{bowl18_min_freq_dist.default} +\alias{bowl18_min_freq_dist.fr_chord} +\title{Minimum frequency distance} +\usage{ +bowl18_min_freq_dist(x) + +\method{bowl18_min_freq_dist}{default}(x) + +\method{bowl18_min_freq_dist}{fr_chord}(x) +} +\arguments{ +\item{x}{Chord to analyse. +The default method assumes that the chord is expressed +as a numeric vector of frequencies. +Representations from the hrep package +(e.g. \code{\link[hrep]{pi_chord}()}) +can be used to analyse chords expressed as MIDI note numbers.} +} +\value{ +(Numeric scalar) +The minimum distance between the fundamental frequencies of the chord, +in Hz. +} +\description{ +This function returns the minimum distance between +the fundamental frequencies of a chord, +after Bowling et al. (2018). +It makes no assumptions about the chord's tuning. +} +\examples{ +bowl18_min_freq_dist(c(220, 440, 560)) # processed as frequencies +bowl18_min_freq_dist(hrep::fr_chord(c(220, 440, 560))) # same as above +bowl18_min_freq_dist(hrep::pi_chord(c(60, 64, 67))) # MIDI note numbers +} +\references{ +\insertRef{Bowling2018}{incon} +} diff --git a/man/demo_wang.Rd b/man/demo_wang.Rd index 007e5a0..fbf1343 100644 --- a/man/demo_wang.Rd +++ b/man/demo_wang.Rd @@ -22,5 +22,5 @@ The demo takes the form of an Shiny app (\url{https://shiny.rstudio.com/}). } \references{ -\insertRef{Wang2013}{wang13} +\insertRef{Wang2013}{incon} } diff --git a/man/gill09_harmonicity.Rd b/man/gill09_harmonicity.Rd new file mode 100644 index 0000000..94e9def --- /dev/null +++ b/man/gill09_harmonicity.Rd @@ -0,0 +1,49 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/model-bowl18.R +\name{gill09_harmonicity} +\alias{gill09_harmonicity} +\title{Harmonicity} +\usage{ +gill09_harmonicity(x, tonic = 0L) +} +\arguments{ +\item{x}{Chord to analyse, +expressed as an integerish vector of MIDI note numbers, +or more generally as any valid input to \code{\link[hrep:pi_chord]{pi_chord}}.} + +\item{tonic}{(Integerish scalar, default = 0L) +Tonic to use when defining the just-tuned scale.} +} +\value{ +(Numeric scalar) +The chord's harmonicity, defined as the proportion of harmonics +of the chord's fundamental frequency that coincide with the +harmonics of the chord tones. +} +\description{ +Computes the harmonicity of a chord using the +percentage similarity measure of Gill & Purves (2009). +} +\details{ +This percentage similarity measure corresponds to the percentage of the harmonics +that the chord holds in common with a harmonic series rooted on +the chord's fundamental frequency. + +While \insertCite{Gill2009;textual}{incon} originally presented this measure +to quantify the harmonicity of two-note chords (dyads) +\insertCite{Bowling2018;textual}{incon} subsequently demonstrated the application +of this measure to chords of arbitrary sizes. +} +\note{ +The algorithm assumes that chord pitches are precisely +tuned to the just-tuned scale provided by Bowling et al. (2018). +This scale can be found at \code{\link{rational_scale}}. +} +\examples{ +gill09_harmonicity(c(60, 64, 67)) +gill09_harmonicity("60 64 67") +gill09_harmonicity(hrep::pi_chord(c(60, 64, 67))) +} +\references{ +\insertRef{Bowling2018}{incon} +} diff --git a/man/har_19_composite.Rd b/man/har_19_composite.Rd index f43afd6..79df26c 100644 --- a/man/har_19_composite.Rd +++ b/man/har_19_composite.Rd @@ -46,7 +46,7 @@ This model uses the regression coefficients provided in \code{\link{har_19_composite_coef}}, with one caveat: by default, the chord size effect is disabled, because it's thought that this effect came from a confound -in the perceptual data of \insertCite{Bowling2018;textual}{bowl18}. +in the perceptual data of \insertCite{Bowling2018;textual}{incon}. } \references{ \insertAllCited{} diff --git a/man/incon.Rd b/man/incon.Rd index 784b3ac..d59718a 100644 --- a/man/incon.Rd +++ b/man/incon.Rd @@ -67,8 +67,8 @@ according to various computational models. The following models are available: \itemize{ \item \code{gill_09_harmonicity}: -the harmonicity model of \insertCite{Gill2009;textual}{bowl18} -(see \code{bowl18::\link[bowl18]{gill09_harmonicity}}). +the harmonicity model of \insertCite{Gill2009;textual}{incon} +(see \code{incon::\link[incon]{gill09_harmonicity}}). \item \code{har_18_harmonicity}: the harmonicity model of \insertCite{Harrison2018;textual}{har18} (see \code{har18::\link[har18]{pc_harmonicity}}). @@ -87,8 +87,8 @@ after \insertCite{Stolzenburg2015;textual}{stolz15} (see \code{stolz15::\link[stolz15]{smooth_log_periodicity}}). \item \code{bowl_18_min_freq_dist}: the minimum frequency distance feature of -\insertCite{Bowling2018;textual}{bowl18} -(see \code{bowl18::\link[bowl18]{bowl18_min_freq_dist}}). +\insertCite{Bowling2018;textual}{incon} +(see \code{incon::\link[incon]{bowl18_min_freq_dist}}). \item \code{huron_94_dyadic}: aggregate dyadic consonance, after \insertCite{Huron1994;textual}{incon}. \item \code{hutch_78_roughness}: @@ -105,7 +105,7 @@ the roughness model of \insertCite{Vassilakis2001;textual}{dycon} (see \code{dycon::\link[dycon]{roughness_vass}}). \item \code{wang_13_roughness}: the roughness model of \insertCite{Wang2013;textual}{wang13} -(see \code{wang13::\link[wang13]{roughness_wang}}). +(see \code{incon::\link[incon]{roughness_wang}}). \item \code{jl_12_tonal}: the tonal dissonance model of \insertCite{Johnson-Laird2012;textual}{jl12} (see \code{jl12::\link[jl12]{jl_tonal_dissonance}}). diff --git a/man/rational_scale.Rd b/man/rational_scale.Rd new file mode 100644 index 0000000..52c8320 --- /dev/null +++ b/man/rational_scale.Rd @@ -0,0 +1,23 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/model-bowl18.R +\docType{data} +\name{rational_scale} +\alias{rational_scale} +\title{Rational scale} +\format{ +An object of class \code{matrix} (inherits from \code{array}) with 2 rows and 12 columns. +} +\usage{ +rational_scale +} +\description{ +This defines the rational scale used when computing harmonicity with +\code{\link{gill09_harmonicity}}. +It is a matrix with 2 rows and 12 columns, +where the first row corresponds to fraction numerators, +and the second row corresponds to fraction denominators. +Column i identifes the interval of size (i - 1) semitones. +For example, column 8 identifies the perfect fifth +(7 semitones) as a 3:2 ratio. +} +\keyword{data} diff --git a/man/roughness_wang.Rd b/man/roughness_wang.Rd index a4a1d69..3f5305c 100644 --- a/man/roughness_wang.Rd +++ b/man/roughness_wang.Rd @@ -91,5 +91,5 @@ This implementation is designed for sparse input spectra, that is, spectra containing only a few (< 100) components. } \references{ -\insertRef{Wang2013}{wang13} +\insertRef{Wang2013}{incon} } diff --git a/tests/testthat/test-bowl18.R b/tests/testthat/test-bowl18.R new file mode 100644 index 0000000..f4395a1 --- /dev/null +++ b/tests/testthat/test-bowl18.R @@ -0,0 +1,164 @@ +context("test-gill09_harmonicity") + +test_that("regression tests: dyads", { + df <- read.csv(system.file("bowling-data/dyads.csv", package = "incon"), + stringsAsFactors = FALSE) + chords <- purrr::pmap(list(df$pc_1, df$pc_2), ~ hrep::pi_chord(c(..1, ..2))) + + for (i in seq_along(chords)) { + expect_equal(df$bowling_harm_sim[i], + gill09_harmonicity(chords[[i]]), + tolerance = 1e-3) + } +}) + +test_that("regression tests: triads", { + df <- read.csv(system.file("bowling-data/triads.csv", package = "incon"), + stringsAsFactors = FALSE) + chords <- purrr::pmap(list(df$pc_1, df$pc_2, df$pc_3), + ~ hrep::pi_chord(c(..1, ..2, ..3))) + + set.seed(1) + ind <- sample(length(chords), size = 20L) + for (i in ind) { + expect_equal(df$bowling_harm_sim[i], + gill09_harmonicity(chords[[i]]), + tolerance = 1e-3) + } +}) + +test_that("regression tests: tetrads", { + df <- read.csv(system.file("bowling-data/tetrads.csv", package = "incon"), + stringsAsFactors = FALSE) + chords <- purrr::pmap(list(df$pc_1, df$pc_2, df$pc_3, df$pc_4), + ~ hrep::pi_chord(c(..1, ..2, ..3, ..4))) + + set.seed(1) + ind <- sample(length(chords), size = 20L) + for (i in ind) { + expect_equal(df$bowling_harm_sim[i], + gill09_harmonicity(chords[[i]]), + tolerance = 1e-3) + } +}) + +context("test-bowl18_min_freq_dist") + +test_that("regression tests: dyads", { + df <- read.csv(system.file("bowling-data/dyads.csv", package = "incon"), + stringsAsFactors = FALSE) + fr <- purrr::pmap(list(df$f_1, df$f_2), + ~ hrep::fr_chord(c(..1, ..2))) + min_fr_int_old <- df$bowling_min_freq_int + min_fr_int_new <- purrr::map_dbl(fr, bowl18_min_freq_dist) + + for (i in seq_along(fr)) { + expect_equal(min_fr_int_old[i], + min_fr_int_new[i]) + } +}) + +test_that("regression tests: triads", { + df <- read.csv(system.file("bowling-data/triads.csv", package = "incon"), + stringsAsFactors = FALSE) + fr <- purrr::pmap(list(df$f_1, df$f_2, df$f_3), + ~ hrep::fr_chord(c(..1, ..2, ..3))) + min_fr_int_old <- df$bowling_min_freq_int + min_fr_int_new <- purrr::map_dbl(fr, bowl18_min_freq_dist) + + for (i in seq_along(fr)) { + expect_equal(min_fr_int_old[i], + min_fr_int_new[i]) + } +}) + +test_that("regression tests: tetrads", { + df <- read.csv(system.file("bowling-data/tetrads.csv", package = "incon"), + stringsAsFactors = FALSE) + fr <- purrr::pmap(list(df$f_1, df$f_2, df$f_3, df$f_4), + ~ hrep::fr_chord(c(..1, ..2, ..3, ..4))) + min_fr_int_old <- df$bowling_min_freq_int + min_fr_int_new <- purrr::map_dbl(fr, bowl18_min_freq_dist) + + for (i in seq_along(fr)) { + expect_equal(min_fr_int_old[i], + min_fr_int_new[i]) + } +}) + +context("test-gcd") + +test_that("examples", { + x <- rational_chord(matrix(c(4, 5, 6, + 1, 1, 1), + nrow = 2, byrow = TRUE)) + expect_equal(gcd(x), fraction(c(1, 1))) + + x <- rational_chord(matrix(c(3, 4, 5, + 2, 3, 4), + nrow = 2, byrow = TRUE)) + + expect_equal(gcd(x), fraction(c(1, 12))) +}) + +context("test-rationalise_chord") + +test_that("examples", { + expect_equal( + rationalise_chord(hrep::pi_chord(c(0, 4, 7)), tonic = 0), + rational_chord(matrix(c(1, 5, 3, + 1, 4, 2), + nrow = 2, byrow = TRUE)) + ) + + expect_equal( + rationalise_chord(hrep::pi_chord(c(60, 64, 67)), tonic = 0), + rational_chord(matrix(c(1, 5, 3, + 1, 4, 2), + nrow = 2, byrow = TRUE)) + ) + + expect_equal( + rationalise_chord(hrep::pi_chord(c(60, 64, 67 + 12)), tonic = 0), + rational_chord(matrix(c(1, 5, 3, + 1, 4, 1), + nrow = 2, byrow = TRUE)) + ) +}) + +context("test-rational_pitch_class") + +test_that("examples", { + expect_equal(rationalise_pitch_class(0), fraction(c(1, 1))) + expect_equal(rationalise_pitch_class(7), fraction(c(3, 2))) +}) + +context("test-rational_pitch") + +test_that("examples", { + expect_equal(rationalise_pitch(7), fraction(c(3, 2))) + expect_equal(rationalise_pitch(0), fraction(c(1, 1))) + expect_equal(rationalise_pitch(12), fraction(c(2, 1))) + expect_equal(rationalise_pitch(12 + 7), fraction(c(3, 1))) + expect_equal(rationalise_pitch(12 + 6), fraction(c(14, 5))) +}) + +context("test-tonic") + +test_that("examples", { + expect_gt(gill09_harmonicity(c(0, 4, 7) + 0), + gill09_harmonicity(c(0, 4, 7) + 2)) + + expect_equal(gill09_harmonicity(c(0, 4, 7) + 0), + gill09_harmonicity(c(0, 4, 7) + 2, tonic = 2)) + + for (i in seq_len(10)) { + tonic <- sample(11, 1) + x1 <- sample(40, size = 3) %>% hrep::pi_chord() + x2 <- x1 + tonic + expect_equal( + gill09_harmonicity(x1), + gill09_harmonicity(x2, tonic = tonic) + ) + } +}) diff --git a/tests/testthat/test-par.R b/tests/testthat/test-par.R index 1534721..0d9c924 100644 --- a/tests/testthat/test-par.R +++ b/tests/testthat/test-par.R @@ -1,8 +1,8 @@ context("test-par") test_that("Gill & Purves example", { - a <- bowl18::gill09_harmonicity(c(0, 4, 7)) - b <- bowl18::gill09_harmonicity(c(0, 4, 7), tonic = 3) + a <- gill09_harmonicity(c(0, 4, 7)) + b <- gill09_harmonicity(c(0, 4, 7), tonic = 3) expect_gt(a, b) incon(c(0, 4, 7), model = "gill_09_harmonicity") %>% as.numeric %>% expect_equal(a) incon(c(0, 4, 7), diff --git a/tests/testthat/testthat-problems.rds b/tests/testthat/testthat-problems.rds new file mode 100644 index 0000000000000000000000000000000000000000..6adadd8dfe7131bf3d4aa1498d3a99ae6b088890 GIT binary patch literal 23477 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 literal 0 HcmV?d00001 From c2edd4d8ce67ae2ec505e97354f1ddf0193b8e35 Mon Sep 17 00:00:00 2001 From: Peter Harrison Date: Tue, 23 Jan 2024 22:22:20 +0000 Subject: [PATCH 09/24] Add corpdiss --- NAMESPACE | 8 ++ R/imports.R | 3 + R/model-corpdiss.R | 151 +++++++++++++++++++++++++++++++ R/model-har-2019.R | 2 +- R/models.R | 4 +- R/top-level.R | 2 +- README.md | 21 ----- data-raw/make-tables.R | 23 +++++ data/popular_1_pc_chord_type.rda | Bin 0 -> 2651 bytes man/corpus_dissonance.Rd | 47 ++++++++++ man/corpus_dissonance_table.Rd | 35 +++++++ man/count_chords.Rd | 24 +++++ man/har_19_composite.Rd | 2 +- man/incon.Rd | 2 +- man/popular_1_pc_chord_type.Rd | 22 +++++ man/type.Rd | 17 ++++ tests/testthat/test-corpdiss.R | 50 ++++++++++ 17 files changed, 386 insertions(+), 27 deletions(-) create mode 100644 R/model-corpdiss.R create mode 100644 data-raw/make-tables.R create mode 100644 data/popular_1_pc_chord_type.rda create mode 100644 man/corpus_dissonance.Rd create mode 100644 man/corpus_dissonance_table.Rd create mode 100644 man/count_chords.Rd create mode 100644 man/popular_1_pc_chord_type.Rd create mode 100644 man/type.Rd create mode 100644 tests/testthat/test-corpdiss.R diff --git a/NAMESPACE b/NAMESPACE index 99fcdad..e57cec8 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -2,12 +2,19 @@ S3method(bowl18_min_freq_dist,default) S3method(bowl18_min_freq_dist,fr_chord) +S3method(corpus_dissonance_table,corpus) +S3method(count_chords,corpus) S3method(huron_1994,default) S3method(huron_1994,int_vec) S3method(huron_1994,pc_set) +S3method(print,corpus_dissonance_table) S3method(roughness_wang,default) S3method(roughness_wang,sparse_fr_spectrum) +S3method(type,corpus_dissonance_table) export(bowl18_min_freq_dist) +export(corpus_dissonance) +export(corpus_dissonance_table) +export(count_chords) export(demo_wang) export(gill09_harmonicity) export(har_19_composite_coef) @@ -17,6 +24,7 @@ export(incon) export(incon_models) export(list_models) export(roughness_wang) +export(type) importFrom(magrittr,"%>%") importFrom(methods,"is") importFrom(stats,"approx") diff --git a/R/imports.R b/R/imports.R index 5143559..4602009 100644 --- a/R/imports.R +++ b/R/imports.R @@ -10,6 +10,9 @@ NULL #' @importFrom stats "approx" "cor" "fft" NULL +#' @importFrom tibble tibble +NULL + #' @importFrom utils "capture.output" NULL diff --git a/R/model-corpdiss.R b/R/model-corpdiss.R new file mode 100644 index 0000000..78a746d --- /dev/null +++ b/R/model-corpdiss.R @@ -0,0 +1,151 @@ +#' Consonance tables for popular music +#' +#' This table summarises chord prevalences in the +#' McGill Billboard corpus \insertCite{Burgoyne2011}{hcorp}. +#' These pieces were sampled from the Billboard magazine's +#' United States "Hot 100" chart between 1958 and 1991, +#' and transcribed by expert musicians. +#' See \code{\link[hcorp]{popular_1}} for more details. +#' +#' @details +#' Chords are represented as pitch-class chord types: +#' see \code{\link[hrep]{pc_chord_type}} for details. +#' +#' @name popular_1_pc_chord_type +#' @docType data +#' @references +#' \insertAllCited{} +#' @keywords data +NULL + +#' Corpus dissonance table +#' +#' Derives sufficient information from a corpus to formulate +#' a corpus-based dissonance model. +#' @param x Corpus to analyse, as created by \code{hrep::\link[hrep]{corpus}}. +#' @param type (Scalar character) Representation to which chords should be coerced +#' before counting. +#' @param add (Scalar numeric) Number to add to each count before computing probabilities. +#' This is useful for ensuring that chord probabilities exceed zero. +#' @return Returns an object of class \code{corpus_dissonance_table}, +#' a \link[tibble]{tibble} where each row corresponds to a +#' different pitch-class chord type (i.e. an object of class \code{pc_chord_type}), +#' with the mapping between integer indices and chord types defined by +#' the \code{hrep} package. +#' This tibble contains the following columns: +#' \item{count}{The number of times the chord type was observed in the corpus.} +#' \item{prob}{The inferred probability of that chord type.} +#' \item{log_prob}{The inferred log probability of that chord type.} +#' @rdname corpus_dissonance_table +#' @export +corpus_dissonance_table <- function(x, type = "pc_chord_type", add = 1L) { + UseMethod("corpus_dissonance_table") +} + +#' @rdname corpus_dissonance_table +#' @export +corpus_dissonance_table.corpus <- function(x, type = "pc_chord_type", add = 1L) { + df <- tibble::tibble(count = count_chords(x, type)) + df$count <- df$count + df$prob <- (df$count + add) / sum(df$count + add) + df$neg_log_prob <- - log(df$prob) + .corpus_dissonance_table(df, type) +} + +.corpus_dissonance_table <- function(df, type) { + class(df) <- c("corpus_dissonance_table", class(df)) + attr(df, "type") <- type + df +} + +#' Get type +#' +#' Gets the type of an object. +#' @param x Object. +#' @return (Character scalar) Type. +#' @export +type <- function(x) { + UseMethod("type") +} + +#' @export +print.corpus_dissonance_table <- function(x, ...) { + cat("# An object of class 'corpus_dissonance_table'\n") + cat("# Type: ", type(x), "\n", sep = "") + NextMethod() +} + +#' @export +type.corpus_dissonance_table <- function(x) { + attr(x, "type") +} + +`type<-.corpus_dissonance_table` <- function(x, value) { + attr(x, "type") <- value + x +} + +#' Corpus dissonance +#' +#' Calculates a corpus-based estimate of the dissonance of a sonority. +#' @details +#' By default, dissonance is estimated from chord prevalences +#' in the McGill Billboard dataset \insertCite{Burgoyne2011}{hcorp}. +#' The dataset's contents were sampled from the Billboard magazine's +#' United States "Hot 100" chart between 1958 and 1991, +#' and transcribed by expert musicians. +#' See \code{\link[hcorp]{popular_1}} for more details. +#' +#' By default, +#' the dissonance estimation treats chords as transposition invariant, +#' and chord pitches as octave-invariant, +#' but differentiates between different inversions of the same chord. +#' Different behaviour can be achieved by passing a custom corpus analysis +#' to the \code{table} argument. +#' @param x Sonority to analyse. +#' This should be an object created by the \code{hrep} package, +#' representing a pitch chord (\code{\link[hrep]{pi_chord}}), +#' representing a pitch-class chord (\code{\link[hrep]{pc_chord}}), +#' representing a pitch-class chord type (\code{\link[hrep]{pc_chord_type}}), +#' or a pitch-class set (\code{\link[hrep]{pc_set}}). +#' This object will be coerced to the same type as the corpus dissonance table, +#' i.e. \code{type(table)}. +#' @param table Corpus dissonance table, as created by +#' \code{\link{corpus_dissonance_table}()}. +#' This table summarises chord prevalences within a corpus. +#' The default is \code{\link{popular_1_pc_chord_type}}. +#' @return Dissonance estimate, as a numeric scalar. +#' @references +#' \insertAllCited{} +#' @export +corpus_dissonance <- function(x, table = popular_1_pc_chord_type) { + typ <- type(table) + x <- hrep::represent(x, typ) + i <- hrep::encode(x) + table$neg_log_prob[i] +} + +#' Count chords +#' +#' This function counts chords within a corpus. +#' @param x Corpus to analyse. +#' @param type Representation to which chords should be coerced +#' before counting. +#' @return Integer vector providing the observed counts for each chord, +#' indexed by the type encoding defined in the \code{hrep} package. +#' @rdname count_chords +#' @export +count_chords <- function(x, type = "pc_chord_type") { + UseMethod("count_chords") +} + +#' @rdname count_chords +#' @export +count_chords.corpus <- function(x, type = "pc_chord_type") { + hrep::represent(x, type) %>% + do.call(c, .) %>% + factor(levels = seq_len(hrep::alphabet_size(type))) %>% + table %>% + as.integer +} + diff --git a/R/model-har-2019.R b/R/model-har-2019.R index a9f8d3c..08d95c9 100644 --- a/R/model-har-2019.R +++ b/R/model-har-2019.R @@ -30,7 +30,7 @@ har_19_composite_coef <- tibble::tribble( #' - `har_19_corpus`: #' a corpus-based model of cultural familiarity #' (Harrison & Pearce, in preparation) -#' (see \code{corpdiss::\link[corpdiss]{corpus_dissonance}}). +#' (see \code{incon::\link[incon]{corpus_dissonance}}). #' #' This model uses the regression coefficients #' provided in \code{\link{har_19_composite_coef}}, with one caveat: diff --git a/R/models.R b/R/models.R index fc9fb8b..bef316f 100644 --- a/R/models.R +++ b/R/models.R @@ -231,12 +231,12 @@ add_model("jl_12_tonal", add_model("har_19_corpus", "Harrison & Pearce (2019)", "Culture", - "corpdiss", + "incon", consonance = FALSE, spectrum_sensitive = FALSE, continuous_pitch = FALSE, f = function(x, num_harmonics, roll_off, ...) - corpdiss::corpus_dissonance(x, ...)) + corpus_dissonance(x, ...)) add_model("parn_94_mult", "Parncutt & Strasburger (1994)", diff --git a/R/top-level.R b/R/top-level.R index 508f14f..31faa2c 100644 --- a/R/top-level.R +++ b/R/top-level.R @@ -103,7 +103,7 @@ #' * `har_19_corpus`: #' a corpus-based model of cultural familiarity #' \insertCite{Harrison2019}{incon} -#' (see \code{corpdiss::\link[corpdiss]{corpus_dissonance}}). +#' (see \code{incon::\link[incon]{corpus_dissonance}}). #' * `parn_94_mult`: #' the multiplicity feature of \insertCite{Parncutt1994;textual}{parn94} #' (see \code{parn94::\link[parn94]{multiplicity}}). diff --git a/README.md b/README.md index d0e4131..22c035a 100644 --- a/README.md +++ b/README.md @@ -81,27 +81,6 @@ implemented: See `?incon` for more details. -## Packages - -The functionality of `incon` is split between several low-level R -packages, listed -below. - -| Package | DOI | GitHub | -| :------- | :--------------------------------------------------------------- | :---------------------------------------- | -| bowl18 | [10.5281/zenodo.2545741](https://doi.org/10.5281/zenodo.2545741) | | -| corpdiss | [10.5281/zenodo.2545748](https://doi.org/10.5281/zenodo.2545748) | | -| dycon | [10.5281/zenodo.2545750](https://doi.org/10.5281/zenodo.2545750) | | -| har18 | [10.5281/zenodo.2545752](https://doi.org/10.5281/zenodo.2545752) | | -| hcorp | [10.5281/zenodo.2545754](https://doi.org/10.5281/zenodo.2545754) | | -| hrep | [10.5281/zenodo.2545770](https://doi.org/10.5281/zenodo.2545770) | | -| jl12 | [10.5281/zenodo.2545756](https://doi.org/10.5281/zenodo.2545756) | | -| parn88 | [10.5281/zenodo.1491909](https://doi.org/10.5281/zenodo.1491909) | | -| parn94 | [10.5281/zenodo.2545759](https://doi.org/10.5281/zenodo.2545759) | | -| stolz15 | [10.5281/zenodo.2545762](https://doi.org/10.5281/zenodo.2545762) | | -| wang13 | [10.5281/zenodo.2545764](https://doi.org/10.5281/zenodo.2545764) | | - - ## Spectral representations Certain `incon` models can be applied to full frequency spectra rather than just diff --git a/data-raw/make-tables.R b/data-raw/make-tables.R new file mode 100644 index 0000000..6dcfb85 --- /dev/null +++ b/data-raw/make-tables.R @@ -0,0 +1,23 @@ +# Generates the corpus dissonance tables that are +# stored along with this package. + +library(incon) + +corpora <- list(popular_1 = hcorp::popular_1) +type <- "pc_chord_type" + +unlink("data", recursive = TRUE) +dir.create("data") + +for (i in seq_along(corpora)) { + corpus <- corpora[[i]] + corpus_label <- names(corpora)[[i]] + data_label <- paste0(corpus_label, "_", type) + + env <- new.env() + env[[data_label]] <- corpus_dissonance_table(corpus, type) + + save(list = data_label, + envir = env, + file = file.path("data", paste0(data_label, ".rda"))) +} diff --git a/data/popular_1_pc_chord_type.rda b/data/popular_1_pc_chord_type.rda new file mode 100644 index 0000000000000000000000000000000000000000..0526e0a42ec2de697a500e85456803748325fabd GIT binary patch literal 2651 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 E3Fi>5v;Y7A literal 0 HcmV?d00001 diff --git a/man/corpus_dissonance.Rd b/man/corpus_dissonance.Rd new file mode 100644 index 0000000..c4527b7 --- /dev/null +++ b/man/corpus_dissonance.Rd @@ -0,0 +1,47 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/model-corpdiss.R +\name{corpus_dissonance} +\alias{corpus_dissonance} +\title{Corpus dissonance} +\usage{ +corpus_dissonance(x, table = popular_1_pc_chord_type) +} +\arguments{ +\item{x}{Sonority to analyse. +This should be an object created by the \code{hrep} package, +representing a pitch chord (\code{\link[hrep]{pi_chord}}), +representing a pitch-class chord (\code{\link[hrep]{pc_chord}}), +representing a pitch-class chord type (\code{\link[hrep]{pc_chord_type}}), +or a pitch-class set (\code{\link[hrep]{pc_set}}). +This object will be coerced to the same type as the corpus dissonance table, +i.e. \code{type(table)}.} + +\item{table}{Corpus dissonance table, as created by +\code{\link{corpus_dissonance_table}()}. +This table summarises chord prevalences within a corpus. +The default is \code{\link{popular_1_pc_chord_type}}.} +} +\value{ +Dissonance estimate, as a numeric scalar. +} +\description{ +Calculates a corpus-based estimate of the dissonance of a sonority. +} +\details{ +By default, dissonance is estimated from chord prevalences +in the McGill Billboard dataset \insertCite{Burgoyne2011}{hcorp}. +The dataset's contents were sampled from the Billboard magazine's +United States "Hot 100" chart between 1958 and 1991, +and transcribed by expert musicians. +See \code{\link[hcorp]{popular_1}} for more details. + +By default, +the dissonance estimation treats chords as transposition invariant, +and chord pitches as octave-invariant, +but differentiates between different inversions of the same chord. +Different behaviour can be achieved by passing a custom corpus analysis +to the \code{table} argument. +} +\references{ +\insertAllCited{} +} diff --git a/man/corpus_dissonance_table.Rd b/man/corpus_dissonance_table.Rd new file mode 100644 index 0000000..e0d9af2 --- /dev/null +++ b/man/corpus_dissonance_table.Rd @@ -0,0 +1,35 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/model-corpdiss.R +\name{corpus_dissonance_table} +\alias{corpus_dissonance_table} +\alias{corpus_dissonance_table.corpus} +\title{Corpus dissonance table} +\usage{ +corpus_dissonance_table(x, type = "pc_chord_type", add = 1L) + +\method{corpus_dissonance_table}{corpus}(x, type = "pc_chord_type", add = 1L) +} +\arguments{ +\item{x}{Corpus to analyse, as created by \code{hrep::\link[hrep]{corpus}}.} + +\item{type}{(Scalar character) Representation to which chords should be coerced +before counting.} + +\item{add}{(Scalar numeric) Number to add to each count before computing probabilities. +This is useful for ensuring that chord probabilities exceed zero.} +} +\value{ +Returns an object of class \code{corpus_dissonance_table}, +a \link[tibble]{tibble} where each row corresponds to a +different pitch-class chord type (i.e. an object of class \code{pc_chord_type}), +with the mapping between integer indices and chord types defined by +the \code{hrep} package. +This tibble contains the following columns: +\item{count}{The number of times the chord type was observed in the corpus.} +\item{prob}{The inferred probability of that chord type.} +\item{log_prob}{The inferred log probability of that chord type.} +} +\description{ +Derives sufficient information from a corpus to formulate +a corpus-based dissonance model. +} diff --git a/man/count_chords.Rd b/man/count_chords.Rd new file mode 100644 index 0000000..c94507e --- /dev/null +++ b/man/count_chords.Rd @@ -0,0 +1,24 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/model-corpdiss.R +\name{count_chords} +\alias{count_chords} +\alias{count_chords.corpus} +\title{Count chords} +\usage{ +count_chords(x, type = "pc_chord_type") + +\method{count_chords}{corpus}(x, type = "pc_chord_type") +} +\arguments{ +\item{x}{Corpus to analyse.} + +\item{type}{Representation to which chords should be coerced +before counting.} +} +\value{ +Integer vector providing the observed counts for each chord, +indexed by the type encoding defined in the \code{hrep} package. +} +\description{ +This function counts chords within a corpus. +} diff --git a/man/har_19_composite.Rd b/man/har_19_composite.Rd index 79df26c..cd3229f 100644 --- a/man/har_19_composite.Rd +++ b/man/har_19_composite.Rd @@ -39,7 +39,7 @@ the harmonicity model of \insertCite{Harrison2018;textual}{har18} \item \code{har_19_corpus}: a corpus-based model of cultural familiarity (Harrison & Pearce, in preparation) -(see \code{corpdiss::\link[corpdiss]{corpus_dissonance}}). +(see \code{incon::\link[incon]{corpus_dissonance}}). } This model uses the regression coefficients diff --git a/man/incon.Rd b/man/incon.Rd index d59718a..12bc8eb 100644 --- a/man/incon.Rd +++ b/man/incon.Rd @@ -112,7 +112,7 @@ the tonal dissonance model of \insertCite{Johnson-Laird2012;textual}{jl12} \item \code{har_19_corpus}: a corpus-based model of cultural familiarity \insertCite{Harrison2019}{incon} -(see \code{corpdiss::\link[corpdiss]{corpus_dissonance}}). +(see \code{incon::\link[incon]{corpus_dissonance}}). \item \code{parn_94_mult}: the multiplicity feature of \insertCite{Parncutt1994;textual}{parn94} (see \code{parn94::\link[parn94]{multiplicity}}). diff --git a/man/popular_1_pc_chord_type.Rd b/man/popular_1_pc_chord_type.Rd new file mode 100644 index 0000000..64a7593 --- /dev/null +++ b/man/popular_1_pc_chord_type.Rd @@ -0,0 +1,22 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/model-corpdiss.R +\docType{data} +\name{popular_1_pc_chord_type} +\alias{popular_1_pc_chord_type} +\title{Consonance tables for popular music} +\description{ +This table summarises chord prevalences in the +McGill Billboard corpus \insertCite{Burgoyne2011}{hcorp}. +These pieces were sampled from the Billboard magazine's +United States "Hot 100" chart between 1958 and 1991, +and transcribed by expert musicians. +See \code{\link[hcorp]{popular_1}} for more details. +} +\details{ +Chords are represented as pitch-class chord types: +see \code{\link[hrep]{pc_chord_type}} for details. +} +\references{ +\insertAllCited{} +} +\keyword{data} diff --git a/man/type.Rd b/man/type.Rd new file mode 100644 index 0000000..9723cfa --- /dev/null +++ b/man/type.Rd @@ -0,0 +1,17 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/model-corpdiss.R +\name{type} +\alias{type} +\title{Get type} +\usage{ +type(x) +} +\arguments{ +\item{x}{Object.} +} +\value{ +(Character scalar) Type. +} +\description{ +Gets the type of an object. +} diff --git a/tests/testthat/test-corpdiss.R b/tests/testthat/test-corpdiss.R new file mode 100644 index 0000000..0818e6d --- /dev/null +++ b/tests/testthat/test-corpdiss.R @@ -0,0 +1,50 @@ +context("test-corpus_dissonance_table") + +library(magrittr) + +test_that("examples", { + list( + c(1, 2), + c(3, 4), + c(5, 6) + ) %>% + purrr::map(~ hrep::coded_vec(., type = "pc_set")) %>% + hrep::corpus(type = "pc_set") %>% + corpus_dissonance_table(type = "pc_set") %>% + expect_equal( + .corpus_dissonance_table( + tibble::tibble( + count = c(rep(1, times = 6), rep(0, times = hrep::alphabet_size("pc_set") - 6)), + prob = (count + 1) / sum(count + 1), + neg_log_prob = - log(prob) + ), type = "pc_set")) +}) + + +context("test-corpus_dissonance") + +test_that("examples", { + test <- function(x, y, tolerance = 1e-5) { + expect_equal(corpus_dissonance(x), y, tolerance = tolerance) + } + test(c(60, 64, 67), 0.7802433) + test(c(64, 67, 72), 4.06715) + test(c(60, 63, 67), 2.271055) + test(c(60, 63, 66), 6.134397) + test(c(60, 64, 68), 7.551463) + test(c(60, 64, 67, 70), 2.610535) + test(c(60, 63, 67, 70), 2.650456) + + expect_equal( + corpus_dissonance(c(60, 64, 67)), + corpus_dissonance(hrep::pc_chord(c(0, 4, 7))) + ) + expect_equal( + corpus_dissonance(c(64, 67, 72)), + corpus_dissonance(hrep::pc_chord(c(4, 7, 0))) + ) + expect_equal( + corpus_dissonance(hrep::pc_set(c(0, 3, 6))), + corpus_dissonance(hrep::pc_chord_type(c(0, 3, 6))) + ) +}) From c00c33f646a44ed383921ceec06c7502435006b9 Mon Sep 17 00:00:00 2001 From: Peter Harrison Date: Tue, 23 Jan 2024 22:25:07 +0000 Subject: [PATCH 10/24] Add dycon --- DESCRIPTION | 2 + NAMESPACE | 16 + R/imports.R | 3 + R/model-dycon.R | 498 +++++++++++++++++++++++++++++++ man/dyad_roughness_seth.Rd | 60 ++++ man/dyad_roughness_vass.Rd | 31 ++ man/hutch_cbw.Rd | 24 ++ man/hutch_dissonance_function.Rd | 25 ++ man/hutch_g.Rd | 38 +++ man/hutch_visualise.Rd | 46 +++ man/hutch_visualise_theme.Rd | 18 ++ man/hutch_y.Rd | 24 ++ man/roughness_hutch.Rd | 69 +++++ man/roughness_seth.Rd | 55 ++++ man/roughness_vass.Rd | 38 +++ tests/test-dycon.R | 172 +++++++++++ 16 files changed, 1119 insertions(+) create mode 100644 R/model-dycon.R create mode 100644 man/dyad_roughness_seth.Rd create mode 100644 man/dyad_roughness_vass.Rd create mode 100644 man/hutch_cbw.Rd create mode 100644 man/hutch_dissonance_function.Rd create mode 100644 man/hutch_g.Rd create mode 100644 man/hutch_visualise.Rd create mode 100644 man/hutch_visualise_theme.Rd create mode 100644 man/hutch_y.Rd create mode 100644 man/roughness_hutch.Rd create mode 100644 man/roughness_seth.Rd create mode 100644 man/roughness_vass.Rd create mode 100644 tests/test-dycon.R diff --git a/DESCRIPTION b/DESCRIPTION index 2bbb928..6979d44 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -19,6 +19,7 @@ Imports: methods, tibble (>= 2.1.3), dplyr (>= 0.8.3), + rlang (>= 0.4.0), zeallot (>= 0.1.0), utils RdMacros: Rdpack @@ -27,6 +28,7 @@ Suggests: knitr (>= 1.23), DT (>= 0.5), covr (>= 3.2.1) + ggplot2 (>= 3.1.0.9000) RoxygenNote: 7.3.1 Remotes: pmcharrison/hrep diff --git a/NAMESPACE b/NAMESPACE index e57cec8..f767c83 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -8,6 +8,12 @@ S3method(huron_1994,default) S3method(huron_1994,int_vec) S3method(huron_1994,pc_set) S3method(print,corpus_dissonance_table) +S3method(roughness_hutch,default) +S3method(roughness_hutch,sparse_fr_spectrum) +S3method(roughness_seth,default) +S3method(roughness_seth,sparse_fr_spectrum) +S3method(roughness_vass,default) +S3method(roughness_vass,sparse_fr_spectrum) S3method(roughness_wang,default) S3method(roughness_wang,sparse_fr_spectrum) S3method(type,corpus_dissonance_table) @@ -20,13 +26,23 @@ export(gill09_harmonicity) export(har_19_composite_coef) export(huron_1994) export(huron_1994_weights) +export(hutch_cbw) +export(hutch_dissonance_function) +export(hutch_g) +export(hutch_visualise) +export(hutch_visualise_theme) +export(hutch_y) export(incon) export(incon_models) export(list_models) +export(roughness_hutch) +export(roughness_seth) +export(roughness_vass) export(roughness_wang) export(type) importFrom(magrittr,"%>%") importFrom(methods,"is") +importFrom(rlang,".data") importFrom(stats,"approx") importFrom(stats,"cor") importFrom(stats,"fft") diff --git a/R/imports.R b/R/imports.R index 4602009..eb38036 100644 --- a/R/imports.R +++ b/R/imports.R @@ -16,4 +16,7 @@ NULL #' @importFrom utils "capture.output" NULL +#' @importFrom rlang ".data" +NULL + `.` <- NULL diff --git a/R/model-dycon.R b/R/model-dycon.R new file mode 100644 index 0000000..a14d326 --- /dev/null +++ b/R/model-dycon.R @@ -0,0 +1,498 @@ +#' Spectral roughness (Hutchinson & Knopoff) +#' +#' Gets the roughness of a sonority according to the model of +#' \insertCite{Hutchinson1978;textual}{incon}. +#' @param x Object to analyse, which is coerced to the class +#' \code{\link[hrep]{sparse_fr_spectrum}}. +#' * Numeric vectors will be treated as vectors of MIDI note numbers, +#' and expanded into their implied harmonics. +#' * Two-element lists will be treated as finalised spectra, +#' with the first element being a numeric vector of frequencies, +#' and the second element being a numeric vector of amplitudes. +#' +#' @param a Parameter passed to \code{\link{hutch_g}()}. +#' +#' @param b Parameter passed to \code{\link{hutch_g}()}. +#' +#' @param cbw_cut_off Parameter passed to \code{\link{hutch_g}()}. +#' +#' @param dissonance_function +#' Function for computing dissonance contribution as a function of +#' critical bandwidth distance, defaulting to \code{\link{hutch_dissonance_function}}. +#' Custom functions may be specified here as long as they use the same parameter list +#' as the original function. +#' +#' @return Numeric scalar, identifying the roughness of the spectrum. +#' +#' @references +#' \insertAllCited{} +#' +#' @rdname roughness_hutch +#' +#' @md +#' +#' @export +roughness_hutch <- function( + x, + cbw_cut_off = 1.2, + a = 0.25, + b = 2, + dissonance_function = hutch_dissonance_function, + ...) { + UseMethod("roughness_hutch") +} + +#' @param ... Further arguments to pass to \code{\link[hrep]{sparse_fr_spectrum}}. +#' @rdname roughness_hutch +#' @export +roughness_hutch.default <- function( + x, + cbw_cut_off = 1.2, + a = 0.25, + b = 2, + dissonance_function = hutch_dissonance_function, + ... +) { + x <- hrep::sparse_fr_spectrum(x, ...) + roughness_hutch(x, cbw_cut_off = cbw_cut_off, a = a, b = b, dissonance_function = dissonance_function) +} + +#' @rdname roughness_hutch +#' @export +roughness_hutch.sparse_fr_spectrum <- function( + x, + cbw_cut_off = 1.2, + a = 0.25, + b = 2, + dissonance_function = hutch_dissonance_function, + ... +) { + frequency <- hrep::freq(x) + amplitude <- hrep::amp(x) + n <- length(frequency) + if (n < 2) 0 else { + # Compute denominator + denominator <- sum(amplitude ^ 2) + # Compute numerator + df <- expand.grid(j = seq_len(n), i = seq_len(n)) %>% + (function(df) { + df[df$i < df$j, ] + }) + df$a_i_a_j <- amplitude[df$i] * amplitude[df$j] + df$g_ij <- dissonance_function( + f1 = frequency[df$i], + f2 = frequency[df$j], + cbw_cut_off = cbw_cut_off, + a = a, + b = b + ) + numerator <- sum(df$a_i_a_j * df$g_ij) + numerator / denominator + } +} + +#' Get dissonance contribution +#' +#' Computes the dissonance contribution of a pair of pure tones. +#' +#' @inheritParams roughness_hutch +#' @inheritParams hutch_cbw +#' +#' @return Numeric vector of dissonance contributions. +#' +#' @export +hutch_dissonance_function <- function(f1, f2, cbw_cut_off = 1.2, a = 0.25, b = 2) { + hutch_g( + y = hutch_y(f1 = f1, f2 = f2), + cbw_cut_off = cbw_cut_off, + a = a, + b = b + ) +} + +#' Critical bandwidth (Hutchison & Knopoff) +#' +#' Calculates the critical bandwidth given pairs of frequencies +#' \code{f1} and \code{f2}, +#' according to the model of \insertCite{Hutchinson1978;textual}{incon}. +#' @param f1 (Numeric vector) Frequency 1, Hz +#' @param f2 (Numeric vector) Frequency 2, Hz +#' @return (Numeric vector) Critical bandwidths. +#' @references +#' \insertAllCited{} +#' @export +hutch_cbw <- function(f1, f2) { + mean_f <- (f1 + f2) / 2 + 1.72 * (mean_f ^ 0.65) +} + +#' Critical bandwidth distance (Hutchison & Knopoff) +#' +#' Calculates the distance between pairs of frequencies in units of +#' critical bandwidths, according to the model of +#' \insertCite{Hutchinson1978;textual}{incon}. +#' @param f1 (Numeric vector) Frequency 1, Hz +#' @param f2 (Numeric vector) Frequency 2, Hz +#' @return (Numeric vector) Unsigned distances in frequency bandwidths. +#' @references +#' \insertAllCited{} +#' @export +hutch_y <- function(f1, f2) { + abs_freq_diff <- abs(f1 - f2) + critical_bandwidth <- hutch_cbw(f1, f2) + abs_freq_diff / critical_bandwidth +} + +#' Dissonance factor (Hutchinson & Knopoff) +#' +#' Computes dissonance factors given frequency distances in units of +#' critical bandwidths, after Mashinter's implementation of +#' Hutchinson & Knopoff's model +#' \insertCite{Mashinter2006,Hutchinson1978}{incon}. +#' This function corresponds to an approximation of the +#' look-up table in \insertCite{Plomp1965;textual}{incon}. +#' @param y (Numeric vector) Frequency distance in units of critical bandwidths. +#' @param cbw_cut_off (Numeric scalar) +#' If not \code{NULL}, then should be a number +#' corresponding to the variable CBWcutoff in Mashinter's own implementation. +#' If \code{y >= cbw_cut_off}, then the dissonance factor will be approximated as 0. +#' Setting \code{cbw_cut_off} to 1.2 is necessary for replicating Mashinter's results. +#' A cut-off of 1.2 was also used by \insertCite{Bigand1996;textual}{incon}. +#' @param a (Numeric scalar, default = 0.25) +#' Parameter from \insertCite{Mashinter2006;textual}{incon}. +#' @param b (Numeric scalar, default = 2) +#' Parameter from \insertCite{Mashinter2006;textual}{incon}. +#' @return (Numeric vector) Dissonance factors. +#' @references +#' \insertAllCited{} +#' @export +hutch_g <- function(y, cbw_cut_off = 1.2, a = 0.25, b = 2) { + assertthat::assert_that( + is.numeric(y), all(y >= 0), + is.null(cbw_cut_off) || (assertthat::is.scalar(cbw_cut_off) && is.numeric(cbw_cut_off)), + is.numeric(a), is.numeric(b), + assertthat::is.scalar(a), assertthat::is.scalar(b) + ) + res <- ((y / a) * exp(1 - (y / a))) ^ b + if (!is.null(cbw_cut_off)) { + res[y > cbw_cut_off] <- 0 + } + res +} + +hutch_visualise_data <- function(x, cbw_cut_off, a, b, min_freq, max_freq, ...) { + frequency <- c(min_freq, hrep::freq(x), max_freq) + amplitude <- c(0, hrep::amp(x), 0) + n <- length(frequency) + df <- expand.grid(j = seq_len(n), i = seq_len(n)) %>% + tibble::as_tibble() %>% + dplyr::mutate( + a_i_a_j = amplitude[.data$i] * amplitude[.data$j], + g_ij = hutch_g( + y = hutch_y(f1 = frequency[.data$i], + f2 = frequency[.data$j]), + cbw_cut_off = cbw_cut_off, + a = a, + b = b + )) %>% + dplyr::group_by(.data$i) %>% + dplyr::summarise(dissonance = sum(.data$g_ij)) + df2 <- tibble::tibble(i = seq_len(n), frequency, amplitude) %>% + dplyr::left_join(df, by = "i") %>% + dplyr::mutate(pitch = hrep::freq_to_midi(frequency)) + df3 <- data.frame(pitch = numeric(n * 5), + amplitude = numeric(n * 5), + dissonance = numeric(n * 5)) + for (i in seq_len(n)) { + I <- (i - 1L) * 5L + df3[I + 1:5, "pitch"] <- df2[i, "pitch"] + df3[I + 2:4, "dissonance"] <- df2[i, "dissonance"] + df3$amplitude[I + 3L] <- df2$amplitude[i] + } + df3 +} + +#' ggplot theme +#' +#' Defines a default theme for visualising computations +#' for Hutchinson & Knopoff's (1978) model +#' (see \code{\link{hutch_visualise}}). +#' @export +hutch_visualise_theme <- ggplot2::theme_classic() + + ggplot2::theme( + panel.spacing = ggplot2::unit(1.9, "lines"), + strip.background = ggplot2::element_blank(), + axis.text.x = ggplot2::element_text(colour = "black"), + axis.text.y = ggplot2::element_text(colour = "black"), + axis.ticks = ggplot2::element_line(colour = "black") + ) + +#' Visualise +#' +#' Creates a plot visualising computations for Hutchinson & Knopoff's model. +#' +#' @param x Passed to \code{\link{roughness_hutch}}. +#' @param cbw_cut_off Passed to \code{\link{roughness_hutch}}. +#' @param a Passed to \code{\link{roughness_hutch}}. +#' @param b Passed to \code{\link{roughness_hutch}}. +#' @param label (Character scalar) x-axis label. +#' @param amplitude_breaks Numeric vector of tick locations for the y-axis. +#' @param colour_limits Defines the limits of the roughness scale. +#' @param colour_low Colour to use for the lowest roughness. +#' @param colour_high Colour to use for the highest roughness. +#' @param theme \code{\link[ggplot2]{ggplot}} theme to use. +#' @param ... Passed to \code{\link[hrep]{sparse_fr_spectrum}}. +#' @export +hutch_visualise <- function(x, + cbw_cut_off = 1.2, + a = 0.25, + b = 2, + label = "Roughness", + amplitude_breaks = c(0, 1), + colour_limits = c(0, 3), + colour_low = "darkblue", + colour_high = "red", + theme = hutch_visualise_theme, + ...) { + stopifnot(is.list(x), !is.null(names(x)), !anyDuplicated(names(x))) + x <- purrr::map(x, hrep::sparse_fr_spectrum, ...) + min_freq <- min(purrr::map_dbl(x, ~ min(hrep::freq(.)))) + max_freq <- max(purrr::map_dbl(x, ~ max(hrep::freq(.)))) + labels <- factor(names(x), levels = names(x)) + purrr::map(x, hutch_visualise_data, cbw_cut_off, a, b, min_freq, max_freq, ...) %>% + purrr::map2(labels, ~ dplyr::mutate(.x, label = .y)) %>% + dplyr::bind_rows() %>% + ggplot2::ggplot(ggplot2::aes_string(x = "pitch", + y = "amplitude", + colour = "dissonance")) + + ggplot2::geom_line() + + ggplot2::scale_x_continuous("Pitch (MIDI)") + + ggplot2::scale_y_continuous("Amplitude", breaks = amplitude_breaks) + + ggplot2::scale_colour_gradient(label, + low = colour_low, + high = colour_high, + limits = colour_limits) + + ggplot2::facet_wrap(~ label, ncol = 1) + + theme +} + +#' Spectral roughness (Sethares) +#' +#' Gets the roughness of a sonority according to the model of Sethares (1993). +#' By default, the algorithm is modified according to +#' \insertCite{Sethares2005;textual}{incon} and +#' \insertCite{Weisser2013;textual}{incon}: +#' roughness is proportional to the minimum amplitude of each pair of partials, +#' not the product of their amplitudes. +#' This behaviour can be disabled by setting \code{min_amplitude = FALSE}. +#' @param x Object to analyse, which is coerced to the class +#' \code{\link[hrep]{sparse_fr_spectrum}}. +#' * Numeric vectors will be treated as vectors of MIDI note numbers, +#' and expanded into their implied harmonics. +#' * Two-element lists will be treated as finalised spectra, +#' with the first element being a numeric vector of frequencies, +#' and the second element being a numeric vector of amplitudes. +#' @param min_amplitude See \code{\link{dyad_roughness_seth}}. +#' @return Estimated roughness, as a numeric scalar. +#' @note \insertCite{Sethares2005;textual}{incon} +#' suggests using loudnesses instead of amplitudes. +#' However, he acknowledges that loudness is difficult to calculate +#' for arbitrary timbres. +#' Furthermore, if we replace amplitude with roughness, +#' we lose the original model's invariance to multiplicative +#' scaling of the original signal. +#' In this implementation, we therefore stay with amplitude, +#' consistent with \insertCite{Sethares1993;textual}{incon}. +#' @references +#' \insertAllCited{} +#' @rdname roughness_seth +#' @md +#' @export +roughness_seth <- function(x, min_amplitude = TRUE, ...) { + UseMethod("roughness_seth") +} + +#' @param ... Further arguments to pass to \code{\link[hrep]{sparse_fr_spectrum}}. +#' @rdname roughness_seth +#' @export +roughness_seth.default <- function(x, min_amplitude = TRUE, ...) { + x <- hrep::sparse_fr_spectrum(x, ...) + roughness_seth(x, min_amplitude = min_amplitude) +} + +#' @rdname roughness_seth +#' @export +roughness_seth.sparse_fr_spectrum <- function(x, min_amplitude = TRUE, ...) { + frequency <- hrep::freq(x) + amplitude <- hrep::amp(x) + n <- length(frequency) + if (n < 2) 0 else { + # The formula given in Sethares (1993) iterates over all pairs of [i, j], + # but because roughness is symmetric we can save time by only considering pairs. + # This gets rid of the 'divide by two' component in the original equation. + df <- expand.grid(j = seq_len(n), i = seq_len(n)) %>% + (function(df) { + df[df$i < df$j, ] + }) + dyad_roughness_seth( + f1 = frequency[df$i], + f2 = frequency[df$j], + a1 = amplitude[df$i], + a2 = amplitude[df$j], + min_amplitude = min_amplitude + ) %>% sum + } +} + +#' Dyad roughness (Sethares) +#' +#' Gets the roughness of a dyad according to the model of Sethares (1993). +#' @param f1 Frequency of tone 1 (Hz) (numeric vector). +#' @param f2 Frequency of tone 2 (Hz) (numeric vector). Must be greater than \code{f1}. +#' @param a1 amplitude of tone 1 (numeric vector). +#' @param a2 amplitude of tone 2 (numeric vector). +#' @param ensure_f1_is_less_than_f2 If \code{FALSE}, assumes that \code{f1 < f2}. +#' @param min_amplitude If \code{TRUE}, +#' roughness is considered to be proportional to +#' the minimum amplitude of each pair of partials, +#' rather than the product of their amplitudes. +#' The default (\code{TRUE}) corresponds to the algorithm as updated by +#' \insertCite{Sethares2005;textual}{incon} and +#' \insertCite{Weisser2013;textual}{incon}. +#' Set to \code{FALSE} to recover the original algorithm from +#' \insertCite{Sethares1993;textual}{incon}. +#' @param a Numeric scalar parameter, optimised to 3.5 (default) in Sethares (1993). +#' @param b Numeric scalar parameter, optimised to 5.75 (default) in Sethares (1993). +#' @param s1 Numeric scalar parameter from Sethares (1993). +#' @param s2 Numeric scalar parameter from Sethares (1993). +#' @param d_star Numeric scalar parameter from Sethares (1993). +#' @return Numeric vector of roughnesses. +#' @references +#' \insertAllCited{} +dyad_roughness_seth <- function(f1, f2, a1, a2, + ensure_f1_is_less_than_f2 = TRUE, + min_amplitude = TRUE, + a = 3.5, + b = 5.75, + s1 = 0.021, + s2 = 19, + d_star = 0.24) { + assertthat::assert_that( + is.numeric(f1), is.numeric(f2), + is.numeric(a1), is.numeric(a2), + is.logical(min_amplitude), length(min_amplitude) == 1L, + length(f1) == length(f2), + length(f1) == length(a1), + length(f1) == length(a2) + ) + if (ensure_f1_is_less_than_f2) { + need_reversal <- f1 > f2 + dyad_roughness_seth( + f1 = ifelse(need_reversal, f2, f1), + f2 = ifelse(need_reversal, f1, f2), + a1 = ifelse(need_reversal, a2, a1), + a2 = ifelse(need_reversal, a1, a2), + ensure_f1_is_less_than_f2 = FALSE, + a = a, b = b, s1 = s1, s2 = s2, d_star = d_star + ) + } else { + s <- d_star / (s1 * f1 + s2) + A <- if (min_amplitude) pmin(a1, a2) else a1 * a2 + A * ( + exp(- a * s * (f2 - f1)) - + exp(- b * s * (f2 - f1)) + ) + } +} + +#' Spectral roughness (Vassilakis) +#' +#' Gets the roughness of a sonority according to the model of +#' \insertCite{Vassilakis2001;textual}{incon} +#' \insertCite{Villegas2010;textual}{incon} +#' @param x Object to analyse, which is coerced to the class +#' \code{\link[hrep]{sparse_fr_spectrum}}. +#' * Numeric vectors will be treated as vectors of MIDI note numbers, +#' and expanded into their implied harmonics. +#' * Two-element lists will be treated as finalised spectra, +#' with the first element being a numeric vector of frequencies, +#' and the second element being a numeric vector of amplitudes. +#' @return Estimated roughness, as a numeric scalar. +#' @references +#' \insertAllCited{} +#' @rdname roughness_vass +#' @md +#' @export +roughness_vass <- function(x, ...) { + UseMethod("roughness_vass") +} + +#' @param ... Further arguments to pass to \code{\link[hrep]{sparse_fr_spectrum}}. +#' @rdname roughness_vass +#' @export +roughness_vass.default <- function(x, ...) { + x <- hrep::sparse_fr_spectrum(x, ...) + roughness_vass(x) +} + +#' @rdname roughness_vass +#' @export +roughness_vass.sparse_fr_spectrum <- function(x, ...) { + tolerance <- 1e-5 + x <- x[x$y > tolerance, ] # eliminate partials with near-zero weight + frequency <- hrep::freq(x) + amplitude <- hrep::amp(x) + n <- length(frequency) + if (n < 2) 0 else { + # Roughness is computed by summing over all dyadic roughnesses. + # Noting that the formula for dyadic roughness is symmetric, + # we can instead only compute dyadic roughnesses for pairs + # where i < j, and then double the resulting sum. + df <- expand.grid(j = seq_len(n), i = seq_len(n)) %>% + (function(df) { + df[df$i < df$j, ] + }) + dyad_roughness_vass( + f1 = frequency[df$i], + f2 = frequency[df$j], + a1 = amplitude[df$i], + a2 = amplitude[df$j] + ) %>% sum %>% magrittr::multiply_by(2) + } +} + +#' Dyad roughness (Vassilakis) +#' +#' Gets the roughness of a dyad according to the model of +#' \insertCite{Vassilakis2001;textual}{incon} +#' \insertCite{Villegas2010;textual}{incon} +#' @param f1 Frequency of tone 1 (Hz) (numeric vector). +#' @param f2 Frequency of tone 2 (Hz) (numeric vector). +#' @param a1 amplitude of tone 1 (numeric vector). +#' @param a2 amplitude of tone 2 (numeric vector). +#' @return Numeric vector of roughnesses. +#' @note The function is vectorised over all inputs. +#' @references +#' \insertAllCited{} +dyad_roughness_vass <- function(f1, f2, a1, a2) { + assertthat::assert_that( + is.numeric(f1), is.numeric(f2), + is.numeric(a1), is.numeric(a2), + length(f1) == length(f2), + length(f1) == length(a1), + length(f1) == length(a2) + ) + ((a1 * a2) ^ 0.1) * + 0.5 * + (((2 * pmin(a1, a2)) / (a1 + a2)) ^ 3.11) * + (exp(- 3.5 * f_vass(f1, f2)) - exp(- 5.75 * f_vass(f1, f2))) +} + +f_vass <- function(f1, f2) { + s_vass(pmin(f1, f2)) * + abs(f1 - f2) +} + +s_vass <- function(f) { + 0.24 / (0.0207 * f + 18.96) +} diff --git a/man/dyad_roughness_seth.Rd b/man/dyad_roughness_seth.Rd new file mode 100644 index 0000000..78315e3 --- /dev/null +++ b/man/dyad_roughness_seth.Rd @@ -0,0 +1,60 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/model-dycon.R +\name{dyad_roughness_seth} +\alias{dyad_roughness_seth} +\title{Dyad roughness (Sethares)} +\usage{ +dyad_roughness_seth( + f1, + f2, + a1, + a2, + ensure_f1_is_less_than_f2 = TRUE, + min_amplitude = TRUE, + a = 3.5, + b = 5.75, + s1 = 0.021, + s2 = 19, + d_star = 0.24 +) +} +\arguments{ +\item{f1}{Frequency of tone 1 (Hz) (numeric vector).} + +\item{f2}{Frequency of tone 2 (Hz) (numeric vector). Must be greater than \code{f1}.} + +\item{a1}{amplitude of tone 1 (numeric vector).} + +\item{a2}{amplitude of tone 2 (numeric vector).} + +\item{ensure_f1_is_less_than_f2}{If \code{FALSE}, assumes that \code{f1 < f2}.} + +\item{min_amplitude}{If \code{TRUE}, +roughness is considered to be proportional to +the minimum amplitude of each pair of partials, +rather than the product of their amplitudes. +The default (\code{TRUE}) corresponds to the algorithm as updated by +\insertCite{Sethares2005;textual}{incon} and +\insertCite{Weisser2013;textual}{incon}. +Set to \code{FALSE} to recover the original algorithm from +\insertCite{Sethares1993;textual}{incon}.} + +\item{a}{Numeric scalar parameter, optimised to 3.5 (default) in Sethares (1993).} + +\item{b}{Numeric scalar parameter, optimised to 5.75 (default) in Sethares (1993).} + +\item{s1}{Numeric scalar parameter from Sethares (1993).} + +\item{s2}{Numeric scalar parameter from Sethares (1993).} + +\item{d_star}{Numeric scalar parameter from Sethares (1993).} +} +\value{ +Numeric vector of roughnesses. +} +\description{ +Gets the roughness of a dyad according to the model of Sethares (1993). +} +\references{ +\insertAllCited{} +} diff --git a/man/dyad_roughness_vass.Rd b/man/dyad_roughness_vass.Rd new file mode 100644 index 0000000..6d67249 --- /dev/null +++ b/man/dyad_roughness_vass.Rd @@ -0,0 +1,31 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/model-dycon.R +\name{dyad_roughness_vass} +\alias{dyad_roughness_vass} +\title{Dyad roughness (Vassilakis)} +\usage{ +dyad_roughness_vass(f1, f2, a1, a2) +} +\arguments{ +\item{f1}{Frequency of tone 1 (Hz) (numeric vector).} + +\item{f2}{Frequency of tone 2 (Hz) (numeric vector).} + +\item{a1}{amplitude of tone 1 (numeric vector).} + +\item{a2}{amplitude of tone 2 (numeric vector).} +} +\value{ +Numeric vector of roughnesses. +} +\description{ +Gets the roughness of a dyad according to the model of +\insertCite{Vassilakis2001;textual}{incon} +\insertCite{Villegas2010;textual}{incon} +} +\note{ +The function is vectorised over all inputs. +} +\references{ +\insertAllCited{} +} diff --git a/man/hutch_cbw.Rd b/man/hutch_cbw.Rd new file mode 100644 index 0000000..9defa80 --- /dev/null +++ b/man/hutch_cbw.Rd @@ -0,0 +1,24 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/model-dycon.R +\name{hutch_cbw} +\alias{hutch_cbw} +\title{Critical bandwidth (Hutchison & Knopoff)} +\usage{ +hutch_cbw(f1, f2) +} +\arguments{ +\item{f1}{(Numeric vector) Frequency 1, Hz} + +\item{f2}{(Numeric vector) Frequency 2, Hz} +} +\value{ +(Numeric vector) Critical bandwidths. +} +\description{ +Calculates the critical bandwidth given pairs of frequencies +\code{f1} and \code{f2}, +according to the model of \insertCite{Hutchinson1978;textual}{incon}. +} +\references{ +\insertAllCited{} +} diff --git a/man/hutch_dissonance_function.Rd b/man/hutch_dissonance_function.Rd new file mode 100644 index 0000000..ed6569c --- /dev/null +++ b/man/hutch_dissonance_function.Rd @@ -0,0 +1,25 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/model-dycon.R +\name{hutch_dissonance_function} +\alias{hutch_dissonance_function} +\title{Get dissonance contribution} +\usage{ +hutch_dissonance_function(f1, f2, cbw_cut_off = 1.2, a = 0.25, b = 2) +} +\arguments{ +\item{f1}{(Numeric vector) Frequency 1, Hz} + +\item{f2}{(Numeric vector) Frequency 2, Hz} + +\item{cbw_cut_off}{Parameter passed to \code{\link{hutch_g}()}.} + +\item{a}{Parameter passed to \code{\link{hutch_g}()}.} + +\item{b}{Parameter passed to \code{\link{hutch_g}()}.} +} +\value{ +Numeric vector of dissonance contributions. +} +\description{ +Computes the dissonance contribution of a pair of pure tones. +} diff --git a/man/hutch_g.Rd b/man/hutch_g.Rd new file mode 100644 index 0000000..751efbd --- /dev/null +++ b/man/hutch_g.Rd @@ -0,0 +1,38 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/model-dycon.R +\name{hutch_g} +\alias{hutch_g} +\title{Dissonance factor (Hutchinson & Knopoff)} +\usage{ +hutch_g(y, cbw_cut_off = 1.2, a = 0.25, b = 2) +} +\arguments{ +\item{y}{(Numeric vector) Frequency distance in units of critical bandwidths.} + +\item{cbw_cut_off}{(Numeric scalar) +If not \code{NULL}, then should be a number +corresponding to the variable CBWcutoff in Mashinter's own implementation. +If \code{y >= cbw_cut_off}, then the dissonance factor will be approximated as 0. +Setting \code{cbw_cut_off} to 1.2 is necessary for replicating Mashinter's results. +A cut-off of 1.2 was also used by \insertCite{Bigand1996;textual}{incon}.} + +\item{a}{(Numeric scalar, default = 0.25) +Parameter from \insertCite{Mashinter2006;textual}{incon}.} + +\item{b}{(Numeric scalar, default = 2) +Parameter from \insertCite{Mashinter2006;textual}{incon}.} +} +\value{ +(Numeric vector) Dissonance factors. +} +\description{ +Computes dissonance factors given frequency distances in units of +critical bandwidths, after Mashinter's implementation of +Hutchinson & Knopoff's model +\insertCite{Mashinter2006,Hutchinson1978}{incon}. +This function corresponds to an approximation of the +look-up table in \insertCite{Plomp1965;textual}{incon}. +} +\references{ +\insertAllCited{} +} diff --git a/man/hutch_visualise.Rd b/man/hutch_visualise.Rd new file mode 100644 index 0000000..a285219 --- /dev/null +++ b/man/hutch_visualise.Rd @@ -0,0 +1,46 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/model-dycon.R +\name{hutch_visualise} +\alias{hutch_visualise} +\title{Visualise} +\usage{ +hutch_visualise( + x, + cbw_cut_off = 1.2, + a = 0.25, + b = 2, + label = "Roughness", + amplitude_breaks = c(0, 1), + colour_limits = c(0, 3), + colour_low = "darkblue", + colour_high = "red", + theme = hutch_visualise_theme, + ... +) +} +\arguments{ +\item{x}{Passed to \code{\link{roughness_hutch}}.} + +\item{cbw_cut_off}{Passed to \code{\link{roughness_hutch}}.} + +\item{a}{Passed to \code{\link{roughness_hutch}}.} + +\item{b}{Passed to \code{\link{roughness_hutch}}.} + +\item{label}{(Character scalar) x-axis label.} + +\item{amplitude_breaks}{Numeric vector of tick locations for the y-axis.} + +\item{colour_limits}{Defines the limits of the roughness scale.} + +\item{colour_low}{Colour to use for the lowest roughness.} + +\item{colour_high}{Colour to use for the highest roughness.} + +\item{theme}{\code{\link[ggplot2]{ggplot}} theme to use.} + +\item{...}{Passed to \code{\link[hrep]{sparse_fr_spectrum}}.} +} +\description{ +Creates a plot visualising computations for Hutchinson & Knopoff's model. +} diff --git a/man/hutch_visualise_theme.Rd b/man/hutch_visualise_theme.Rd new file mode 100644 index 0000000..2527132 --- /dev/null +++ b/man/hutch_visualise_theme.Rd @@ -0,0 +1,18 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/model-dycon.R +\docType{data} +\name{hutch_visualise_theme} +\alias{hutch_visualise_theme} +\title{ggplot theme} +\format{ +An object of class \code{theme} (inherits from \code{gg}) of length 97. +} +\usage{ +hutch_visualise_theme +} +\description{ +Defines a default theme for visualising computations +for Hutchinson & Knopoff's (1978) model +(see \code{\link{hutch_visualise}}). +} +\keyword{datasets} diff --git a/man/hutch_y.Rd b/man/hutch_y.Rd new file mode 100644 index 0000000..74f35fb --- /dev/null +++ b/man/hutch_y.Rd @@ -0,0 +1,24 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/model-dycon.R +\name{hutch_y} +\alias{hutch_y} +\title{Critical bandwidth distance (Hutchison & Knopoff)} +\usage{ +hutch_y(f1, f2) +} +\arguments{ +\item{f1}{(Numeric vector) Frequency 1, Hz} + +\item{f2}{(Numeric vector) Frequency 2, Hz} +} +\value{ +(Numeric vector) Unsigned distances in frequency bandwidths. +} +\description{ +Calculates the distance between pairs of frequencies in units of +critical bandwidths, according to the model of +\insertCite{Hutchinson1978;textual}{incon}. +} +\references{ +\insertAllCited{} +} diff --git a/man/roughness_hutch.Rd b/man/roughness_hutch.Rd new file mode 100644 index 0000000..57c269a --- /dev/null +++ b/man/roughness_hutch.Rd @@ -0,0 +1,69 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/model-dycon.R +\name{roughness_hutch} +\alias{roughness_hutch} +\alias{roughness_hutch.default} +\alias{roughness_hutch.sparse_fr_spectrum} +\title{Spectral roughness (Hutchinson & Knopoff)} +\usage{ +roughness_hutch( + x, + cbw_cut_off = 1.2, + a = 0.25, + b = 2, + dissonance_function = hutch_dissonance_function, + ... +) + +\method{roughness_hutch}{default}( + x, + cbw_cut_off = 1.2, + a = 0.25, + b = 2, + dissonance_function = hutch_dissonance_function, + ... +) + +\method{roughness_hutch}{sparse_fr_spectrum}( + x, + cbw_cut_off = 1.2, + a = 0.25, + b = 2, + dissonance_function = hutch_dissonance_function, + ... +) +} +\arguments{ +\item{x}{Object to analyse, which is coerced to the class +\code{\link[hrep]{sparse_fr_spectrum}}. +\itemize{ +\item Numeric vectors will be treated as vectors of MIDI note numbers, +and expanded into their implied harmonics. +\item Two-element lists will be treated as finalised spectra, +with the first element being a numeric vector of frequencies, +and the second element being a numeric vector of amplitudes. +}} + +\item{cbw_cut_off}{Parameter passed to \code{\link{hutch_g}()}.} + +\item{a}{Parameter passed to \code{\link{hutch_g}()}.} + +\item{b}{Parameter passed to \code{\link{hutch_g}()}.} + +\item{dissonance_function}{Function for computing dissonance contribution as a function of +critical bandwidth distance, defaulting to \code{\link{hutch_dissonance_function}}. +Custom functions may be specified here as long as they use the same parameter list +as the original function.} + +\item{...}{Further arguments to pass to \code{\link[hrep]{sparse_fr_spectrum}}.} +} +\value{ +Numeric scalar, identifying the roughness of the spectrum. +} +\description{ +Gets the roughness of a sonority according to the model of +\insertCite{Hutchinson1978;textual}{incon}. +} +\references{ +\insertAllCited{} +} diff --git a/man/roughness_seth.Rd b/man/roughness_seth.Rd new file mode 100644 index 0000000..f863e17 --- /dev/null +++ b/man/roughness_seth.Rd @@ -0,0 +1,55 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/model-dycon.R +\name{roughness_seth} +\alias{roughness_seth} +\alias{roughness_seth.default} +\alias{roughness_seth.sparse_fr_spectrum} +\title{Spectral roughness (Sethares)} +\usage{ +roughness_seth(x, min_amplitude = TRUE, ...) + +\method{roughness_seth}{default}(x, min_amplitude = TRUE, ...) + +\method{roughness_seth}{sparse_fr_spectrum}(x, min_amplitude = TRUE, ...) +} +\arguments{ +\item{x}{Object to analyse, which is coerced to the class +\code{\link[hrep]{sparse_fr_spectrum}}. +\itemize{ +\item Numeric vectors will be treated as vectors of MIDI note numbers, +and expanded into their implied harmonics. +\item Two-element lists will be treated as finalised spectra, +with the first element being a numeric vector of frequencies, +and the second element being a numeric vector of amplitudes. +}} + +\item{min_amplitude}{See \code{\link{dyad_roughness_seth}}.} + +\item{...}{Further arguments to pass to \code{\link[hrep]{sparse_fr_spectrum}}.} +} +\value{ +Estimated roughness, as a numeric scalar. +} +\description{ +Gets the roughness of a sonority according to the model of Sethares (1993). +By default, the algorithm is modified according to +\insertCite{Sethares2005;textual}{incon} and +\insertCite{Weisser2013;textual}{incon}: +roughness is proportional to the minimum amplitude of each pair of partials, +not the product of their amplitudes. +This behaviour can be disabled by setting \code{min_amplitude = FALSE}. +} +\note{ +\insertCite{Sethares2005;textual}{incon} +suggests using loudnesses instead of amplitudes. +However, he acknowledges that loudness is difficult to calculate +for arbitrary timbres. +Furthermore, if we replace amplitude with roughness, +we lose the original model's invariance to multiplicative +scaling of the original signal. +In this implementation, we therefore stay with amplitude, +consistent with \insertCite{Sethares1993;textual}{incon}. +} +\references{ +\insertAllCited{} +} diff --git a/man/roughness_vass.Rd b/man/roughness_vass.Rd new file mode 100644 index 0000000..379eff2 --- /dev/null +++ b/man/roughness_vass.Rd @@ -0,0 +1,38 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/model-dycon.R +\name{roughness_vass} +\alias{roughness_vass} +\alias{roughness_vass.default} +\alias{roughness_vass.sparse_fr_spectrum} +\title{Spectral roughness (Vassilakis)} +\usage{ +roughness_vass(x, ...) + +\method{roughness_vass}{default}(x, ...) + +\method{roughness_vass}{sparse_fr_spectrum}(x, ...) +} +\arguments{ +\item{x}{Object to analyse, which is coerced to the class +\code{\link[hrep]{sparse_fr_spectrum}}. +\itemize{ +\item Numeric vectors will be treated as vectors of MIDI note numbers, +and expanded into their implied harmonics. +\item Two-element lists will be treated as finalised spectra, +with the first element being a numeric vector of frequencies, +and the second element being a numeric vector of amplitudes. +}} + +\item{...}{Further arguments to pass to \code{\link[hrep]{sparse_fr_spectrum}}.} +} +\value{ +Estimated roughness, as a numeric scalar. +} +\description{ +Gets the roughness of a sonority according to the model of +\insertCite{Vassilakis2001;textual}{incon} +\insertCite{Villegas2010;textual}{incon} +} +\references{ +\insertAllCited{} +} diff --git a/tests/test-dycon.R b/tests/test-dycon.R new file mode 100644 index 0000000..53cdb6b --- /dev/null +++ b/tests/test-dycon.R @@ -0,0 +1,172 @@ +context("hutch") + +library(magrittr) + +test_that( + "hutch_g", { + expect_equal( + hutch_g( + 1, cbw_cut_off = 1.2 + ) %>% round(digits = 4), + 0.0397 + ) + expect_equal( + hutch_g( + 0.5, cbw_cut_off = 1.2 + ) %>% round(digits = 4), + 0.5413 + ) + expect_equal( + hutch_g( + 1.5, cbw_cut_off = NULL + ) %>% round(digits = 4), + 0.0016 + ) + expect_equal( + hutch_g( + 1.5, cbw_cut_off = 1.2 + ) %>% round(digits = 4), + 0 + ) + } +) + +test_that( + "hutch_cbw", { + expect_equal( + hutch_cbw(400, 440) %>% round(digits = 3), + 87.225 + ) + expect_equal( + hutch_cbw(400, 380) %>% round(digits = 3), + 83.123 + ) + } +) + +test_that("get_roughness_hutch", { + test_midi <- function(midi, expect, num_harmonics, tolerance = 1e-3) { + midi %>% + roughness_hutch(num_harmonics = num_harmonics) %>% + expect_equal(expect, tolerance = tolerance) + } + test_midi("60 61", 0.499, num_harmonics = 1) + test_midi("69 70", 0.491, num_harmonics = 1) + test_midi("60 61", 0.484, num_harmonics = 11) + test_midi("60 64 67", 0.120, num_harmonics = 11) + test_midi("60 63 67", 0.130, num_harmonics = 11) + + expect_equal( + roughness_hutch(c(60, 61), dissonance_function = function(...) 0), + 0 + ) +}) + +context("test-sethares") + +test_that("Regression tests generated from Sethares's implementation", { + # See below for MATLAB implementation of Sethares's (1993) model, + # sourced from http://sethares.engr.wisc.edu/comprog.html. + # To reproduce the exact results, it would be necessary + # to adjust the parameters slightly: s1 = 0.0207, s2 = 18.96, a = 3.51 + # Note that Sethares's implementation also introduces an arbitrary + # scaling factor, which we need to compensate for in our testing. + f <- function(frequency, amplitude, ref = TRUE) { + x <- roughness_seth(hrep::sparse_fr_spectrum(list(frequency, amplitude))) + if (ref) { + x / f(frequency = c(440, 460), amplitude = c(1, 1), ref = FALSE) + } else x + } + + # MATLAB: + # dissmeasure([440, 460, 480], [1, 1, 1]) / dissmeasure([440, 460], [1, 1]) + expect_equal(f(c(440, 460, 480), c(1, 1, 1)), 2.9194, tolerance = 1e-2) + + expect_equal(f(c(440, 460, 480), c(1, 2, 3)), 3.9161, tolerance = 1e-2) + expect_equal(f(c(440, 460, 480), c(3, 2, 1)), 3.9194, tolerance = 1e-2) + expect_equal(f(c(300, 250, 275, 425), c(1.5, 2, 9, 4)), 4.8657, tolerance = 1e-2) +}) + +# Sethares's MATLAB code: +# http://sethares.engr.wisc.edu/comprog.html + +# function d=dissmeasure(fvec,amp) +# % +# % given a set of partials in fvec, +# % with amplitudes in amp, +# % this routine calculates the dissonance +# % +# Dstar=0.24; S1=0.0207; S2=18.96; C1=5; C2=-5; +# A1=-3.51; A2=-5.75; firstpass=1; +# N=length(fvec); +# [fvec,ind]=sort(fvec); +# ams=amp(ind); +# D=0; +# for i=2:N +# Fmin=fvec(1:N-i+1); +# S=Dstar./(S1*Fmin+S2); +# Fdif=fvec(i:N)-fvec(1:N-i+1); +# a=min(ams(i:N),ams(1:N-i+1)); +# Dnew=a.*(C1*exp(A1*S.*Fdif)+C2*exp(A2*S.*Fdif)); +# D=D+Dnew*ones(size(Dnew))'; +# end +# d=D; + +context("test-vass") + +library(magrittr) +library(tibble) + +test_that("Comparing model outputs to Vassilakis (2001, p. 210)", { + # This table comes from p. 208 + .f <- tribble( + ~ f1, ~ f2, ~ f3, ~ f4, ~ f5, ~ f6, + 262, 526, 790, 1049, 1318, 1573, + 277, 554, 837, 1118, 1398, 1677, + 294, 590, 886, 1180, 1473, 1772, + 311, 624, 932, 1244, 1569, 1873, + 330, 663, 995, 1323, 1654, 1994, + 349, 701, 1053, 1408, 1751, 2107, + 370, 741, 1118, 1482, 1852, 2235, + 392, 783, 1179, 1570, 1973, 2373, + 415, 834, 1250, 1670, 2093, 2499, + 440, 884, 1329, 1768, 2200, 2666, + 466, 937, 1400, 1874, 2345, 2799, + 494, 990, 1484, 1985, 2476, 2973, + 524, 1052, 1573, 2110, 2634, 3154 + ) + .a <- 1 / 1:6 + get_tone <- function(pc) { + hrep::sparse_fr_spectrum(list(as.numeric(.f[pc + 1, ]), .a)) + } + get_dyad <- function(pc_1, pc_2) { + hrep::combine_sparse_spectra(get_tone(pc_1), + get_tone(pc_2)) + } + get_dyad_roughness <- function(pc_1, pc_2) { + get_dyad(pc_1, pc_2) %>% + {roughness_vass(list(hrep::freq(.), hrep::amp(.)))} + } + + # These results come from p. 210 + res <- tibble(int = 2:12, + old = 40.383 / + c(27.617, 18.117, 16.002, + 11.446, 12.826, 6.17877, + 10.103, 5.782, 6.214, + 6.996, 1.589)) + res$new <- vapply(res$int, function(x) { + get_dyad_roughness(0, 1) / get_dyad_roughness(0, x) + }, numeric(1)) + res + + expect_gt(cor(res$old, res$new), 0.998) +}) + +test_that("zero amplitudes", { + # The original version sometimes returned NaN when the spectrum contained + # amplitudes of magnitude zero + spectrum <- hrep:::.sparse_fr_spectrum(frequency = c(400, 420, 440, 600), + amplitude = c(1, 0, 0, 1)) + expect_false(is.na(roughness_vass(spectrum))) +}) From fe344a5c8480e9188a417bace9a0120eb8507b38 Mon Sep 17 00:00:00 2001 From: Peter Harrison Date: Tue, 23 Jan 2024 22:29:50 +0000 Subject: [PATCH 11/24] Add har18 --- DESCRIPTION | 4 +- NAMESPACE | 15 ++ R/RcppExports.R | 19 +++ R/imports.R | 10 ++ R/model-har-2019.R | 4 +- R/model-har18.R | 264 ++++++++++++++++++++++++++++++ R/models.R | 12 +- R/top-level.R | 10 +- inst/REFERENCES.bib | 29 ++++ man/cosine_similarity.Rd | 19 +++ man/har_19_composite.Rd | 4 +- man/incon.Rd | 10 +- man/kl_div_from_uniform.Rd | 22 +++ man/pc_harmonicity.Rd | 102 ++++++++++++ man/peak.Rd | 20 +++ man/sweep_harmonic_template.Rd | 49 ++++++ man/sweep_template.Rd | 25 +++ src/.gitignore | 3 + src/RcppExports.cpp | 75 +++++++++ src/code.cpp | 61 +++++++ tests/{ => testthat}/test-dycon.R | 0 tests/testthat/test-har18.R | 175 ++++++++++++++++++++ 22 files changed, 911 insertions(+), 21 deletions(-) create mode 100644 R/RcppExports.R create mode 100644 R/model-har18.R create mode 100644 man/cosine_similarity.Rd create mode 100644 man/kl_div_from_uniform.Rd create mode 100644 man/pc_harmonicity.Rd create mode 100644 man/peak.Rd create mode 100644 man/sweep_harmonic_template.Rd create mode 100644 man/sweep_template.Rd create mode 100644 src/.gitignore create mode 100644 src/RcppExports.cpp create mode 100644 src/code.cpp rename tests/{ => testthat}/test-dycon.R (100%) create mode 100644 tests/testthat/test-har18.R diff --git a/DESCRIPTION b/DESCRIPTION index 6979d44..9ee92d0 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -27,10 +27,12 @@ Suggests: testthat (>= 2.1.1), knitr (>= 1.23), DT (>= 0.5), - covr (>= 3.2.1) + covr (>= 3.2.1), ggplot2 (>= 3.1.0.9000) RoxygenNote: 7.3.1 Remotes: pmcharrison/hrep VignetteBuilder: knitr Byte-Compile: yes +LinkingTo: + Rcpp diff --git a/NAMESPACE b/NAMESPACE index f767c83..84804a9 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -7,6 +7,11 @@ S3method(count_chords,corpus) S3method(huron_1994,default) S3method(huron_1994,int_vec) S3method(huron_1994,pc_set) +S3method(kl_div_from_uniform,smooth_spectrum) +S3method(pc_harmonicity,default) +S3method(pc_harmonicity,milne_pc_spectrum) +S3method(pc_harmonicity,pc_set) +S3method(peak,milne_pc_spectrum) S3method(print,corpus_dissonance_table) S3method(roughness_hutch,default) S3method(roughness_hutch,sparse_fr_spectrum) @@ -16,10 +21,13 @@ S3method(roughness_vass,default) S3method(roughness_vass,sparse_fr_spectrum) S3method(roughness_wang,default) S3method(roughness_wang,sparse_fr_spectrum) +S3method(sweep_harmonic_template,milne_pc_spectrum) +S3method(sweep_harmonic_template,pc_set) S3method(type,corpus_dissonance_table) export(bowl18_min_freq_dist) export(corpus_dissonance) export(corpus_dissonance_table) +export(cosine_similarity) export(count_chords) export(demo_wang) export(gill09_harmonicity) @@ -34,12 +42,18 @@ export(hutch_visualise_theme) export(hutch_y) export(incon) export(incon_models) +export(kl_div_from_uniform) export(list_models) +export(pc_harmonicity) +export(peak) export(roughness_hutch) export(roughness_seth) export(roughness_vass) export(roughness_wang) +export(sweep_harmonic_template) +export(sweep_template) export(type) +importFrom(Rcpp,sourceCpp) importFrom(magrittr,"%>%") importFrom(methods,"is") importFrom(rlang,".data") @@ -49,3 +63,4 @@ importFrom(stats,"fft") importFrom(tibble,tibble) importFrom(utils,"capture.output") importFrom(zeallot,"%<-%") +useDynLib(incon, .registration = TRUE) diff --git a/R/RcppExports.R b/R/RcppExports.R new file mode 100644 index 0000000..42afe10 --- /dev/null +++ b/R/RcppExports.R @@ -0,0 +1,19 @@ +# Generated by using Rcpp::compileAttributes() -> do not edit by hand +# Generator token: 10BE3573-1514-4C36-9D1C-5A225CD40393 + +mod <- function(x, base) { + .Call(`_incon_mod`, x, base) +} + +get_index <- function(x, index, offset) { + .Call(`_incon_get_index`, x, index, offset) +} + +cosine_similarity_cpp <- function(x, template_, offset) { + .Call(`_incon_cosine_similarity_cpp`, x, template_, offset) +} + +sweep_template_cpp <- function(x, template_) { + .Call(`_incon_sweep_template_cpp`, x, template_) +} + diff --git a/R/imports.R b/R/imports.R index eb38036..74c75ee 100644 --- a/R/imports.R +++ b/R/imports.R @@ -1,3 +1,13 @@ +## usethis namespace: start +#' @useDynLib incon, .registration = TRUE +## usethis namespace: end +NULL + +## usethis namespace: start +#' @importFrom Rcpp sourceCpp +## usethis namespace: end +NULL + #' @importFrom magrittr "%>%" NULL diff --git a/R/model-har-2019.R b/R/model-har-2019.R index 08d95c9..bd73bfd 100644 --- a/R/model-har-2019.R +++ b/R/model-har-2019.R @@ -25,8 +25,8 @@ har_19_composite_coef <- tibble::tribble( #' the roughness model of \insertCite{Hutchinson1978;textual}{dycon} #' (see \code{dycon::\link[dycon]{roughness_hutch}}); #' - `har_18_harmonicity`, -#' the harmonicity model of \insertCite{Harrison2018;textual}{har18} -#' (see \code{har18::\link[har18]{pc_harmonicity}}); +#' the harmonicity model of \insertCite{Harrison2018;textual}{incon} +#' (see \code{incon::\link[incon]{pc_harmonicity}}); #' - `har_19_corpus`: #' a corpus-based model of cultural familiarity #' (Harrison & Pearce, in preparation) diff --git a/R/model-har18.R b/R/model-har18.R new file mode 100644 index 0000000..e1555c6 --- /dev/null +++ b/R/model-har18.R @@ -0,0 +1,264 @@ +#' Cosine similarity +#' +#' Computes the cosine similarity between two numeric vectors. +#' @param x Numeric vector 1. +#' @param y Numeric vector 2. +#' @return Cosine similarity, as a numeric scalar. +#' @export +cosine_similarity <- function(x, y) { + numerator <- sum(x * y) + denominator <- + sqrt(sum(x ^ 2)) * + sqrt(sum(y ^ 2)) + numerator / denominator +} + +#' Peak +#' +#' Gets the peak value of an object. +#' @param x Object to analyse. +#' @return The object's peak value, as a numeric scalar. +#' @rdname peak +#' @export +peak <- function(x) { + UseMethod("peak") +} + +#' @rdname peak +#' @export +peak.milne_pc_spectrum <- function(x) { + max(as.numeric(x)) +} + +#' Kullback-Leibler divergence from uniform +#' +#' Gets the Kullback-Leibler divergence of a provided distribution +#' from a uniform distribution. +#' @param x Input distribution. +#' @return The Kullback-Leibler divergence from a uniform distribution +#' to the input distribution. +#' @rdname kl_div_from_uniform +#' @export +kl_div_from_uniform <- function(x) { + UseMethod("kl_div_from_uniform") +} + +#' @rdname kl_div_from_uniform +#' @export +kl_div_from_uniform.smooth_spectrum <- function(x) { + # Construct a probability vector, where each bin corresponds to + # the probability of a discrete event + x <- as.numeric(x) + probs <- x / sum(x) + n <- length(probs) + uniform_probs <- 1 / n + non_zero_probs <- probs[probs > 0] + sum( + non_zero_probs * log(non_zero_probs / uniform_probs, base = 2) + ) +} + +#' Pitch-class harmonicity +#' +#' Gets the pitch-class harmonicity of an input sonority, after +#' \insertCite{Harrison2018;textual}{incon} and +#' \insertCite{Milne2013;textual}{incon}. +#' @param x Object to analyse. +#' @param method (Character scalar) Method to use. +#' * \code{"kl"} (default) delivers the Kullback-Leibler method of +#' \insertCite{Harrison2018;textual}{incon}. +#' * \code{"peak"} delivers the peak-value method of +#' \insertCite{Milne2013;textual}{incon}. +#' @param num_harmonics (Integerish scalar) +#' Number of harmonics to use when expanding tones into their implied harmonics, +#' and when defining the harmonic template +#' (including the fundamental frequency). +#' Defaults to 12, after +#' \insertCite{Milne2016;textual}{incon}. +#' @param rho (Numeric scalar) +#' Roll-off parameter for harmonic expansion. +#' Defaults to 0.75, after +#' \insertCite{Milne2016;textual}{incon}. +#' @param sigma (Numeric scalar) +#' Standard deviation of the Gaussian smoothing distribution (cents). +#' Defaults to 6.83, after +#' \insertCite{Milne2016;textual}{incon}. +#' @param array_dim (Integerish scalar) +#' Dimensionality of the pitch-class spectrum array. +#' Defaults to 1200, after +#' \insertCite{Milne2016;textual}{incon}. +#' @param ... Arguments passed to specific methods. +#' @return Pitch-class harmonicity, as a numeric scalar. +#' @note This algorithm makes use of \code{\link[hrep]{milne_pc_spectrum}()} +#' as defined in the \code{hrep} package. +#' @md +#' @references +#' \insertAllCited{} +#' @examples +#' pc_harmonicity(c(0, 4, 7)) +#' pc_harmonicity(c(0, 3, 7)) +#' pc_harmonicity(c(0, 3, 6)) +#' @rdname pc_harmonicity +#' @export +pc_harmonicity <- function(x, + method = "kl", + num_harmonics = 12, + rho = 0.75, + sigma = 6.83, + ...) { + UseMethod("pc_harmonicity") +} + +#' @rdname pc_harmonicity +#' @export +pc_harmonicity.default <- function(x, + method = "kl", + num_harmonics = 12, + rho = 0.75, + sigma = 6.83, + array_dim = 1200, + ...) { + x <- hrep::pc_set(x) + do.call(pc_harmonicity, as.list(environment())) +} + +#' @rdname pc_harmonicity +#' @export +pc_harmonicity.pc_set <- function(x, + method = "kl", + num_harmonics = 12, + rho = 0.75, + sigma = 6.83, + array_dim = 1200, + ...) { + checkmate::qassert(method, "S1") + + x <- hrep::milne_pc_spectrum(x, + num_harmonics = num_harmonics, + rho = rho, + sigma = sigma, + array_dim = array_dim) + pc_harmonicity(x, + method = method, + num_harmonics = num_harmonics, + rho = rho, + sigma = sigma) +} + +#' @rdname pc_harmonicity +#' @export +pc_harmonicity.milne_pc_spectrum <- function(x, + method = "kl", + num_harmonics = 12, + rho = 0.75, + sigma = 6.83, + ...) { + checkmate::qassert(method, "S1") + + y <- sweep_harmonic_template(x, + num_harmonics = num_harmonics, + rho = rho, + sigma = sigma) + if (method == "kl") { + kl_div_from_uniform(y) + } else if (method == "peak") { + peak(y) + } else stop("unrecognised method") +} + +#' Sweep harmonic template +#' +#' Sweeps a harmonic template over an input spectrum. +#' @param x Object to analyse. +#' @param num_harmonics See \code{\link{pc_harmonicity}}. +#' @param rho See \code{\link{pc_harmonicity}}. +#' @param sigma See \code{\link{pc_harmonicity}}. +#' @param array_dim See \code{\link{pc_harmonicity}}. +#' @param ... Arguments passed to specific methods. +#' @return An object of class \code{\link[hrep]{milne_pc_spectrum}}, +#' identifying each pitch class with a perceptual weight +#' corresponding to its harmonic template fit. +#' @rdname sweep_harmonic_template +#' @export +sweep_harmonic_template <- function(x, + num_harmonics = 12, + rho = 0.75, + sigma = 6.83, + array_dim = 1200, + ...) { + UseMethod("sweep_harmonic_template") +} + +#' @rdname sweep_harmonic_template +#' @export +sweep_harmonic_template.pc_set <- function(x, + num_harmonics = 12, + rho = 0.75, + sigma = 6.83, + array_dim = 1200, + ...) { + hrep::milne_pc_spectrum(x, + num_harmonics = num_harmonics, + rho = rho, + sigma = sigma, + array_dim = array_dim) %>% + sweep_harmonic_template(num_harmonics = num_harmonics, + rho = rho, + sigma = sigma) +} + +#' @rdname sweep_harmonic_template +#' @export +sweep_harmonic_template.milne_pc_spectrum <- function(x, + num_harmonics = 12, + rho = 0.75, + sigma = 6.83, + ...) { + x <- as.numeric(x) + array_dim <- length(x) + template <- hrep::milne_pc_spectrum(hrep::pc_set(0), + array_dim = array_dim, + num_harmonics = num_harmonics, + rho = rho, + sigma = sigma) + res <- sweep_template(x, template) + + hrep::.milne_pc_spectrum(res) +} + +#' Sweep template +#' +#' Sweeps a circular template over a circular vector +#' and computes the cosine similarity at each possible offset. +#' +#' @param x +#' (Numeric vector) +#' The vector to be swept over. +#' +#' @param template +#' (Numeric vector) +#' The template to sweep over \code{x}. +#' Should have the same dimensionality as \code{x}. +#' +#' @param legacy +#' (Logical scalar) +#' Whether to use the legacy R implementation +#' (default = \code{FALSE}). +#' Otherwise the faster C++ implementation is used. +#' +#' @export +sweep_template <- function(x, template, legacy = FALSE) { + if (!legacy) { + return(sweep_template_cpp(x, template)) + } + + array_dim <- length(x) + res <- numeric(array_dim) + + for (i in seq_len(array_dim)) { + indices <- 1 + (seq(from = i - 1, length.out = array_dim) %% array_dim) + res[i] <- cosine_similarity(template, x[indices]) + } + + res +} diff --git a/R/models.R b/R/models.R index bef316f..6a3273e 100644 --- a/R/models.R +++ b/R/models.R @@ -74,16 +74,16 @@ add_model("gill_09_harmonicity", add_model("har_18_harmonicity", "Harrison & Pearce (2018)", "Periodicity/harmonicity", - "har18", + "incon", consonance = TRUE, spectrum_sensitive = TRUE, continuous_pitch = TRUE, f = function(x, num_harmonics, roll_off, ...) - har18::pc_harmonicity(x, - method = "kl", - num_harmonics = num_harmonics, - rho = roll_off * 0.75, - ...)) + pc_harmonicity(x, + method = "kl", + num_harmonics = num_harmonics, + rho = roll_off * 0.75, + ...)) add_model("milne_13_harmonicity", "Milne (2013)", diff --git a/R/top-level.R b/R/top-level.R index 31faa2c..dfd0f92 100644 --- a/R/top-level.R +++ b/R/top-level.R @@ -61,11 +61,11 @@ #' the harmonicity model of \insertCite{Gill2009;textual}{incon} #' (see \code{incon::\link[incon]{gill09_harmonicity}}). #' * `har_18_harmonicity`: -#' the harmonicity model of \insertCite{Harrison2018;textual}{har18} -#' (see \code{har18::\link[har18]{pc_harmonicity}}). +#' the harmonicity model of \insertCite{Harrison2018;textual}{incon} +#' (see \code{incon::\link[incon]{pc_harmonicity}}). #' * `milne_13_harmonicity`: -#' the harmonicity model of \insertCite{Milne2013;textual}{har18} -#' (see \code{har18::\link[har18]{pc_harmonicity}}). +#' the harmonicity model of \insertCite{Milne2013;textual}{incon} +#' (see \code{incon::\link[incon]{pc_harmonicity}}). #' * `parn_88_root_ambig`: #' the root ambiguity model of \insertCite{Parncutt1988;textual}{parn88} #' (see \code{parn88::\link[parn88]{root_ambiguity}}). @@ -109,7 +109,7 @@ #' (see \code{parn94::\link[parn94]{multiplicity}}). #' * `har_19_composite`: #' a model combining interference \insertCite{Hutchinson1978}{dycon}, -#' periodicity/harmonicity \insertCite{Harrison2018}{har18}, +#' periodicity/harmonicity \insertCite{Harrison2018}{incon}, #' and cultural familiarity, #' as introduced by \insertCite{Harrison2019;textual}{incon}. #' @references diff --git a/inst/REFERENCES.bib b/inst/REFERENCES.bib index 33f2323..f71f4c9 100644 --- a/inst/REFERENCES.bib +++ b/inst/REFERENCES.bib @@ -31,3 +31,32 @@ @article{Wang2013 volume = {332}, year = {2013} } + +@inproceedings{Harrison2018, +address = {Paris, France}, +author = {Harrison, Peter M. C. and Pearce, Marcus T.}, +booktitle = {Proceedings of the 19th International Society for Music Information Retrieval Conference}, +pages = {160--167}, +title = {{An energy-based generative sequence model for testing sensory theories of Western harmony}}, +year = {2018} +} + +@phdthesis{Milne2013, +address = {Milton Keynes, England}, +author = {Milne, Andrew J.}, +mendeley-groups = {Harmony,Harmony/Psychoacoustic interpretation}, +school = {The Open University}, +title = {{A computational model of the cognition of tonality}}, +year = {2013} +} + +@article{Milne2016, +author = {Milne, Andrew J. and Holland, Simon}, +doi = {10.1080/17459737.2016.1152517}, +journal = {Journal of Mathematics and Music}, +number = {1}, +pages = {59--85}, +title = {{Empirically testing Tonnetz, voice-leading, and spectral models of perceived triadic distance}}, +volume = {10}, +year = {2016} +} diff --git a/man/cosine_similarity.Rd b/man/cosine_similarity.Rd new file mode 100644 index 0000000..56a7edf --- /dev/null +++ b/man/cosine_similarity.Rd @@ -0,0 +1,19 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/model-har18.R +\name{cosine_similarity} +\alias{cosine_similarity} +\title{Cosine similarity} +\usage{ +cosine_similarity(x, y) +} +\arguments{ +\item{x}{Numeric vector 1.} + +\item{y}{Numeric vector 2.} +} +\value{ +Cosine similarity, as a numeric scalar. +} +\description{ +Computes the cosine similarity between two numeric vectors. +} diff --git a/man/har_19_composite.Rd b/man/har_19_composite.Rd index cd3229f..2338b23 100644 --- a/man/har_19_composite.Rd +++ b/man/har_19_composite.Rd @@ -34,8 +34,8 @@ The model combines several sub-models: the roughness model of \insertCite{Hutchinson1978;textual}{dycon} (see \code{dycon::\link[dycon]{roughness_hutch}}); \item \code{har_18_harmonicity}, -the harmonicity model of \insertCite{Harrison2018;textual}{har18} -(see \code{har18::\link[har18]{pc_harmonicity}}); +the harmonicity model of \insertCite{Harrison2018;textual}{incon} +(see \code{incon::\link[incon]{pc_harmonicity}}); \item \code{har_19_corpus}: a corpus-based model of cultural familiarity (Harrison & Pearce, in preparation) diff --git a/man/incon.Rd b/man/incon.Rd index 12bc8eb..43bab7d 100644 --- a/man/incon.Rd +++ b/man/incon.Rd @@ -70,11 +70,11 @@ The following models are available: the harmonicity model of \insertCite{Gill2009;textual}{incon} (see \code{incon::\link[incon]{gill09_harmonicity}}). \item \code{har_18_harmonicity}: -the harmonicity model of \insertCite{Harrison2018;textual}{har18} -(see \code{har18::\link[har18]{pc_harmonicity}}). +the harmonicity model of \insertCite{Harrison2018;textual}{incon} +(see \code{incon::\link[incon]{pc_harmonicity}}). \item \code{milne_13_harmonicity}: -the harmonicity model of \insertCite{Milne2013;textual}{har18} -(see \code{har18::\link[har18]{pc_harmonicity}}). +the harmonicity model of \insertCite{Milne2013;textual}{incon} +(see \code{incon::\link[incon]{pc_harmonicity}}). \item \code{parn_88_root_ambig}: the root ambiguity model of \insertCite{Parncutt1988;textual}{parn88} (see \code{parn88::\link[parn88]{root_ambiguity}}). @@ -118,7 +118,7 @@ the multiplicity feature of \insertCite{Parncutt1994;textual}{parn94} (see \code{parn94::\link[parn94]{multiplicity}}). \item \code{har_19_composite}: a model combining interference \insertCite{Hutchinson1978}{dycon}, -periodicity/harmonicity \insertCite{Harrison2018}{har18}, +periodicity/harmonicity \insertCite{Harrison2018}{incon}, and cultural familiarity, as introduced by \insertCite{Harrison2019;textual}{incon}. } diff --git a/man/kl_div_from_uniform.Rd b/man/kl_div_from_uniform.Rd new file mode 100644 index 0000000..5c2478c --- /dev/null +++ b/man/kl_div_from_uniform.Rd @@ -0,0 +1,22 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/model-har18.R +\name{kl_div_from_uniform} +\alias{kl_div_from_uniform} +\alias{kl_div_from_uniform.smooth_spectrum} +\title{Kullback-Leibler divergence from uniform} +\usage{ +kl_div_from_uniform(x) + +\method{kl_div_from_uniform}{smooth_spectrum}(x) +} +\arguments{ +\item{x}{Input distribution.} +} +\value{ +The Kullback-Leibler divergence from a uniform distribution +to the input distribution. +} +\description{ +Gets the Kullback-Leibler divergence of a provided distribution +from a uniform distribution. +} diff --git a/man/pc_harmonicity.Rd b/man/pc_harmonicity.Rd new file mode 100644 index 0000000..6c9c7e6 --- /dev/null +++ b/man/pc_harmonicity.Rd @@ -0,0 +1,102 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/model-har18.R +\name{pc_harmonicity} +\alias{pc_harmonicity} +\alias{pc_harmonicity.default} +\alias{pc_harmonicity.pc_set} +\alias{pc_harmonicity.milne_pc_spectrum} +\title{Pitch-class harmonicity} +\usage{ +pc_harmonicity( + x, + method = "kl", + num_harmonics = 12, + rho = 0.75, + sigma = 6.83, + ... +) + +\method{pc_harmonicity}{default}( + x, + method = "kl", + num_harmonics = 12, + rho = 0.75, + sigma = 6.83, + array_dim = 1200, + ... +) + +\method{pc_harmonicity}{pc_set}( + x, + method = "kl", + num_harmonics = 12, + rho = 0.75, + sigma = 6.83, + array_dim = 1200, + ... +) + +\method{pc_harmonicity}{milne_pc_spectrum}( + x, + method = "kl", + num_harmonics = 12, + rho = 0.75, + sigma = 6.83, + ... +) +} +\arguments{ +\item{x}{Object to analyse.} + +\item{method}{(Character scalar) Method to use. +\itemize{ +\item \code{"kl"} (default) delivers the Kullback-Leibler method of +\insertCite{Harrison2018;textual}{incon}. +\item \code{"peak"} delivers the peak-value method of +\insertCite{Milne2013;textual}{incon}. +}} + +\item{num_harmonics}{(Integerish scalar) +Number of harmonics to use when expanding tones into their implied harmonics, +and when defining the harmonic template +(including the fundamental frequency). +Defaults to 12, after +\insertCite{Milne2016;textual}{incon}.} + +\item{rho}{(Numeric scalar) +Roll-off parameter for harmonic expansion. +Defaults to 0.75, after +\insertCite{Milne2016;textual}{incon}.} + +\item{sigma}{(Numeric scalar) +Standard deviation of the Gaussian smoothing distribution (cents). +Defaults to 6.83, after +\insertCite{Milne2016;textual}{incon}.} + +\item{...}{Arguments passed to specific methods.} + +\item{array_dim}{(Integerish scalar) +Dimensionality of the pitch-class spectrum array. +Defaults to 1200, after +\insertCite{Milne2016;textual}{incon}.} +} +\value{ +Pitch-class harmonicity, as a numeric scalar. +} +\description{ +Gets the pitch-class harmonicity of an input sonority, after +\insertCite{Harrison2018;textual}{incon} and +\insertCite{Milne2013;textual}{incon}. +} +\note{ +This algorithm makes use of \code{\link[hrep]{milne_pc_spectrum}()} +as defined in the \code{hrep} package. +} +\examples{ +pc_harmonicity(c(0, 4, 7)) +pc_harmonicity(c(0, 3, 7)) +pc_harmonicity(c(0, 3, 6)) +} +\references{ +\insertAllCited{} +} diff --git a/man/peak.Rd b/man/peak.Rd new file mode 100644 index 0000000..6124cc0 --- /dev/null +++ b/man/peak.Rd @@ -0,0 +1,20 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/model-har18.R +\name{peak} +\alias{peak} +\alias{peak.milne_pc_spectrum} +\title{Peak} +\usage{ +peak(x) + +\method{peak}{milne_pc_spectrum}(x) +} +\arguments{ +\item{x}{Object to analyse.} +} +\value{ +The object's peak value, as a numeric scalar. +} +\description{ +Gets the peak value of an object. +} diff --git a/man/sweep_harmonic_template.Rd b/man/sweep_harmonic_template.Rd new file mode 100644 index 0000000..c90b150 --- /dev/null +++ b/man/sweep_harmonic_template.Rd @@ -0,0 +1,49 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/model-har18.R +\name{sweep_harmonic_template} +\alias{sweep_harmonic_template} +\alias{sweep_harmonic_template.pc_set} +\alias{sweep_harmonic_template.milne_pc_spectrum} +\title{Sweep harmonic template} +\usage{ +sweep_harmonic_template( + x, + num_harmonics = 12, + rho = 0.75, + sigma = 6.83, + array_dim = 1200, + ... +) + +\method{sweep_harmonic_template}{pc_set}( + x, + num_harmonics = 12, + rho = 0.75, + sigma = 6.83, + array_dim = 1200, + ... +) + +\method{sweep_harmonic_template}{milne_pc_spectrum}(x, num_harmonics = 12, rho = 0.75, sigma = 6.83, ...) +} +\arguments{ +\item{x}{Object to analyse.} + +\item{num_harmonics}{See \code{\link{pc_harmonicity}}.} + +\item{rho}{See \code{\link{pc_harmonicity}}.} + +\item{sigma}{See \code{\link{pc_harmonicity}}.} + +\item{array_dim}{See \code{\link{pc_harmonicity}}.} + +\item{...}{Arguments passed to specific methods.} +} +\value{ +An object of class \code{\link[hrep]{milne_pc_spectrum}}, +identifying each pitch class with a perceptual weight +corresponding to its harmonic template fit. +} +\description{ +Sweeps a harmonic template over an input spectrum. +} diff --git a/man/sweep_template.Rd b/man/sweep_template.Rd new file mode 100644 index 0000000..6b15725 --- /dev/null +++ b/man/sweep_template.Rd @@ -0,0 +1,25 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/model-har18.R +\name{sweep_template} +\alias{sweep_template} +\title{Sweep template} +\usage{ +sweep_template(x, template, legacy = FALSE) +} +\arguments{ +\item{x}{(Numeric vector) +The vector to be swept over.} + +\item{template}{(Numeric vector) +The template to sweep over \code{x}. +Should have the same dimensionality as \code{x}.} + +\item{legacy}{(Logical scalar) +Whether to use the legacy R implementation +(default = \code{FALSE}). +Otherwise the faster C++ implementation is used.} +} +\description{ +Sweeps a circular template over a circular vector +and computes the cosine similarity at each possible offset. +} diff --git a/src/.gitignore b/src/.gitignore new file mode 100644 index 0000000..22034c4 --- /dev/null +++ b/src/.gitignore @@ -0,0 +1,3 @@ +*.o +*.so +*.dll diff --git a/src/RcppExports.cpp b/src/RcppExports.cpp new file mode 100644 index 0000000..c33a35a --- /dev/null +++ b/src/RcppExports.cpp @@ -0,0 +1,75 @@ +// Generated by using Rcpp::compileAttributes() -> do not edit by hand +// Generator token: 10BE3573-1514-4C36-9D1C-5A225CD40393 + +#include + +using namespace Rcpp; + +#ifdef RCPP_USE_GLOBAL_ROSTREAM +Rcpp::Rostream& Rcpp::Rcout = Rcpp::Rcpp_cout_get(); +Rcpp::Rostream& Rcpp::Rcerr = Rcpp::Rcpp_cerr_get(); +#endif + +// mod +double mod(double x, int base); +RcppExport SEXP _incon_mod(SEXP xSEXP, SEXP baseSEXP) { +BEGIN_RCPP + Rcpp::RObject rcpp_result_gen; + Rcpp::RNGScope rcpp_rngScope_gen; + Rcpp::traits::input_parameter< double >::type x(xSEXP); + Rcpp::traits::input_parameter< int >::type base(baseSEXP); + rcpp_result_gen = Rcpp::wrap(mod(x, base)); + return rcpp_result_gen; +END_RCPP +} +// get_index +double get_index(NumericVector& x, int index, int offset); +RcppExport SEXP _incon_get_index(SEXP xSEXP, SEXP indexSEXP, SEXP offsetSEXP) { +BEGIN_RCPP + Rcpp::RObject rcpp_result_gen; + Rcpp::RNGScope rcpp_rngScope_gen; + Rcpp::traits::input_parameter< NumericVector& >::type x(xSEXP); + Rcpp::traits::input_parameter< int >::type index(indexSEXP); + Rcpp::traits::input_parameter< int >::type offset(offsetSEXP); + rcpp_result_gen = Rcpp::wrap(get_index(x, index, offset)); + return rcpp_result_gen; +END_RCPP +} +// cosine_similarity_cpp +double cosine_similarity_cpp(NumericVector& x, NumericVector& template_, int offset); +RcppExport SEXP _incon_cosine_similarity_cpp(SEXP xSEXP, SEXP template_SEXP, SEXP offsetSEXP) { +BEGIN_RCPP + Rcpp::RObject rcpp_result_gen; + Rcpp::RNGScope rcpp_rngScope_gen; + Rcpp::traits::input_parameter< NumericVector& >::type x(xSEXP); + Rcpp::traits::input_parameter< NumericVector& >::type template_(template_SEXP); + Rcpp::traits::input_parameter< int >::type offset(offsetSEXP); + rcpp_result_gen = Rcpp::wrap(cosine_similarity_cpp(x, template_, offset)); + return rcpp_result_gen; +END_RCPP +} +// sweep_template_cpp +NumericVector sweep_template_cpp(NumericVector& x, NumericVector& template_); +RcppExport SEXP _incon_sweep_template_cpp(SEXP xSEXP, SEXP template_SEXP) { +BEGIN_RCPP + Rcpp::RObject rcpp_result_gen; + Rcpp::RNGScope rcpp_rngScope_gen; + Rcpp::traits::input_parameter< NumericVector& >::type x(xSEXP); + Rcpp::traits::input_parameter< NumericVector& >::type template_(template_SEXP); + rcpp_result_gen = Rcpp::wrap(sweep_template_cpp(x, template_)); + return rcpp_result_gen; +END_RCPP +} + +static const R_CallMethodDef CallEntries[] = { + {"_incon_mod", (DL_FUNC) &_incon_mod, 2}, + {"_incon_get_index", (DL_FUNC) &_incon_get_index, 3}, + {"_incon_cosine_similarity_cpp", (DL_FUNC) &_incon_cosine_similarity_cpp, 3}, + {"_incon_sweep_template_cpp", (DL_FUNC) &_incon_sweep_template_cpp, 2}, + {NULL, NULL, 0} +}; + +RcppExport void R_init_incon(DllInfo *dll) { + R_registerRoutines(dll, NULL, CallEntries, NULL, NULL); + R_useDynamicSymbols(dll, FALSE); +} diff --git a/src/code.cpp b/src/code.cpp new file mode 100644 index 0000000..38fb6dd --- /dev/null +++ b/src/code.cpp @@ -0,0 +1,61 @@ +#include +using namespace Rcpp; + +// [[Rcpp::export]] +double mod(double x, int base) { + return x - floor(x / base) * base; +} + +// [[Rcpp::export]] +double get_index( + NumericVector &x, + int index, + int offset +) { + int N = x.size(); + int j = (index + offset) % N; + return x[j]; +} + +// [[Rcpp::export]] +double cosine_similarity_cpp( + NumericVector &x, + NumericVector &template_, + int offset +) { + double Sxy = 0.0; + double Sxx = 0.0; + double Syy = 0.0; + + int N = x.size(); + int N2 = template_.size(); + + if (N != N2) { + stop(" and should both have the same size."); + } + + for (int i = 0; i < N; i ++) { + double x_i = get_index(x, i, offset); + double y_i = template_[i]; + Sxy += x_i * y_i; + Sxx += x_i * x_i; + Syy += y_i * y_i; + } + + return Sxy / (sqrt(Sxx) * sqrt(Syy)); +} + +// [[Rcpp::export]] +NumericVector sweep_template_cpp( + NumericVector &x, + NumericVector &template_ +) { + int array_dim = x.size(); + NumericVector res = NumericVector(array_dim); + + for (int offset = 0; offset < array_dim; offset ++) { + res[offset] = cosine_similarity_cpp(x, template_, offset); + } + + return res; +} diff --git a/tests/test-dycon.R b/tests/testthat/test-dycon.R similarity index 100% rename from tests/test-dycon.R rename to tests/testthat/test-dycon.R diff --git a/tests/testthat/test-har18.R b/tests/testthat/test-har18.R new file mode 100644 index 0000000..916bd66 --- /dev/null +++ b/tests/testthat/test-har18.R @@ -0,0 +1,175 @@ +context("test-cpp") + +test_that("get_index", { + x <- c(0, 1, 2, 3) + + expect_equal(get_index(x, index = 0, offset = 0), 0) + expect_equal(get_index(x, index = 1, offset = 0), 1) + expect_equal(get_index(x, index = 2, offset = 0), 2) + expect_equal(get_index(x, index = 3, offset = 0), 3) + + expect_equal(get_index(x, index = 0, offset = 1), 1) + expect_equal(get_index(x, index = 1, offset = 1), 2) + expect_equal(get_index(x, index = 2, offset = 1), 3) + expect_equal(get_index(x, index = 3, offset = 1), 0) + + expect_equal(get_index(x, index = 0, offset = 2), 2) + expect_equal(get_index(x, index = 1, offset = 2), 3) + expect_equal(get_index(x, index = 2, offset = 2), 0) + expect_equal(get_index(x, index = 3, offset = 2), 1) +}) + +test_that("cosine_similarity", { + x <- c(12, 25, -40, 20, 49) + y <- c(-40, 30, 10, 5, -30) + + expect_equal( + cosine_similarity_cpp(x, y, offset = 0), # <-- C++ version + cosine_similarity(x, y), # <-- R version + ) + + expect_equal( + cosine_similarity_cpp(x, y, offset = 1), + cosine_similarity(x[c(2:5, 1)], y), + ) + + expect_equal( + cosine_similarity_cpp(x, y, offset = 2), + cosine_similarity(x[c(3:5, 1:2)], y), + ) + + expect_equal( + cosine_similarity_cpp(x, y, offset = 3), + cosine_similarity(x[c(4:5, 1:3)], y), + ) +}) + +test_that("sweep_template", { + x <- c(-5, 20, 15, 35, 40, 20) + y <- c(20, 15, 27, 40, 10, 15) + + expect_equal( + sweep_template(x, y, legacy = TRUE), + sweep_template_cpp(x, y) + ) +}) + +context("test-harmonicity") + +library(hrep) +library(magrittr) + +test_that("Legacy comparisons with HarmonyStats package", { + pc_set_ids <- c(1, 100, 300, 500, 650, 800, 900, 1000, 1200, 1500) + pc_sets <- pc_set_ids %>% coded_vec("pc_set") %>% decode %>% as.list + pc_set_sizes <- vapply(pc_sets, length, integer(1)) + + # The following commented out code was used to generate the + # reference vector. + + # library(tidyverse) + # x <- unclass(readRDS("/Users/peter/Dropbox/Academic/projects/pearce-marcus/harmony/HarmonyStats/inst/extdata/feature_cache.rds")) + # scale_info <- attr(x, "scale_info")$instantaneous %>% filter(measure == "harmonicity") + # + # ref <- tibble(pc_set_id = pc_set_ids, + # size = pc_set_sizes, + # scaled_harmonicity = x@data$`NA`["harmonicity", pc_set_ids], + # center = scale_info$center[size], + # scale = scale_info$scale[size], + # harmonicity = scaled_harmonicity * scale + center) %>% + # pull(harmonicity) + # dump("ref", file = "") + + ref <- + c(1.56025992760304, 0.763094086276816, 0.699233982570559, 0.525533347758732, + 0.649556486732275, 0.763094086276816, 0.774127828829956, 0.525533347758732, + 0.699233982570559, 0.552087654180455) + + expect_equal(ref, + pc_sets %>% vapply(pc_harmonicity, numeric(1))) +}) + +context("test-kl_div_from_uniform") + +library(hrep) +library(magrittr) + +test_that("example output", { + spec <- c(0.2, 0.4, 0.3, 0.1) %>% .milne_pc_spectrum() + unif <- 1 / length(spec) + d <- 0 + for (i in seq_along(spec)) { + d <- d + spec[i] * log(spec[i] / unif, base = 2) + } + expect_equal( + d, kl_div_from_uniform(spec) + ) +}) + +test_that("invariance to the number of bins", { + v1 <- c(0.2, 0.4, 0.3, 0.1) + v2 <- rep(v1, each = 2) + s1 <- .milne_pc_spectrum(v1) + s2 <- .milne_pc_spectrum(v2) + expect_equal( + kl_div_from_uniform(s1), + kl_div_from_uniform(s2) + ) +}) + +test_that("invariance to magnitude", { + v1 <- c(0.2, 0.4, 0.3, 0.1) + v2 <- v1 * 2 + s1 <- .milne_pc_spectrum(v1) + s2 <- .milne_pc_spectrum(v2) + expect_equal( + kl_div_from_uniform(s1), + kl_div_from_uniform(s2) + ) +}) + +test_that("Peak measure produces different patterns to KL measure", { + organised_sub_peaks <- c(0, 0, 3, 0, 0, 5, 0, 0, 0, 0) %>% .milne_pc_spectrum() + disorganised_sub_peaks <- c(1, 1, 1, 0, 0, 5, 0, 0, 0, 0) %>% .milne_pc_spectrum() + expect_equal( + peak(organised_sub_peaks), + peak(disorganised_sub_peaks) + ) + expect_gt( + kl_div_from_uniform(organised_sub_peaks), + kl_div_from_uniform(disorganised_sub_peaks) + ) +}) + +context("test-peak") + +library(hrep) +library(magrittr) + +test_that("example", { + c(0, 0, 3, 4, 1) %>% + .milne_pc_spectrum() %>% + peak %>% + expect_equal(4) +}) + +test_that("invariance to doubling the number of bins", { + for (i in 1:10) { + spec1 <- rnorm(10) %>% .milne_pc_spectrum() + spec2 <- rep(as.numeric(spec1), each = 2) %>% .milne_pc_spectrum() + expect_equal( + peak(spec1), + peak(spec2) + ) + } +}) + +test_that("more pointy distributions get higher peaks", { + x <- seq(from = -100, to = 100) + pointy <- dnorm(x, sd = 1) + less_pointy <- dnorm(x, sd = 4) + expect_gt( + peak(.milne_pc_spectrum(pointy)), + peak(.milne_pc_spectrum(less_pointy)) + ) +}) From a861b014d11b7c02486415ccc3ab23a7373c5288 Mon Sep 17 00:00:00 2001 From: Peter Harrison Date: Tue, 23 Jan 2024 22:40:23 +0000 Subject: [PATCH 12/24] Add jl2012 --- DESCRIPTION | 1 + NAMESPACE | 6 + R/model-jl12.R | 149 ++++++++++++++++++++++ inst/REFERENCES.bib | 13 ++ inst/johnson-laird-2012-data/figure-2.csv | 56 ++++++++ inst/johnson-laird-2012-data/figure-3.csv | 49 +++++++ man/jl_rule_1.Rd | 25 ++++ man/jl_rule_2.Rd | 26 ++++ man/jl_rule_3.Rd | 28 ++++ man/jl_tonal_dissonance.Rd | 34 +++++ tests/testthat/test-jl12.R | 86 +++++++++++++ 11 files changed, 473 insertions(+) create mode 100644 R/model-jl12.R create mode 100644 inst/johnson-laird-2012-data/figure-2.csv create mode 100644 inst/johnson-laird-2012-data/figure-3.csv create mode 100644 man/jl_rule_1.Rd create mode 100644 man/jl_rule_2.Rd create mode 100644 man/jl_rule_3.Rd create mode 100644 man/jl_tonal_dissonance.Rd create mode 100644 tests/testthat/test-jl12.R diff --git a/DESCRIPTION b/DESCRIPTION index 9ee92d0..833b3ec 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -17,6 +17,7 @@ Imports: magrittr (>= 1.5), purrr (>= 0.3.2), methods, + gtools, tibble (>= 2.1.3), dplyr (>= 0.8.3), rlang (>= 0.4.0), diff --git a/NAMESPACE b/NAMESPACE index 84804a9..d9a2b26 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -7,6 +7,8 @@ S3method(count_chords,corpus) S3method(huron_1994,default) S3method(huron_1994,int_vec) S3method(huron_1994,pc_set) +S3method(jl_tonal_dissonance,default) +S3method(jl_tonal_dissonance,pc_set) S3method(kl_div_from_uniform,smooth_spectrum) S3method(pc_harmonicity,default) S3method(pc_harmonicity,milne_pc_spectrum) @@ -42,6 +44,10 @@ export(hutch_visualise_theme) export(hutch_y) export(incon) export(incon_models) +export(jl_rule_1) +export(jl_rule_2) +export(jl_rule_3) +export(jl_tonal_dissonance) export(kl_div_from_uniform) export(list_models) export(pc_harmonicity) diff --git a/R/model-jl12.R b/R/model-jl12.R new file mode 100644 index 0000000..e35cae5 --- /dev/null +++ b/R/model-jl12.R @@ -0,0 +1,149 @@ +#' Tonal dissonance +#' +#' Computes tonal dissonance using the algorithm +#' of \insertCite{Johnson-Laird2012;textual}{jl12}. +#' @param x Sonority to analyse. +#' This will be coerced to an object of class \code{\link[hrep]{pc_set}}. +#' @return Integer scalar identifying the chord's consonance rank, +#' with higher values corresponding to increasing degrees of dissonance. +#' @references +#' \insertAllCited{} +#' @rdname jl_tonal_dissonance +#' @examples +#' jl_tonal_dissonance(c(0, 4, 7)) +#' jl_tonal_dissonance(c(0, 3, 7)) +#' jl_tonal_dissonance(c(0, 3, 6)) +#' @export +jl_tonal_dissonance <- function(x) { + UseMethod("jl_tonal_dissonance") +} + +#' @rdname jl_tonal_dissonance +#' @export +jl_tonal_dissonance.default <- function(x) { + x <- hrep::pc_set(x) + jl_tonal_dissonance(x) +} + +#' @rdname jl_tonal_dissonance +#' @export +jl_tonal_dissonance.pc_set <- function(x) { + y <- jl_rule_1(x) * 4L + + jl_rule_2(x) * 2L + + jl_rule_3(x) - 3L + attributes(y) <- NULL + y +} + +#' Tonal Dissonance, Rule 1 +#' +#' "Chords occurring in a major scale should be less dissonant +#' than chords occurring only in a minor scale, +#' which in turn should be less dissonant than chords +#' occurring in neither sort of scale." +#' \insertCite{Johnson-Laird2012}{jl12}. +#' @param pc_set (Numeric vector) Pitch-class set to analyse. +#' No input checking is performed. +#' @return 1, 2, or 3, corresponding to increasing degrees of dissonance. +#' @references +#' \insertAllCited{} +#' @export +jl_rule_1 <- function(pc_set) { + if (in_major_scale(pc_set)) 1L else + if (in_minor_scale(pc_set)) 2L else + 3L +} + +#' Tonal Dissonance, Rule 2 +#' +#' "Chords that are consistent with a major triad are more consonant +#' than chords that are not consistent with a major triad" +#' \insertCite{Johnson-Laird2012}{jl12}. +#' Consistency with the major triad means that a major triad +#' must be contained within the chord. +#' Additionally, all notes must be contained within a major scale. +#' @param pc_set (Numeric vector) Pitch-class set to analyse. +#' No input checking is performed. +#' @return \code{TRUE} for dissonant, \code{FALSE} for consonant. +#' @references +#' \insertAllCited{} +#' @export +jl_rule_2 <- function(pc_set) { + pc_set <- as.numeric(pc_set) + for (root in 0:11) { + triad <- sort((c(0, 4, 7) + root) %% 12) + if (all(triad %in% pc_set) && + in_major_scale(pc_set)) return(FALSE) + } + TRUE +} + +#' Tonal Dissonance, Rule 3 +#' +#' "Chords built from intervals of a third should be more consonant +#' than chords that are not built from thirds." +#' "The principle allows for just one missing third intervening +#' between two pitch classes a fifth apart" +#' \insertCite{Johnson-Laird2012}{jl12}. +#' @param pc_set (Numeric vector) Pitch-class set to analyse. +#' @return \code{FALSE} for consonant, \code{TRUE} for dissonant. +#' If a consonant solution is found, +#' the stacked-thirds version of the chord is returned +#' as the attribute \code{"solution"}, +#' accessible with the command \code{attr(x, "solution")}. +#' @references +#' \insertAllCited{} +#' @export +jl_rule_3 <- function(pc_set) { + N <- length(pc_set) + pc_set <- as.numeric(pc_set) + # These contain all the different possible intervallic structures + # for creating a pitch-class set the same size as the + # current pitch-class set by stacking thirds, and + # allowing one pitch class to be missing. + # Solutions with no pitch class missing correspond to + # subsets of these vectors. + perm <- gtools::permutations(n = 2L, r = N, + v = c(3L, 4L), + repeats.allowed = TRUE) + # Iterate over every pitch class and try and + # build the chord by stacking thirds on top of + # that pitch class + for (i in seq_along(pc_set)) { + base <- pc_set[i] + for (j in seq_len(nrow(perm))) { + res <- (base + c(0L, cumsum(perm[j, ]))) %% 12L + if (all(pc_set %in% res)) { + out <- FALSE + attr(out, "solution") <- res + return(out) + } + } + } + TRUE +} + +in_major_scale <- function(pc_set) { + for (scale in major_scales) { + if (all(as.numeric(pc_set) %in% scale)) return(TRUE) + } + FALSE +} +in_minor_scale <- function(pc_set) { + for (scale in minor_scales) { + if (all(as.numeric(pc_set) %in% scale)) return(TRUE) + } + FALSE +} + +c_major_scale <- c(0, 2, 4, 5, 7, 9, 11) +c_minor_scale <- c(0, 2, 3, 5, 7, 8, 11) + +major_scales <- lapply(0:11, function(x) { + sort((c_major_scale + x) %% 12) +}) +minor_scales <- lapply(0:11, function(x) { + sort((c_minor_scale + x) %% 12) +}) +names(major_scales) <- 0:11 +names(minor_scales) <- 0:11 diff --git a/inst/REFERENCES.bib b/inst/REFERENCES.bib index f71f4c9..7b5686d 100644 --- a/inst/REFERENCES.bib +++ b/inst/REFERENCES.bib @@ -60,3 +60,16 @@ @article{Milne2016 volume = {10}, year = {2016} } + +@article{Johnson-Laird2012, +author = {Johnson-Laird, Phil N. and Kang, Olivia E. and Leong, Yuan Chang}, +file = {:Users/peter/Dropbox/Academic/literature/Johnson-Laird, Kang, Leong/Johnson-Laird, Kang, Leong - 2012 - On musical dissonance.pdf:pdf}, +journal = {Music Perception}, +keywords = {consonance,dissonance,harmony,sensory}, +number = {1}, +pages = {19--35}, +title = {{On musical dissonance}}, +volume = {30}, +year = {2012} +} + diff --git a/inst/johnson-laird-2012-data/figure-2.csv b/inst/johnson-laird-2012-data/figure-2.csv new file mode 100644 index 0000000..48a80d4 --- /dev/null +++ b/inst/johnson-laird-2012-data/figure-2.csv @@ -0,0 +1,56 @@ +id,midi,mean_rating,roughness.jl,dual_process +1a,48 64 67,1.667,7.27,1 +1b,52 67 72,2.889,6.11,1 +1c,55 72 76,2.741,9.92,1 +2a,55 74 77,2.481,4,2 +2b,50 65 79,4.444,13.93,2 +2c,53 67 74,3.63,11.52,2 +3a,55 71 77,2.63,7.6,2 +3b,59 65 79,3.556,17.85,2 +3c,53 67 71,4,19.4,2 +4a,50 65 72,2.926,8.94,2 +4b,53 60 74,3.333,8.96,2 +4c,48 74 77,4.296,11.03,2 +5a,45 60 64,2.407,9.72,2 +5b,48 64 69,3.593,10.77,2 +5c,52 69 72,3.481,18.38,2 +6a,53 60 67,3.148,9.83,2 +6b,48 55 65,2.852,13.4,2 +6c,55 65 72,3.111,12.14,2 +7a,48 67 71,3.37,10.6,2 +7b,55 59 72,3.296,22.67,2 +7c,59 60 67,4.741,36.97,2 +8a,48 64 71,2.63,10.97,2 +8b,52 59 72,3.704,19.94,2 +8c,59 60 64,3.963,37.07,2 +9a,47 62 65,3.889,22.14,2 +9b,50 65 71,3.519,13.69,2 +9c,53 71 74,3.667,16.83,2 +10a,48 71 74,3.963,13.9,3 +10b,50 59 72,4.148,22.14,3 +10c,59 60 62,5.185,54.29,3 +11a,47 65 69,5.148,17.46,3 +11b,53 57 71,3.444,20.93,3 +11c,57 71 77,5.296,18.74,3 +12a,48 65 71,3.926,18.67,3 +12b,53 59 72,3.704,17.66,3 +12c,59 60 65,4.519,39.64,3 +13a,50 64 67,3.481,19.02,3 +13b,52 62 79,3.333,17.07,3 +13c,55 64 74,3.222,7.51,3 +14a,45 60 71,3.63,24.85,3 +14b,59 60 69,5.185,40.3,3 +14c,45 59 72,4,30.65,3 +15a,53 64 71,4.815,26.39,3 +15b,64 65 71,5.296,35.4,3 +15c,59 64 77,5.333,38.53,3 +16,48 64 68,5.259,16.07,4 +17a,48 63 71,5.593,17.27,4 +17b,51 59 72,5.815,25.87,4 +17c,59 60 63,4.148,41.16,4 +18a,48 68 71,5.63,21.64,4 +18b,56 59 72,4.259,23.31,4 +18c,59 60 68,5.296,39.95,4 +19a,48 62 73,5.593,30.42,5 +19b,49 60 74,5.852,29.1,5 +19c,50 61 72,6.667,33.88,5 \ No newline at end of file diff --git a/inst/johnson-laird-2012-data/figure-3.csv b/inst/johnson-laird-2012-data/figure-3.csv new file mode 100644 index 0000000..9f984ec --- /dev/null +++ b/inst/johnson-laird-2012-data/figure-3.csv @@ -0,0 +1,49 @@ +id,midi,mean_rating,roughness.jl,dual_process +1,43 53 59 62,2.54,22.1,1 +2,48 59 64 67,2.08,22.17,1 +3,48 62 64 67,2,24.08,1 +4a,48 62 64 71,2.03,27.62,1 +4b,52 60 62 71,3.59,32.81,1 +5,48 59 62 67,2.38,28.6,1 +6,43 53 57 59,3.38,40.39,1 +7a,48 57 64 67,2.46,14.41,2 +7b,45 55 60 64,2.46,19.78,2 +8a,48 57 62 67,2.41,17.51,2 +8b,45 55 60 62,3.16,31.94,2 +9a,48 62 64 69 ,2.81,24.91,2 +9b,50 60 64 69,3.22,18.87,2 +10a,48 59 64 69,2.92,25.2,2 +10b,45 59 60 64,3.11,39.16,2 +11a,48 59 62 69,2.95,32.41,2 +11b,50 59 60 69,4.54,38.63,2 +12,43 53 59 64,3.35,33.32,2 +13a,48 57 59 67,3.54,37.32,2 +13b,45 55 59 60,3.19,49.11,2 +14,43 53 59 60,3.49,41.92,2 +15a,50 59 65 69,3.43,15.6,3 +15b,47 57 62 65,3.14,30.63,3 +16a,53 59 69 72,4.83,20.7,3 +16b,47 57 60 65,4.76,38.15,3 +17,53 57 64 71,4.78,27.09,3 +18,53 59 64 72,4.84,28.22,3 +19,50 60 65 71,4.49,28.49,3 +20,50 53 64 71,4.81,36.45,3 +21,45 56 60 64,5.08,25.23,4 +22,48 59 64 68,4.54,26.69,4 +23a,50 60 64 68,4.73,28.34,4 +23b,52 62 68 72,4.73,23.29,4 +24,44 53 59 62,3.65,29.08,4 +25,53 57 68 72,4.78,20.56,5 +26a,53 64 68 74,5.22,25.35,5 +26b,52 62 65 68,4.57,29.23,5 +27,50 57 65 68,4.86,26.78,5 +28,47 56 65 69,5.46,29.92,5 +29,53 57 64 68,4.92,29.98,5 +30,45 60 68 71,4.73,30.2,5 +31,50 57 60 68,4.81,31.83,5 +32,53 56 64 71,5.38,31.88,5 +33,53 57 63 68,4.84,28.49,6 +34,53 57 63 71,4.97,28.73,6 +35,51 57 62 68,5.57,36.93,6 +36,53 63 64 74,5.95,57.43,6 +37,43 53 63 64,6.43,61.83,6 \ No newline at end of file diff --git a/man/jl_rule_1.Rd b/man/jl_rule_1.Rd new file mode 100644 index 0000000..72e8e1e --- /dev/null +++ b/man/jl_rule_1.Rd @@ -0,0 +1,25 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/model-jl12.R +\name{jl_rule_1} +\alias{jl_rule_1} +\title{Tonal Dissonance, Rule 1} +\usage{ +jl_rule_1(pc_set) +} +\arguments{ +\item{pc_set}{(Numeric vector) Pitch-class set to analyse. +No input checking is performed.} +} +\value{ +1, 2, or 3, corresponding to increasing degrees of dissonance. +} +\description{ +"Chords occurring in a major scale should be less dissonant +than chords occurring only in a minor scale, +which in turn should be less dissonant than chords +occurring in neither sort of scale." +\insertCite{Johnson-Laird2012}{jl12}. +} +\references{ +\insertAllCited{} +} diff --git a/man/jl_rule_2.Rd b/man/jl_rule_2.Rd new file mode 100644 index 0000000..ffdf210 --- /dev/null +++ b/man/jl_rule_2.Rd @@ -0,0 +1,26 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/model-jl12.R +\name{jl_rule_2} +\alias{jl_rule_2} +\title{Tonal Dissonance, Rule 2} +\usage{ +jl_rule_2(pc_set) +} +\arguments{ +\item{pc_set}{(Numeric vector) Pitch-class set to analyse. +No input checking is performed.} +} +\value{ +\code{TRUE} for dissonant, \code{FALSE} for consonant. +} +\description{ +"Chords that are consistent with a major triad are more consonant +than chords that are not consistent with a major triad" +\insertCite{Johnson-Laird2012}{jl12}. +Consistency with the major triad means that a major triad +must be contained within the chord. +Additionally, all notes must be contained within a major scale. +} +\references{ +\insertAllCited{} +} diff --git a/man/jl_rule_3.Rd b/man/jl_rule_3.Rd new file mode 100644 index 0000000..9856c64 --- /dev/null +++ b/man/jl_rule_3.Rd @@ -0,0 +1,28 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/model-jl12.R +\name{jl_rule_3} +\alias{jl_rule_3} +\title{Tonal Dissonance, Rule 3} +\usage{ +jl_rule_3(pc_set) +} +\arguments{ +\item{pc_set}{(Numeric vector) Pitch-class set to analyse.} +} +\value{ +\code{FALSE} for consonant, \code{TRUE} for dissonant. +If a consonant solution is found, +the stacked-thirds version of the chord is returned +as the attribute \code{"solution"}, +accessible with the command \code{attr(x, "solution")}. +} +\description{ +"Chords built from intervals of a third should be more consonant +than chords that are not built from thirds." +"The principle allows for just one missing third intervening +between two pitch classes a fifth apart" +\insertCite{Johnson-Laird2012}{jl12}. +} +\references{ +\insertAllCited{} +} diff --git a/man/jl_tonal_dissonance.Rd b/man/jl_tonal_dissonance.Rd new file mode 100644 index 0000000..7a6e105 --- /dev/null +++ b/man/jl_tonal_dissonance.Rd @@ -0,0 +1,34 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/model-jl12.R +\name{jl_tonal_dissonance} +\alias{jl_tonal_dissonance} +\alias{jl_tonal_dissonance.default} +\alias{jl_tonal_dissonance.pc_set} +\title{Tonal dissonance} +\usage{ +jl_tonal_dissonance(x) + +\method{jl_tonal_dissonance}{default}(x) + +\method{jl_tonal_dissonance}{pc_set}(x) +} +\arguments{ +\item{x}{Sonority to analyse. +This will be coerced to an object of class \code{\link[hrep]{pc_set}}.} +} +\value{ +Integer scalar identifying the chord's consonance rank, +with higher values corresponding to increasing degrees of dissonance. +} +\description{ +Computes tonal dissonance using the algorithm +of \insertCite{Johnson-Laird2012;textual}{jl12}. +} +\examples{ +jl_tonal_dissonance(c(0, 4, 7)) +jl_tonal_dissonance(c(0, 3, 7)) +jl_tonal_dissonance(c(0, 3, 6)) +} +\references{ +\insertAllCited{} +} diff --git a/tests/testthat/test-jl12.R b/tests/testthat/test-jl12.R new file mode 100644 index 0000000..896a9de --- /dev/null +++ b/tests/testthat/test-jl12.R @@ -0,0 +1,86 @@ +context("main") + +library(magrittr) + +test_that("combined model", { + # Test against the original paper + df <- c("johnson-laird-2012-data/figure-2.csv", + "johnson-laird-2012-data/figure-3.csv") %>% + system.file(package = "incon") %>% + lapply(function(x) read.csv(x, stringsAsFactors = FALSE)) %>% + do.call(rbind, .) + df$midi <- I(lapply(strsplit(df$midi, " "), as.numeric)) + df$pc_set <- I(lapply(df$midi, function(x) { + stopifnot(all.equal(x, sort(x))) + res <- sort(x %% 12) + stopifnot(!anyDuplicated(res)) + res + })) + df$rule_1 <- sapply(df$pc_set, jl_rule_1) + df$rule_2 <- sapply(df$pc_set, jl_rule_2) + df$rule_3 <- sapply(df$pc_set, jl_rule_3) + df$dual_process_2 <- sapply(df$pc_set, jl_tonal_dissonance) + # expect_equal(df$dual_process, df$dual_process_2) + # Seems there are mistakes in the original paper. + ## Fig 2: + # Here are the implied classifications in the paper: + # 1: consonant according to all rules + # 2-9: consonant by R1, dissonant by rule 2, consonant by R3 + # 10-15: consonant by R1, dissononant by R2, dissonant by R3 + # 16: medium by R1, dissonant by R2, consonant by R3 + # 17-18: medium by R1, dissonant by R2, consonant by R3 (?) same as previous category + # 19: dissonant by all rules + ## Here are some example problems with the paper's annotations: + # chord 6 cannot be built from stacked thirds, whereas chord 7 can. + # => chord 6 should be in a different category to chord 7 + # chord 11 can be bult by stacking thirds, whereas chord 10 can't + # => chord 11 should come in a different category to chord 10 + # chord 16 is not in the same category as chords 17-18, yet they all + # have the same properties: + # - from minor scale (R1) + # - neither contain a major triad (R2) + # - each can be built from thirds + ## Similar problems exist for the Figure 3. + # Conclusion: the original paper doesn't provide useful + # regression tests. +}) + +test_that("major scales", { + expect_equal(major_scales$`2`, + c(1, 2, 4, 6, 7, 9, 11)) + expect_equal(major_scales$`7`, + c(0, 2, 4, 6, 7, 9, 11)) + expect_equal(minor_scales$`0`, + c(0, 2, 3, 5, 7, 8, 11)) + expect_equal(minor_scales$`9`, + c(0, 2, 4, 5, 8, 9, 11)) +}) + +test_that("rule 1", { + expect_equal(jl_rule_1(c(0, 4, 7)), 1) + expect_equal(jl_rule_1(c(0, 3, 7)), 1) + expect_equal(jl_rule_1(c(3, 7, 10)), 1) + expect_equal(jl_rule_1(c(0, 4, 9)), 1) + expect_equal(jl_rule_1(c(1, 2, 6, 9)), 1) + expect_equal(jl_rule_1(c(0, 2, 5, 9)), 1) + expect_equal(jl_rule_1(c(2, 5, 7, 11)), 1) + expect_equal(jl_rule_1(c(2, 5, 9, 11)), 1) + expect_equal(jl_rule_1(c(0, 4, 8, 9)), 2) + expect_equal(jl_rule_1(c(0, 4, 8, 11)), 2) + expect_equal(jl_rule_1(c(2, 5, 8, 11)), 2) + expect_equal(jl_rule_1(c(3, 5, 9, 11)), 3) + expect_equal(jl_rule_1(c(0, 1, 2, 3, 4)), 3) +}) + +test_that("rule 2", { + expect_false(jl_rule_2(c(2, 6, 9))) + expect_true(jl_rule_2(c(2, 5, 9))) + expect_false(jl_rule_2(c(2, 5, 7, 11))) + expect_true(jl_rule_2(c(2, 3, 7, 11))) +}) + +test_that("rule 3", { + expect_false(jl_rule_3(c(0, 4, 9))) + expect_false(jl_rule_3(c(0, 4, 11))) + expect_true(jl_rule_3(c(0, 2, 7))) +}) From 7e0bff86084c801f9d4568587495aa6e2478b49e Mon Sep 17 00:00:00 2001 From: Peter Harrison Date: Tue, 23 Jan 2024 23:05:28 +0000 Subject: [PATCH 13/24] Add parn88 --- NAMESPACE | 7 ++ R/imports.R | 3 + R/model-parn88.R | 159 +++++++++++++++++++++++++++++++++++ R/top-level.R | 2 +- data-raw/by-pc-chord.R | 13 +++ data/root_by_pc_chord.rda | Bin 0 -> 8320 bytes inst/REFERENCES.bib | 22 +++++ man/incon.Rd | 2 +- man/parn88.Rd | 49 +++++++++++ man/root.Rd | 22 +++++ man/root_ambiguity.Rd | 22 +++++ man/root_by_pc_chord.Rd | 13 +++ man/root_support_weights.Rd | 21 +++++ tests/testthat/test-parn88.R | 93 ++++++++++++++++++++ 14 files changed, 426 insertions(+), 2 deletions(-) create mode 100644 R/model-parn88.R create mode 100644 data-raw/by-pc-chord.R create mode 100644 data/root_by_pc_chord.rda create mode 100644 man/parn88.Rd create mode 100644 man/root.Rd create mode 100644 man/root_ambiguity.Rd create mode 100644 man/root_by_pc_chord.Rd create mode 100644 man/root_support_weights.Rd create mode 100644 tests/testthat/test-parn88.R diff --git a/NAMESPACE b/NAMESPACE index d9a2b26..d962054 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -10,6 +10,8 @@ S3method(huron_1994,pc_set) S3method(jl_tonal_dissonance,default) S3method(jl_tonal_dissonance,pc_set) S3method(kl_div_from_uniform,smooth_spectrum) +S3method(parn88,default) +S3method(parn88,pc_set) S3method(pc_harmonicity,default) S3method(pc_harmonicity,milne_pc_spectrum) S3method(pc_harmonicity,pc_set) @@ -50,8 +52,12 @@ export(jl_rule_3) export(jl_tonal_dissonance) export(kl_div_from_uniform) export(list_models) +export(parn88) export(pc_harmonicity) export(peak) +export(root) +export(root_ambiguity) +export(root_support_weights) export(roughness_hutch) export(roughness_seth) export(roughness_vass) @@ -60,6 +66,7 @@ export(sweep_harmonic_template) export(sweep_template) export(type) importFrom(Rcpp,sourceCpp) +importFrom(Rdpack,reprompt) importFrom(magrittr,"%>%") importFrom(methods,"is") importFrom(rlang,".data") diff --git a/R/imports.R b/R/imports.R index 74c75ee..7f7bcf5 100644 --- a/R/imports.R +++ b/R/imports.R @@ -30,3 +30,6 @@ NULL NULL `.` <- NULL + +#' @importFrom Rdpack reprompt +NULL diff --git a/R/model-parn88.R b/R/model-parn88.R new file mode 100644 index 0000000..603e235 --- /dev/null +++ b/R/model-parn88.R @@ -0,0 +1,159 @@ +#' Root by pitch-class chord +#' +#' This vector stores precomputed chord roots for +#' all pitch-class chords, +#' indexed by their encoding as defined in the "hrep" package. +#' See \code{\link[hrep]{pc_chord}} for more details. +#' +#' @name root_by_pc_chord +#' @docType data +#' @keywords data +NULL + +#' Parncutt (1988) +#' +#' Analyses a pitch-class set using the root-finding model of +#' \insertCite{Parncutt1988;textual}{parn88}. +#' @param x Sonority to analyse. +#' This will be coerced to an object of class \code{\link[hrep]{pc_set}}. +#' @param root_support (Character scalar or data frame) +#' Identifies the root support weights to use. +#' * \code{"v2"} (default) uses the updated +#' weights from \insertCite{Parncutt2006;textual}{parn88}. +#' * \code{"v1"} uses the original weights from \insertCite{Parncutt2006;textual}{parn88}. +#' +#' See \code{\link{root_support_weights}} for the values of these weights. +#' Alternatively, root-support weights can be provided as a data frame, +#' with one column (interval) identifying the ascending interval in semitones, +#' and another column (weight) identifying the corresponding root support weight. +#' @param exponent (Numeric scalar) Exponent to be used when computing +#' root ambiguities. Defaults to 0.5, after \insertCite{Parncutt1988;textual}{parn88}. +#' @return A list with three values: +#' * \code{root}, the estimated chord root (integer scalar); +#' * \code{root_ambiguity}, the root ambiguity (numeric scalar), +#' * \code{pc_weight}, a 12-dimensional vector of weights by pitch class. +#' @references +#' \insertAllCited{} +#' @md +#' @rdname parn88 +#' @export +parn88 <- function(x, root_support = "v2", exponent = 0.5) { + UseMethod("parn88") +} + +#' @rdname parn88 +#' @export +parn88.default <- function(x, root_support = "v2", exponent = 0.5) { + x <- hrep::pc_set(x) + do.call(parn88, args = as.list(environment())) +} + +#' @rdname parn88 +#' @export +parn88.pc_set <- function(x, root_support = "v2", exponent = 0.5) { + root_support <- get_root_support_weights(root_support) + + checkmate::qassert(x, "X+[0,11]") + checkmate::qassert(exponent, "R1") + checkmate::qassert(root_support, "R12") + stopifnot(!anyDuplicated(x)) + + w <- purrr::map_dbl(0:11, + pc_weight, + pc_set = encode_pc_set(x), + root_support = root_support) + + list(root = which.max(w) - 1L, + root_ambiguity = get_root_ambiguity(w, exponent = exponent), + pc_weight = w) +} + +#' Root +#' +#' Estimates the chord root of a pitch-class set using the root-finding model of +#' \insertCite{Parncutt1988;textual}{parn88}. +#' This function is a wrapper for \code{\link{parn88}}. +#' @param ... Arguments to pass to \code{\link{parn88}}. +#' @return The estimated chord root (integer scalar). +#' @references +#' \insertAllCited{} +#' @export +root <- function(...) { + parn88(...)$root +} + +#' Root ambiguity +#' +#' Estimates the root ambiguity of a pitch-class set using the root-finding model of +#' \insertCite{Parncutt1988;textual}{parn88}. +#' This function is a wrapper for \code{\link{parn88}}. +#' @param ... Arguments to pass to \code{\link{parn88}}. +#' @return The root ambiguity (numeric scalar). +#' @references +#' \insertAllCited{} +#' @export +root_ambiguity <- function(...) { + parn88(...)$root_ambiguity +} + +#' Root support weights +#' +#' A list of different root support weights that may be used +#' by the root-finding algorithm of \insertCite{Parncutt1988;textual}{parn88}. +#' See \code{\link{parn88}} for more information. +#' @references +#' \insertAllCited{} +#' @export +root_support_weights <- list( + v1 = tibble::tribble( + ~ interval, ~ weight, + 0, 1, + 7, 1/2, + 4, 1/3, + 10, 1/4, + 2, 1/5, + 3, 1/10 + ), + v2 = tibble::tribble( + ~ interval, ~ weight, + 0, 10, + 7, 5, + 4, 3, + 10, 2, + 2, 1 + ) +) %>% purrr::map(function(df) { + x <- numeric(12) + x[df$interval + 1] <- df$weight + x +}) + +get_root_ambiguity <- function(x, exponent) { + checkmate::qassert(x, "R12") + checkmate::qassert(exponent, "R1") + x_max <- max(x) + sum(x / x_max) ^ exponent +} + +pc_weight <- function(pc, pc_set, root_support) { + checkmate::qassert(pc, "X1") + checkmate::qassert(pc_set, "X12[0,1]") + checkmate::qassert(root_support, "R12") + ind <- (seq(from = pc, length.out = 12L) %% 12L) + 1L + sum(pc_set[ind] * root_support) +} + +encode_pc_set <- function(x) { + checkmate::qassert(x, "X[0,11]") + y <- integer(12) + y[x + 1] <- 1L + y +} + +get_root_support_weights <- function(root_support) { + if (is.character(root_support)) { + stopifnot(length(root_support) == 1L, + root_support %in% names(root_support_weights)) + root_support <- root_support_weights[[root_support]] + } +} diff --git a/R/top-level.R b/R/top-level.R index dfd0f92..c20c96c 100644 --- a/R/top-level.R +++ b/R/top-level.R @@ -68,7 +68,7 @@ #' (see \code{incon::\link[incon]{pc_harmonicity}}). #' * `parn_88_root_ambig`: #' the root ambiguity model of \insertCite{Parncutt1988;textual}{parn88} -#' (see \code{parn88::\link[parn88]{root_ambiguity}}). +#' (see \code{incon::\link[incon]{root_ambiguity}}). #' * `parn_94_complex`: #' the complex sonorousness feature of \insertCite{Parncutt1994;textual}{parn94} #' (see \code{parn94::\link[parn94]{complex_sonor}}). diff --git a/data-raw/by-pc-chord.R b/data-raw/by-pc-chord.R new file mode 100644 index 0000000..1b25845 --- /dev/null +++ b/data-raw/by-pc-chord.R @@ -0,0 +1,13 @@ +n <- hrep::alphabet_size("pc_chord") +chords <- hrep::list_chords("pc_chord") +root_by_pc_chord <- integer(n) +pb <- utils::txtProgressBar(max = n, style = 3) + +for (i in seq_len(n)) { + root_by_pc_chord[i] <- parn88::root(chords[[i]]) + utils::setTxtProgressBar(pb, i) +} + +close(pb) + +use_data(root_by_pc_chord, overwrite = TRUE) diff --git a/data/root_by_pc_chord.rda b/data/root_by_pc_chord.rda new file mode 100644 index 0000000000000000000000000000000000000000..271a1f358da673f6718dea30019b75f31c739aa0 GIT binary patch literal 8320 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 K6eJ>u`LckHa*5jj literal 0 HcmV?d00001 diff --git a/inst/REFERENCES.bib b/inst/REFERENCES.bib index 7b5686d..a0e82f1 100644 --- a/inst/REFERENCES.bib +++ b/inst/REFERENCES.bib @@ -73,3 +73,25 @@ @article{Johnson-Laird2012 year = {2012} } +@article{Parncutt2006, +author = {Parncutt, Richard}, +file = {:Users/peter/Dropbox/Academic/literature/Parncutt/Parncutt - 2006 - Commentary on Cook {\&} Fujisawa's The Psychophysics of Harmony Perception Harmony is a Three-Tone Phenomenon.pdf:pdf}, +journal = {Empirical Musicology Review}, +number = {4}, +pages = {204--209}, +title = {{Commentary on Cook & Fujisawa's "The Psychophysics of Harmony Perception: Harmony is a Three-Tone Phenomenon"}}, +volume = {1}, +year = {2006} +} + +@article{Parncutt1988, +author = {Parncutt, Richard}, +file = {:Users/peter/Dropbox/Academic/literature/Parncutt/Parncutt - 1988 - Revision of Terhardt's psychoacoustical model of the root(s) of a musical chord(2).pdf:pdf}, +journal = {Music Perception}, +mendeley-groups = {Harmony,Consonance}, +number = {1}, +pages = {65--93}, +title = {{Revision of Terhardt's psychoacoustical model of the root(s) of a musical chord}}, +volume = {6}, +year = {1988} +} diff --git a/man/incon.Rd b/man/incon.Rd index 43bab7d..e3713f2 100644 --- a/man/incon.Rd +++ b/man/incon.Rd @@ -77,7 +77,7 @@ the harmonicity model of \insertCite{Milne2013;textual}{incon} (see \code{incon::\link[incon]{pc_harmonicity}}). \item \code{parn_88_root_ambig}: the root ambiguity model of \insertCite{Parncutt1988;textual}{parn88} -(see \code{parn88::\link[parn88]{root_ambiguity}}). +(see \code{incon::\link[incon]{root_ambiguity}}). \item \code{parn_94_complex}: the complex sonorousness feature of \insertCite{Parncutt1994;textual}{parn94} (see \code{parn94::\link[parn94]{complex_sonor}}). diff --git a/man/parn88.Rd b/man/parn88.Rd new file mode 100644 index 0000000..e885512 --- /dev/null +++ b/man/parn88.Rd @@ -0,0 +1,49 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/model-parn88.R +\name{parn88} +\alias{parn88} +\alias{parn88.default} +\alias{parn88.pc_set} +\title{Parncutt (1988)} +\usage{ +parn88(x, root_support = "v2", exponent = 0.5) + +\method{parn88}{default}(x, root_support = "v2", exponent = 0.5) + +\method{parn88}{pc_set}(x, root_support = "v2", exponent = 0.5) +} +\arguments{ +\item{x}{Sonority to analyse. +This will be coerced to an object of class \code{\link[hrep]{pc_set}}.} + +\item{root_support}{(Character scalar or data frame) +Identifies the root support weights to use. +\itemize{ +\item \code{"v2"} (default) uses the updated +weights from \insertCite{Parncutt2006;textual}{parn88}. +\item \code{"v1"} uses the original weights from \insertCite{Parncutt2006;textual}{parn88}. +} + +See \code{\link{root_support_weights}} for the values of these weights. +Alternatively, root-support weights can be provided as a data frame, +with one column (interval) identifying the ascending interval in semitones, +and another column (weight) identifying the corresponding root support weight.} + +\item{exponent}{(Numeric scalar) Exponent to be used when computing +root ambiguities. Defaults to 0.5, after \insertCite{Parncutt1988;textual}{parn88}.} +} +\value{ +A list with three values: +\itemize{ +\item \code{root}, the estimated chord root (integer scalar); +\item \code{root_ambiguity}, the root ambiguity (numeric scalar), +\item \code{pc_weight}, a 12-dimensional vector of weights by pitch class. +} +} +\description{ +Analyses a pitch-class set using the root-finding model of +\insertCite{Parncutt1988;textual}{parn88}. +} +\references{ +\insertAllCited{} +} diff --git a/man/root.Rd b/man/root.Rd new file mode 100644 index 0000000..27c4946 --- /dev/null +++ b/man/root.Rd @@ -0,0 +1,22 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/model-parn88.R +\name{root} +\alias{root} +\title{Root} +\usage{ +root(...) +} +\arguments{ +\item{...}{Arguments to pass to \code{\link{parn88}}.} +} +\value{ +The estimated chord root (integer scalar). +} +\description{ +Estimates the chord root of a pitch-class set using the root-finding model of +\insertCite{Parncutt1988;textual}{parn88}. +This function is a wrapper for \code{\link{parn88}}. +} +\references{ +\insertAllCited{} +} diff --git a/man/root_ambiguity.Rd b/man/root_ambiguity.Rd new file mode 100644 index 0000000..ea22fc6 --- /dev/null +++ b/man/root_ambiguity.Rd @@ -0,0 +1,22 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/model-parn88.R +\name{root_ambiguity} +\alias{root_ambiguity} +\title{Root ambiguity} +\usage{ +root_ambiguity(...) +} +\arguments{ +\item{...}{Arguments to pass to \code{\link{parn88}}.} +} +\value{ +The root ambiguity (numeric scalar). +} +\description{ +Estimates the root ambiguity of a pitch-class set using the root-finding model of +\insertCite{Parncutt1988;textual}{parn88}. +This function is a wrapper for \code{\link{parn88}}. +} +\references{ +\insertAllCited{} +} diff --git a/man/root_by_pc_chord.Rd b/man/root_by_pc_chord.Rd new file mode 100644 index 0000000..700ec36 --- /dev/null +++ b/man/root_by_pc_chord.Rd @@ -0,0 +1,13 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/model-parn88.R +\docType{data} +\name{root_by_pc_chord} +\alias{root_by_pc_chord} +\title{Root by pitch-class chord} +\description{ +This vector stores precomputed chord roots for +all pitch-class chords, +indexed by their encoding as defined in the "hrep" package. +See \code{\link[hrep]{pc_chord}} for more details. +} +\keyword{data} diff --git a/man/root_support_weights.Rd b/man/root_support_weights.Rd new file mode 100644 index 0000000..4be7e9e --- /dev/null +++ b/man/root_support_weights.Rd @@ -0,0 +1,21 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/model-parn88.R +\docType{data} +\name{root_support_weights} +\alias{root_support_weights} +\title{Root support weights} +\format{ +An object of class \code{list} of length 2. +} +\usage{ +root_support_weights +} +\description{ +A list of different root support weights that may be used +by the root-finding algorithm of \insertCite{Parncutt1988;textual}{parn88}. +See \code{\link{parn88}} for more information. +} +\references{ +\insertAllCited{} +} +\keyword{datasets} diff --git a/tests/testthat/test-parn88.R b/tests/testthat/test-parn88.R new file mode 100644 index 0000000..ffc7b95 --- /dev/null +++ b/tests/testthat/test-parn88.R @@ -0,0 +1,93 @@ +context("test-encode_pc_set") + +test_that("examples", { + expect_equal(encode_pc_set(c(0, 4, 7)), + as.integer(c(1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0))) + expect_equal(encode_pc_set(c(0)), + as.integer(c(1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0))) + expect_equal(encode_pc_set(integer()), + as.integer(c(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0))) +}) + +context("test-pc_weight") + +test_that("examples", { + expect_equal( + pc_weight(pc = 0, + pc_set = encode_pc_set(c(0, 4, 7)), + root_support = root_support_weights$v2), + 10 + 3 + 5 + ) + expect_equal( + pc_weight(pc = 1, + pc_set = encode_pc_set(c(0, 4, 7)), + root_support = root_support_weights$v2), + 0 + ) + expect_equal( + pc_weight(pc = 2, + pc_set = encode_pc_set(c(0, 4, 7)), + root_support = root_support_weights$v2), + 2 + 1 + ) + expect_equal( + pc_weight(pc = 4, + pc_set = encode_pc_set(c(0, 4, 7)), + root_support = root_support_weights$v2), + 10 + ) +}) + +context("test-regression") + +library(magrittr) + +test_88 <- function(res, ..., digits = 1) { + expect_equal( + root_ambiguity(c(...), root_support = "v1") %>% round(digits), + res + ) +} + +test_that("Parncutt (1988): Table 4", { + # Dyads + test_88(2.2, 0, 1) + test_88(2.0, 0, 2) + test_88(2.1, 0, 3) + test_88(1.9, 0, 4) + test_88(1.8, 0, 5) + test_88(2.2, 0, 6) + + # Triads + test_88(2.0, 0, 4, 7) + test_88(2.1, 0, 3, 7) + test_88(2.3, 0, 4, 8) + test_88(2.5, 0, 3, 6) + + # Sevenths + test_88(2.1, 0, 4, 7, 10) + test_88(2.3, 0, 3, 7, 10) + test_88(2.3, 0, 4, 7, 11) + test_88(2.4, 0, 3, 6, 10) + test_88(2.9, 0, 3, 6, 9) +}) + +test_that("Sanity checks", { + expect_equal(root(c(0, 4, 7)), 0) + expect_equal(root(c(1, 4, 9)), 9) + expect_gt(root_ambiguity(c(0, 3, 6)), + root_ambiguity(c(0, 4, 7))) +}) + +test_that("root_by_pc_chord", { + chords <- list( + hrep::pc_chord(c(0, 4, 7)), + hrep::pc_chord(c(4, 7, 0)), + hrep::pc_chord(c(7, 2, 5, 11)), + hrep::pc_chord(c(0, 5, 9)) + ) + chord_ids <- purrr::map_int(chords, hrep::encode_pc_chord) + + root_by_pc_chord[chord_ids] %>% + expect_equal(c(0, 0, 7, 5)) +}) From 8fcaff7129dfb273fe10832572da2f4ea8d1c740 Mon Sep 17 00:00:00 2001 From: Peter Harrison Date: Tue, 23 Jan 2024 23:11:48 +0000 Subject: [PATCH 14/24] Add parn94 --- NAMESPACE | 29 + R/model-parn94.R | 621 ++++++++++++++++++ R/models.R | 26 +- R/top-level.R | 12 +- inst/REFERENCES.bib | 10 + inst/bigand1996/bigand-1996.R | 152 +++++ inst/bigand1996/compiled/bigand_1996.rds | Bin 0 -> 1847 bytes .../compiled/bigand_1996_reference_chord.rds | Bin 0 -> 107 bytes inst/bigand1996/raw/bigand-1996-data.csv | 51 ++ .../raw/bigand-1996-reference-chord.csv | 2 + man/complex_sonor.Rd | 31 + man/get_free_field_threshold.Rd | 24 + man/get_overall_masking_level.Rd | 26 + man/get_partial_masking_level.Rd | 41 ++ man/get_pure_tone_audibility.Rd | 24 + man/get_pure_tone_audible_level.Rd | 26 + man/get_pure_tone_height.Rd | 23 + man/get_tone_salience.Rd | 28 + man/incon.Rd | 12 +- man/multiplicity.Rd | 31 + man/parn94.Rd | 61 ++ man/parn94_params.Rd | 62 ++ man/pitch_commonality.Rd | 27 + man/pitch_distance.Rd | 27 + man/pitch_salience.Rd | 36 + man/pure_sonor.Rd | 31 + tests/testthat/test-parn94.R | 251 +++++++ 27 files changed, 1639 insertions(+), 25 deletions(-) create mode 100644 R/model-parn94.R create mode 100644 inst/bigand1996/bigand-1996.R create mode 100644 inst/bigand1996/compiled/bigand_1996.rds create mode 100644 inst/bigand1996/compiled/bigand_1996_reference_chord.rds create mode 100644 inst/bigand1996/raw/bigand-1996-data.csv create mode 100644 inst/bigand1996/raw/bigand-1996-reference-chord.csv create mode 100644 man/complex_sonor.Rd create mode 100644 man/get_free_field_threshold.Rd create mode 100644 man/get_overall_masking_level.Rd create mode 100644 man/get_partial_masking_level.Rd create mode 100644 man/get_pure_tone_audibility.Rd create mode 100644 man/get_pure_tone_audible_level.Rd create mode 100644 man/get_pure_tone_height.Rd create mode 100644 man/get_tone_salience.Rd create mode 100644 man/multiplicity.Rd create mode 100644 man/parn94.Rd create mode 100644 man/parn94_params.Rd create mode 100644 man/pitch_commonality.Rd create mode 100644 man/pitch_distance.Rd create mode 100644 man/pitch_salience.Rd create mode 100644 man/pure_sonor.Rd create mode 100644 tests/testthat/test-parn94.R diff --git a/NAMESPACE b/NAMESPACE index d962054..a3db40a 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -2,6 +2,8 @@ S3method(bowl18_min_freq_dist,default) S3method(bowl18_min_freq_dist,fr_chord) +S3method(complex_sonor,default) +S3method(complex_sonor,parn94) S3method(corpus_dissonance_table,corpus) S3method(count_chords,corpus) S3method(huron_1994,default) @@ -10,13 +12,22 @@ S3method(huron_1994,pc_set) S3method(jl_tonal_dissonance,default) S3method(jl_tonal_dissonance,pc_set) S3method(kl_div_from_uniform,smooth_spectrum) +S3method(multiplicity,default) +S3method(multiplicity,parn94) S3method(parn88,default) S3method(parn88,pc_set) +S3method(parn94,default) +S3method(parn94,sparse_pi_spectrum) S3method(pc_harmonicity,default) S3method(pc_harmonicity,milne_pc_spectrum) S3method(pc_harmonicity,pc_set) S3method(peak,milne_pc_spectrum) +S3method(pitch_salience,default) +S3method(pitch_salience,parn94) +S3method(pitch_salience,pitch_salience) S3method(print,corpus_dissonance_table) +S3method(pure_sonor,default) +S3method(pure_sonor,parn94) S3method(roughness_hutch,default) S3method(roughness_hutch,sparse_fr_spectrum) S3method(roughness_seth,default) @@ -29,11 +40,19 @@ S3method(sweep_harmonic_template,milne_pc_spectrum) S3method(sweep_harmonic_template,pc_set) S3method(type,corpus_dissonance_table) export(bowl18_min_freq_dist) +export(complex_sonor) export(corpus_dissonance) export(corpus_dissonance_table) export(cosine_similarity) export(count_chords) export(demo_wang) +export(get_free_field_threshold) +export(get_overall_masking_level) +export(get_partial_masking_level) +export(get_pure_tone_audibility) +export(get_pure_tone_audible_level) +export(get_pure_tone_height) +export(get_tone_salience) export(gill09_harmonicity) export(har_19_composite_coef) export(huron_1994) @@ -52,9 +71,16 @@ export(jl_rule_3) export(jl_tonal_dissonance) export(kl_div_from_uniform) export(list_models) +export(multiplicity) export(parn88) +export(parn94) +export(parn94_params) export(pc_harmonicity) export(peak) +export(pitch_commonality) +export(pitch_distance) +export(pitch_salience) +export(pure_sonor) export(root) export(root_ambiguity) export(root_support_weights) @@ -69,10 +95,13 @@ importFrom(Rcpp,sourceCpp) importFrom(Rdpack,reprompt) importFrom(magrittr,"%>%") importFrom(methods,"is") +importFrom(methods,.valueClassTest) +importFrom(methods,new) importFrom(rlang,".data") importFrom(stats,"approx") importFrom(stats,"cor") importFrom(stats,"fft") +importFrom(stats,cor) importFrom(tibble,tibble) importFrom(utils,"capture.output") importFrom(zeallot,"%<-%") diff --git a/R/model-parn94.R b/R/model-parn94.R new file mode 100644 index 0000000..8d34f05 --- /dev/null +++ b/R/model-parn94.R @@ -0,0 +1,621 @@ +add_combined_spectrum <- function(x, par) { + df <- merge_spectra(x$pure_spectrum, x$complex_spectrum) + df$combined_audibility <- pmax(df$pure_tone_audibility, + df$complex_tone_audibility, + 0, na.rm = TRUE) + df$salience <- get_tone_salience(df$combined_audibility, par$k_s) + x$combined_spectrum <- df[, c("pitch", "combined_audibility", "salience")] + x +} + +merge_spectra <- function(pure_spectrum, complex_spectrum) { + merge(pure_spectrum[, c("pitch", "pure_tone_audibility")], + complex_spectrum[, c("pitch", "complex_tone_audibility")], + by = "pitch", + all = TRUE) +} +add_complex_spectrum <- function(x, par) { + spectrum <- x$pure_spectrum[, c("pitch", "pure_tone_audibility")] + template <- get_template(par) + df <- tibble::tibble( + pitch = seq(from = par$min_midi, + to = par$max_midi), + complex_tone_audibility = purrr::map_dbl(.data$pitch, + template_match, + template, + spectrum, + par) + ) + x$complex_spectrum <- df[df$complex_tone_audibility > 0, ] + x +} + +template_match <- function(fundamental, template, spectrum, par) { + transposed_template <- tibble::tibble(pitch = template$interval + fundamental, + weight = template$weight) + df <- merge(transposed_template, spectrum, + all.x = FALSE, all.y = FALSE) + ((sum(sqrt(df$weight * df$pure_tone_audibility))) ^ 2) / par$k_t +} + +get_template <- function(par) { + hrep::pi_chord(0) %>% + {hrep::sparse_pi_spectrum(., + num_harmonics = par$template_num_harmonics, + roll_off = par$template_roll_off, + digits = 0)} %>% + (tibble::as_tibble) %>% + {magrittr::set_names(., c("interval", "weight"))} +} +# @param res Where results are stored +# @param x Input spectrum +add_pure_spectrum <- function(res, x, par) { + y <- tibble::tibble( + pitch = hrep::pitch(x), + amplitude = hrep::amp(x), + kHz = hrep::midi_to_freq(.data$pitch) / 1000, + level = hrep::amplitude_to_dB(.data$amplitude, par$unit_amplitude_in_dB), + free_field_threshold = get_free_field_threshold(.data$kHz), + auditory_level = pmax(.data$level - .data$free_field_threshold, 0), + pure_tone_height = get_pure_tone_height(.data$kHz), + overall_masking_level = get_overall_masking_level(.data$auditory_level, + .data$pure_tone_height, + k_m = par$k_m), + pure_tone_audible_level = get_pure_tone_audible_level(.data$auditory_level, + .data$overall_masking_level), + pure_tone_audibility = get_pure_tone_audibility(.data$pure_tone_audible_level, + al_0 = par$al_0) + ) + res$pure_spectrum <- y[y$pure_tone_audibility > 0, ] + res +} +#' Get complex sonorousness +#' +#' Computes the complex sonorousness of a sound, after +#' \insertCite{Parncutt1994;textual}{incon}. +#' @param x Object to analyse. +#' @param k_c Parncutt & Strasburger (1994) set this to 0.2 (p. 105) +#' @param ... Further parameters to pass to \code{\link{parn94}()}. +#' @return Complex sonorousness, a numeric scalar. +#' @rdname complex_sonor +#' @references +#' \insertAllCited{} +#' @export +complex_sonor <- function(x, k_c = parn94_params()$k_c, ...) { + UseMethod("complex_sonor") +} + +#' @rdname complex_sonor +#' @export +complex_sonor.parn94 <- function(x, k_c = parn94_params()$k_c, ...) { + audibility <- x$complex_spectrum$complex_tone_audibility + if (length(audibility) == 0) + 0 else + k_c * max(audibility) +} + +#' @rdname complex_sonor +#' @export +complex_sonor.default <- function(x, k_c = parn94_params()$k_c, ...) { + x <- parn94(x, ...) + complex_sonor(x, k_c = k_c) +} +#' Get free field threshold +#' +#' Returns the free-field threshold (dB SPL) of hearing in quiet for pure tones +#' of given frequencies. +#' This is the minimum sound level at which a pure tone at that frequency +#' will be heard. +#' Corresponds to Equation 2 in \insertCite{Parncutt1994;textual}{incon}. +#' @param kHz Numeric vector of frequencies in kHz. +#' @return Numeric vector of corresponding free-field thresholds in dB SPL. +#' @references +#' \insertAllCited{} +#' @export +get_free_field_threshold <- function(kHz) { + 3.64 * (kHz ^ -0.8) - + 6.5 * exp(- 0.6 * (kHz - 3.3) ^ 2) + + (10 ^ (-3)) * (kHz ^ 4) +} + +#' Get pure-tone height +#' +#' Returns the pure-tone heights (a.k.a. critical-band rates) +#' of pure tones of given frequencies. +#' Equation 3 in \insertCite{Parncutt1994;textual}{incon}. +#' @param kHz Numeric vector of frequencies in kHz. +#' @return Numeric vector of corresponding pure-tone heights, +#' with units of equivalent rectangular bandwidths (ERBs). +#' @references +#' \insertAllCited{} +#' @export +get_pure_tone_height <- function(kHz) { + H1 <- 11.17 + H0 <- 43.0 + f1 <- 0.312 + f2 <- 14.675 + H1 * log((kHz + f1) / (kHz + f2)) + H0 +} + +#' Get partial masking level +#' +#' Returns the effective reduction in dB of the audible level +#' of a masked pure tone (maskee) on account of a masking pure tone (masker). +#' Equation 4 in \insertCite{Parncutt1994;textual}{incon}. +#' @param masker_auditory_level Numeric vector of masker auditory levels. +#' @param masker_pure_tone_height Numeric vector of masker pure tone heights. +#' @param maskee_auditory_level Numeric vector of maskee auditory levels. +#' @param maskee_pure_tone_height Numeric vector of maskee pure tone heights. +#' @param k_m Parameter \code{k_m} in \insertCite{Parncutt1994;textual}{incon}. +#' represents the masking pattern gradient for a pure tone, +#' with units of dB per critical band. +#' Parncutt & Strasburger use a value of 12 in their examples, +#' but imply that 12-18 is a typical range of values for this parameter. +#' @return Matrix where element [i, j] gives the level of masking +#' for masker j on maskee i. +#' @references +#' \insertAllCited{} +#' @export +get_partial_masking_level <- function(masker_auditory_level, + masker_pure_tone_height, + maskee_auditory_level, + maskee_pure_tone_height, + k_m) { + assertthat::assert_that( + length(masker_auditory_level) == length(masker_pure_tone_height), + length(maskee_auditory_level) == length(maskee_pure_tone_height) + ) + ncol <- length(masker_auditory_level) + nrow <- length(maskee_auditory_level) + # Masker matrices + masker_auditory_level_matrix <- matrix( + data = rep(masker_auditory_level, each = nrow), + nrow = nrow, ncol = ncol, byrow = FALSE + ) + masker_pure_tone_height_matrix <- matrix( + data = rep(masker_pure_tone_height, each = nrow), + nrow = nrow, ncol = ncol, byrow = FALSE + ) + # Maskee matrix + maskee_pure_tone_height_matrix <- matrix( + data = rep(maskee_pure_tone_height, each = ncol), + nrow = nrow, ncol = ncol, byrow = TRUE + ) + # Result + masker_auditory_level_matrix - + k_m * abs( + masker_pure_tone_height_matrix - maskee_pure_tone_height_matrix + ) +} + +#' Get overall masking level +#' +#' Returns overall masking levels for a set of pure tones +#' that are assumed to be playing simultaneously. +#' Corresponds to Parncutt & Strasburger (1994) Equation 5. +#' @param auditory_level Numeric vector of auditory levels +#' @param pure_tone_height Numeric vector of pure tone heights +#' @param k_m See \code{\link{parn94_params}()}. +#' @return Numeric vector of overall masking levels (dB). +#' @references +#' \insertAllCited{} +#' @export +get_overall_masking_level <- function(auditory_level, + pure_tone_height, + k_m) { + partial_mask_matrix <- get_partial_masking_level( + masker_auditory_level = auditory_level, + masker_pure_tone_height = pure_tone_height, + maskee_auditory_level = auditory_level, + maskee_pure_tone_height = pure_tone_height, + k_m = k_m + ) + # Tones don't mask themselves + diag(partial_mask_matrix) <- 0 + # Sum over maskers to find the masking for each maskee + apply( + partial_mask_matrix, 1, + function(x) { + max(c( + 20 * log(sum(10 ^ (x / 20)), base = 10), + 0 + )) + } + ) +} + +#' Get audible level +#' +#' Returns the audible level for set of pure tones subject +#' to a given masking pattern. +#' Corresponds to Equation 6 of \insertCite{Parncutt1994;textual}{incon}. +#' @param auditory_level Numeric vector of auditory levels for +#' a set of pure tones (dB). +#' @param overall_masking_level Numeric vector of overall masking levels +#' for a set of pure tones (dB). +#' @return Numeric vector of audible levels (dB). +#' @references +#' \insertAllCited{} +#' @export +get_pure_tone_audible_level <- function(auditory_level, overall_masking_level) { + assertthat::assert_that(length(auditory_level) == length(overall_masking_level)) + pmax(0, auditory_level - overall_masking_level) +} + +#' Get audibility +#' +#' Returns the audibility of a set of pure tone components as a +#' function of their audible levels. +#' Corresponds to Equation 7 of \insertCite{Parncutt1994;textual}{incon}. +#' @param pure_tone_audible_level Numeric vector of audible levels (dB). +#' @param al_0 constant (see Equation 7 of Parncutt & Strasburger (1994)). +#' @return Numeric vector of pure tone audibilities. +#' @references +#' \insertAllCited{} +#' @export +get_pure_tone_audibility <- function(pure_tone_audible_level, al_0) { + 1 - exp(- pure_tone_audible_level / al_0) +} + +#' Get tone salience +#' +#' Gets the salience of different tones within a sonority with +#' reference to the combined (complex and pure) spectrum. +#' Salience can be interpreted as the probability of consciously +#' perceiving a given pitch. +#' @param combined_audibility Numeric vector corresponding to the +#' audibilities of each tone in the combined (pure and complex) spectrum. +#' @param k_s Numeric scalar; \insertCite{Parncutt1994;textual}{incon} +#' set this to 0.5. +#' @return Numeric vector of saliences of the same length as +#' \code{combined_audibility}, giving the salience of each respective tone. +#' @references +#' \insertAllCited{} +#' @export +get_tone_salience <- function(combined_audibility, k_s) { + if (length(combined_audibility) == 0) { + numeric() + } else { + a_max <- max(combined_audibility) + m_prime <- sum(combined_audibility) / a_max + m <- m_prime ^ k_s + (combined_audibility / a_max) * + (m / m_prime) + } +} +#' @importFrom magrittr "%>%" +NULL + +#' @importFrom stats cor +NULL + +#' @importFrom methods .valueClassTest +NULL + +#' @importFrom methods new +NULL + +# `.` <- NULL + +utils::globalVariables(c(".", ".data"), package = "incon") +#' Get multiplicity +#' +#' Computes the multiplicity of a sound, after +#' \insertCite{Parncutt1994;textual}{incon}. +#' @param x Object to analyse. +#' @param k_s Numeric scalar, parameter from Parncutt & Strasburger (1994). +#' @param ... Further parameters to pass to \code{\link{incon}()}. +#' @return Multiplicity, a numeric scalar. +#' @rdname multiplicity +#' @references +#' \insertAllCited{} +#' @export +multiplicity <- function(x, k_s = parn94_params()$k_s, ...) { + UseMethod("multiplicity") +} + +#' @rdname multiplicity +#' @export +multiplicity.parn94 <- function(x, k_s = parn94_params()$k_s, ...) { + audibility <- x$combined_spectrum$combined_audibility + if (length(audibility) == 0) { + 0 + } else { + a_max <- max(audibility) + m_prime <- sum(audibility) / a_max + m <- m_prime ^ k_s + m + } +} + +#' @rdname multiplicity +#' @export +multiplicity.default <- function(x, k_s = parn94_params()$k_s, ...) { + x <- parn94(x, ...) + multiplicity(x, k_s = k_s) +} +#' Model parameters +#' +#' This function compiles parameters for Parncutt's psychoacoustic model. +#' The parameters are defined with reference to +#' \insertCite{Parncutt1994;textual}{incon}. +#' @param unit_amplitude_in_dB (Numeric scalar) Describes the number of decibels +#' to assign to a spectral component of amplitude 1. +#' By default, amplitude 1 corresponds to the amplitude of each chord pitch's +#' fundamental frequency (though this can be changed by expressing input chords +#' as pitch-class sparse spectra, see \code{\link[hrep]{sparse_pi_spectrum}()}). +#' @param template_num_harmonics (Integerish scalar) Number of harmonics to +#' include in the spectral template, including the fundamental frequency. +#' @param template_roll_off (Numeric scalar) Roll-off rate for the +#' harmonic template. This parameter is passed to +#' \code{\link[hrep]{sparse_pi_spectrum}()}. +#' @param k_t (Numeric scalar) Relative perceptual weighting of complex versus pure tones. +#' @param k_p (Numeric scalar) Scaling coefficient for pure sonorousness. +#' @param k_c (Numeric scalar) Scaling coefficient for complex sonorousness. +#' @param k_s (Numeric scalar) Scaling coefficient for multiplicity. +#' @param k_m (Numeric scalar) Gradient of a pure tone's masking pattern. +#' @param al_0 (Numeric scalar) Audibility saturation rate. +#' @param min_midi (Numeric scalar) Lowest MIDI pitch considered by the model. +#' @param max_midi (Numeric scalar) Highest MIDI pitch considered by the model. +#' @return A list of parameters which can be passed to analysis functions +#' such as \code{\link{parn94}}. +#' @references +#' \insertAllCited{} +#' @export +parn94_params <- function( + unit_amplitude_in_dB = 60, + template_num_harmonics = 11, + template_roll_off = 1, + k_t = 3, + k_p = 0.5, + k_c = 0.2, + k_s = 0.5, + k_m = 12, + al_0 = 15, + min_midi = 0, + max_midi = 120 +) { + as.list(environment()) +} +#' Parncutt & Strasburger (1994) +#' +#' This function analyses a sonority using Richard Parncutt's +#' psychoacoustic model of harmony, as described in +#' \insertCite{Parncutt1994;textual}{incon}. +#' +#' @param x Object to analyse, +#' which will be coerced to an object of class +#' \code{\link[hrep]{sparse_pi_spectrum}}. +#' Various input types are possible: +#' * Numeric vectors will be treated as vectors of MIDI note numbers, +#' which will be expanded into their implied harmonics. +#' * A two-element list can be used to define a harmonic spectrum. +#' The first element should be a vector of MIDI note numbers, +#' the second a vector of amplitudes. +#' * The function also accepts classes from the \code{hrep} package, +#' such as produced by \code{\link[hrep]{pi_chord}()} and +#' \code{\link[hrep]{sparse_pi_spectrum}()}. +#' +#' @param par Parameter list as created by \code{\link{parn94_params}()}. +#' +#' @param ... Parameters to pass to \code{\link[hrep]{sparse_pi_spectrum}}. +#' * \code{num_harmonics}: Number of harmonics to use when expanding +#' chord tones into their implied harmonics. +#' * \code{roll_off}: Rate of amplitude roll-off for the harmonics. +#' +#' @return An list of class \code{parn94}, comprising the following components: +#' \item{pure_spectrum}{A tibble describing the sonority's pure spectrum. +#' The pure spectrum is a spectral representation of the input sound +#' after auditory masking, but before pattern matching.} +#' \item{pure_spectrum}{A tibble describing the sonority's complex spectrum. +#' The complex spectrum is created from the pure spectrum through +#' harmonic template matching.} +#' \item{combined_spectrum}{A tibble describing the sonority's combined spectrum. +#' The combined spectrum corresponds to the combination of the +#' pure and complex spectra.} +#' \item{par}{A list comprising the parameters used to perform the analysis, +#' as created by \code{\link{parn94_params}()}.} +#' +#' @references +#' \insertAllCited{} +#' +#' @rdname parn94 +#' +#' @md +#' +#' @export +parn94 <- function(x, par = parn94_params(), ...) { + UseMethod("parn94") +} + +#' @rdname parn94 +#' @export +parn94.default <- function(x, par = parn94_params(), ...) { + x <- hrep::sparse_pi_spectrum(x, digits = 0, ...) + parn94(x, par = par) +} + +#' @rdname parn94 +#' @export +parn94.sparse_pi_spectrum <- function(x, par = parn94_params(), ...) { + x <- preprocess_spectrum(x, par) + .parn94() %>% + add_pure_spectrum(x, par) %>% + add_complex_spectrum(par) %>% + add_combined_spectrum(par) %>% + add_par(par) +} + +preprocess_spectrum <- function(x, par) { + if (!hrep::is.equal_tempered(x)) stop("input must be equal-tempered") + x[hrep::pitch(x) >= par$min_midi & + hrep::pitch(x) <= par$max_midi, ] +} + +add_par <- function(x, par) { + x$par <- par + x +} + +.parn94 <- function() { + x <- list() + class(x) <- "parn94" + x +} +#' Get pitch commonality +#' +#' Gets the pitch commonality between two sonorities, after +#' \insertCite{Parncutt1994;textual}{incon}. +#' +#' @param x The first sonority to compare, passed to \code{\link{pitch_salience}()}. +#' Typically will be a numeric vector of MIDI pitches. +#' +#' @param y The second sonority to compare, passed to \code{\link{pitch_salience}()}. +#' Typically will be a numeric vector of MIDI pitches. +#' +#' @param ... Further arguments to pass to \code{\link{pitch_salience}()}. +#' +#' @return Pitch commonality, as a numeric scalar. +#' +#' @references +#' \insertAllCited{} +#' +#' @export +pitch_commonality <- function(x, y, ...) { + s1 <- pitch_salience(x, ...) + s2 <- pitch_salience(y, ...) + if (length(s1) == 0L || length(s2) == 0L) return(as.numeric(NA)) + + if (attr(s1, "min_midi") != attr(s2, "min_midi") || + attr(s1, "max_midi") != attr(s2, "max_midi")) + stop("x and y must be created with identical 'min_midi' and max_midi' ", + "parameters") + + if (all(s1 == 0 ) || all(s2 == 0)) + as.numeric(NA) else + cor(s1, s2) +} +#' Get pitch distance +#' +#' Gets the pitch distance between two sonorities, after +#' \insertCite{Parncutt1994;textual}{incon}. +#' +#' @param x The first sonority to compare, passed to \code{\link{pitch_salience}()}. +#' Typically will be a numeric vector of MIDI pitches. +#' +#' @param y The second sonority to compare, passed to \code{\link{pitch_salience}()}. +#' Typically will be a numeric vector of MIDI pitches. +#' +#' @param ... Further arguments to pass to \code{\link{pitch_salience}()}. +#' +#' @return Pitch distance, as a numeric scalar. +#' +#' @references +#' \insertAllCited{} +#' @export +pitch_distance <- function(x, y, ...) { + s1 <- pitch_salience(x, ...) + s2 <- pitch_salience(y, ...) + if (length(s1) == 0L || length(s2) == 0L) return(as.numeric(NA)) + + min_midi <- attr(s1, "min_midi") + max_midi <- attr(s1, "max_midi") + if (min_midi != attr(s2, "min_midi") || + max_midi != attr(s2, "max_midi")) + stop("x and y must be created with identical 'min_midi' and max_midi' ", + "parameters") + + # We define some matrices that will allow us to vectorise our calculation - + # see Equation 17 of Parncutt & Strasburger (1994). + # Element [i, j] of each matrix corresponds to one combination of P / P' + # in Equation 17. + + dim <- length(s1) + m1 <- matrix(data = rep(seq(from = min_midi, to = max_midi), each = dim), + nrow = dim, byrow = TRUE) + m2 <- matrix(data = rep(seq(from = min_midi, to = max_midi), each = dim), + nrow = dim, byrow = FALSE) + dist <- abs(m1 - m2) + s1_mat <- matrix(data = rep(s1, each = dim), nrow = dim, byrow = TRUE) + s2_mat <- matrix(data = rep(s2, each = dim), nrow = dim, byrow = FALSE) + + sum(s1_mat * s2_mat * dist) - + sqrt(sum(s1_mat * t(s1_mat) * dist) * + sum(t(s2_mat) * s2_mat * dist)) +} +#' Get pitch salience +#' +#' Analyses the pitch salience of a sonority, after +#' \insertCite{Parncutt1994;textual}{incon}. +#' @param x Object to analyse, passed to \code{\link{parn94}()}. +#' @param ... Further arguments to pass to \code{\link{parn94}()}. +#' @return Returns a vector where each element describes +#' the salience of a different chromatic pitch. +#' The first element of this vector corresponds to the +#' \code{min_midi} argument from \code{\link{parn94_params}}, +#' and the last element corresponds to the \code{max_midi} argument. +#' @references +#' \insertAllCited{} +#' @rdname pitch_salience +#' @export +pitch_salience <- function(x, ...) { + UseMethod("pitch_salience") +} + +#' @rdname pitch_salience +#' @export +pitch_salience.default <- function(x, ...) { + x <- parn94(x, ...) + pitch_salience(x) +} + +#' @rdname pitch_salience +#' @export +pitch_salience.pitch_salience <- function(x, ...) { + x +} + +#' @rdname pitch_salience +#' @export +pitch_salience.parn94 <- function(x, ...) { + vec <- numeric(x$par$max_midi - x$par$min_midi + 1) + ind <- x$combined_spectrum$pitch - x$par$min_midi + 1 + val <- x$combined_spectrum$salience + vec[ind] <- val + .pitch_salience(vec, x$par$min_midi, x$par$max_midi) +} + +.pitch_salience <- function(x, min_midi, max_midi) { + class(x) <- "pitch_salience" + attr(x, "min_midi") <- min_midi + attr(x, "max_midi") <- max_midi + x +} +#' Get pure sonorousness +#' +#' Computes the pure sonorousness of a sound, after +#' \insertCite{Parncutt1994;textual}{parn94}. +#' @param x Object to analyse. +#' @param k_p Parncutt & Strasburger (1994) set this to 0.5 (p. 105). +#' @param ... Further parameters to pass to \code{\link{parn94}()}. +#' @return Pure sonorousness, a numeric scalar. +#' @rdname pure_sonor +#' @references +#' \insertAllCited{} +#' @export +pure_sonor <- function(x, k_p = parn94_params()$k_p, ...) { + UseMethod("pure_sonor") +} + +#' @rdname pure_sonor +#' @export +pure_sonor.parn94 <- function(x, k_p = parn94_params()$k_p, ...) { + k_p * sqrt(sum(x$pure_spectrum$pure_tone_audibility ^ 2)) +} + +#' @rdname pure_sonor +#' @export +pure_sonor.default <- function(x, k_p = parn94_params()$k_p, ...) { + x <- parn94(x, ...) + pure_sonor(x, k_p = k_p) +} diff --git a/R/models.R b/R/models.R index 6a3273e..72d2a83 100644 --- a/R/models.R +++ b/R/models.R @@ -88,39 +88,39 @@ add_model("har_18_harmonicity", add_model("milne_13_harmonicity", "Milne (2013)", "Periodicity/harmonicity", - "har18", + "incon", consonance = TRUE, spectrum_sensitive = TRUE, continuous_pitch = TRUE, f = function(x, num_harmonics, roll_off, ...) - har18::pc_harmonicity(x, - method = "peak", - num_harmonics = num_harmonics, - rho = roll_off * 0.75, - ...)) + pc_harmonicity(x, + method = "peak", + num_harmonics = num_harmonics, + rho = roll_off * 0.75, + ...)) add_model("parn_88_root_ambig", "Parncutt (1988)", "Periodicity/harmonicity", - "parn88", + "incon", consonance = FALSE, spectrum_sensitive = FALSE, continuous_pitch = FALSE, f = function(x, num_harmonics, roll_off, ...) - parn88::root_ambiguity(x, ...)) + root_ambiguity(x, ...)) add_model("parn_94_complex", "Parncutt & Strasburger (1994)", "Periodicity/harmonicity", - "parn94", + "incon", consonance = TRUE, spectrum_sensitive = TRUE, continuous_pitch = FALSE, f = function(x, num_harmonics, roll_off, ...) - parn94::complex_sonor(x, - num_harmonics = num_harmonics, - roll_off = roll_off, - ...)) + complex_sonor(x, + num_harmonics = num_harmonics, + roll_off = roll_off, + ...)) add_model("stolz_15_periodicity", "Stolzenburg (2015)", diff --git a/R/top-level.R b/R/top-level.R index c20c96c..4ec5b9d 100644 --- a/R/top-level.R +++ b/R/top-level.R @@ -70,8 +70,8 @@ #' the root ambiguity model of \insertCite{Parncutt1988;textual}{parn88} #' (see \code{incon::\link[incon]{root_ambiguity}}). #' * `parn_94_complex`: -#' the complex sonorousness feature of \insertCite{Parncutt1994;textual}{parn94} -#' (see \code{parn94::\link[parn94]{complex_sonor}}). +#' the complex sonorousness feature of \insertCite{Parncutt1994;textual}{incon} +#' (see \code{incon::\link[incon]{complex_sonor}}). #' * `stolz_15_periodicity`: #' smoothed logarithmic periodicity, #' after \insertCite{Stolzenburg2015;textual}{stolz15} @@ -86,8 +86,8 @@ #' the roughness model of \insertCite{Hutchinson1978;textual}{dycon} #' (see \code{dycon::\link[dycon]{roughness_hutch}}). #' * `parn_94_pure`: -#' the complex sonorousness feature of \insertCite{Parncutt1994;textual}{parn94} -#' (see \code{parn94::\link[parn94]{pure_sonor}}). +#' the complex sonorousness feature of \insertCite{Parncutt1994;textual}{incon} +#' (see \code{incon::\link[incon]{pure_sonor}}). #' * `seth_93_roughness`: #' the roughness model of \insertCite{Sethares1993;textual}{dycon} #' (see \code{dycon::\link[dycon]{roughness_seth}}). @@ -105,8 +105,8 @@ #' \insertCite{Harrison2019}{incon} #' (see \code{incon::\link[incon]{corpus_dissonance}}). #' * `parn_94_mult`: -#' the multiplicity feature of \insertCite{Parncutt1994;textual}{parn94} -#' (see \code{parn94::\link[parn94]{multiplicity}}). +#' the multiplicity feature of \insertCite{Parncutt1994;textual}{incon} +#' (see \code{incon::\link[incon]{multiplicity}}). #' * `har_19_composite`: #' a model combining interference \insertCite{Hutchinson1978}{dycon}, #' periodicity/harmonicity \insertCite{Harrison2018}{incon}, diff --git a/inst/REFERENCES.bib b/inst/REFERENCES.bib index a0e82f1..0ad9b59 100644 --- a/inst/REFERENCES.bib +++ b/inst/REFERENCES.bib @@ -95,3 +95,13 @@ @article{Parncutt1988 volume = {6}, year = {1988} } + +@article{Parncutt1994, +author = {Parncutt, Richard and Strasburger, Hans}, +journal = {Perspectives of New Music}, +number = {2}, +pages = {88--129}, +title = {{Applying psychoacoustics in composition: "Harmonic" progressions of "nonharmonic" sonorities}}, +volume = {32}, +year = {1994} +} diff --git a/inst/bigand1996/bigand-1996.R b/inst/bigand1996/bigand-1996.R new file mode 100644 index 0000000..012c13a --- /dev/null +++ b/inst/bigand1996/bigand-1996.R @@ -0,0 +1,152 @@ +# This script compiles data for Bigand et al. (1996), +# which can subsequently be used for regression testing. + +# Bigand, E., Parncutt, R., & Lerdahl, F. (1996). +# Perception of musical tension in short chord sequences: +# The influence of harmonic function, sensory dissonance, horizontal motion, +# and musical training. Perception & Psychophysics, 58(1), 124–141. +# https://doi.org/10.3758/BF03205482 + +library(magrittr) + +convert_chord_type_to_pc_set <- function(chord_type) { + assertthat::assert_that( + assertthat::is.scalar(chord_type), + is.character(chord_type) || is.factor(chord_type) + ) + if (chord_type == "diminished") { + c(0, 3, 6) + } else if (chord_type == "major") { + c(0, 4, 7) + } else if (chord_type == "minor") { + c(0, 3, 7) + } else if (chord_type == "minor_seventh") { + c(0, 3, 7, 10) + } else if (chord_type == "revoiced") { + c(0, 4, 7) + } else if (chord_type == "seventh") { + c(0, 4, 7, 10) + } else stop("Unrecognised chord type") +} + +deduce_chord_pitches <- function( + pc_set, + bass_interval_size, tenor_interval_size, + alto_interval_size, soprano_interval_size, + previous_chord_pitches +) { + # Known constraints: + # - bass always rises + # - soprano always falls + bass_pitch <- previous_chord_pitches$bass + bass_interval_size + soprano_pitch <- previous_chord_pitches$soprano - soprano_interval_size + bass_pc <- bass_pitch %% 12 + soprano_pc <- soprano_pitch %% 12 + # Check all variants of alto/tenor movement and work out which satisfy the constraints + df <- expand.grid(tenor_ascends = c(FALSE, TRUE), + alto_ascends = c(FALSE, TRUE)) + df$tenor_pitch <- ifelse(df$tenor_ascends, + previous_chord_pitches$tenor + tenor_interval_size, + previous_chord_pitches$tenor - tenor_interval_size) + df$alto_pitch <- ifelse(df$alto_ascends, + previous_chord_pitches$alto + alto_interval_size, + previous_chord_pitches$alto - alto_interval_size) + df$tenor_pc <- df$tenor_pitch %% 12 + df$alto_pc <- df$alto_pitch %% 12 + df$num_repeated_pitches <- mapply( + function(tenor_pitch, alto_pitch) { + 4 - length(unique(c(bass_pitch, tenor_pitch, alto_pitch, soprano_pitch))) + }, df$tenor_pitch, df$alto_pitch + ) + df$num_unique_pcs <- mapply( + function(tenor_pc, alto_pc) { + length(unique(c(bass_pc, tenor_pc, alto_pc, soprano_pc))) + }, + df$tenor_pc, df$alto_pc + ) + df$valid <- mapply( + function(tenor_pc, alto_pc, tenor_pitch, alto_pitch) { + # Must not contain extraneous pitch classes + all( + c(bass_pc, tenor_pc, alto_pc, soprano_pc) %in% pc_set + ) && + # Don't allow part crossing + bass_pitch <= tenor_pitch && + tenor_pitch <= alto_pitch && + alto_pitch <= soprano_pitch && + # Don't allow the third to be doubled. + # The bass is always the root, so the third is 3 or 4 semitones + # above the bass + sum( + c(bass_pc, tenor_pc, alto_pc, soprano_pc) %in% + c((bass_pc + 3) %% 12, (bass_pitch + 4) %% 12) + ) < 2 && + # The seventh cannot be omitted in seventh chords + ( + !((bass_pc + 10) %% 12) %in% pc_set || + ((bass_pc + 10) %% 12) %in% c(bass_pc, tenor_pc, alto_pc, soprano_pc) + ) + }, + df$tenor_pc, df$alto_pc, df$tenor_pitch, df$alto_pitch + ) + # Solution must be valid + df_chosen <- df[df$valid, + setdiff(names(df), c("tenor_ascends", "alto_ascends"))] %>% + unique %>% + # and minimise the number of repeated pitches + (function(df) df[df$num_repeated_pitches == min(df$num_repeated_pitches), ]) %>% + # and maximise the number of unique pitch classes + (function(df) df[df$num_unique_pcs == max(df$num_unique_pcs), ]) + if (nrow(df_chosen) == 0) { + message("No solutions found") + browser() + } else if (nrow(df_chosen) > 1) { + message("Multiple solutions sound") + browser() + } + c(bass_pitch, df_chosen$tenor_pitch, + df_chosen$alto_pitch, soprano_pitch) +} + +# Actions #### + +bigand_1996 <- read.csv("inst/bigand1996/raw/bigand-1996-data.csv", stringsAsFactors = FALSE) %>% + # Correct mistakes + ## Incorrect tenor int. size - fixed using Fig. 1 + (function(df) { + df$tenor_interval_size[df$label == "C#7"] <- 4 + df$tenor_interval_size[df$label == "c#7"] <- 4 + df$tenor_interval_size[df$label == "ab7"] <- 4 + df + }) + +bigand_1996_reference_chord <- read.csv("inst/bigand1996/raw/bigand-1996-reference-chord.csv", + stringsAsFactors = FALSE) %>% as.list + +bigand_1996$root_pc <- bigand_1996$bass_interval_size # because all chords were root position +bigand_1996$pc_set <- mapply(function(root_pc, chord_type) { + convert_chord_type_to_pc_set(chord_type) %>% + add(root_pc) %>% + mod(., 12) %>% + sort +}, bigand_1996$root_pc, bigand_1996$chord_type) %>% I +bigand_1996$pitches <- mapply( + function(pc_set, + bass_interval_size, tenor_interval_size, + alto_interval_size, soprano_interval_size) { + deduce_chord_pitches( + pc_set = pc_set, + bass_interval_size = bass_interval_size, + tenor_interval_size = tenor_interval_size, + alto_interval_size = alto_interval_size, + soprano_interval_size = soprano_interval_size, + previous_chord_pitches = bigand_1996_reference_chord + ) + }, bigand_1996$pc_set, + bigand_1996$bass_interval_size, bigand_1996$tenor_interval_size, + bigand_1996$alto_interval_size, bigand_1996$soprano_interval_size, + SIMPLIFY = FALSE +) %>% I + +saveRDS(bigand_1996, "inst/bigand1996/compiled/bigand_1996.rds") +saveRDS(bigand_1996_reference_chord, "inst/bigand1996/compiled/bigand_1996_reference_chord.rds") diff --git a/inst/bigand1996/compiled/bigand_1996.rds b/inst/bigand1996/compiled/bigand_1996.rds new file mode 100644 index 0000000000000000000000000000000000000000..de2a47087b69e8b718570182ba3fa27b22ce04d1 GIT binary patch literal 1847 zcmV-72gvvziwFP!000001Lau_Y*SSfe%s0zV_V8Lwh438K~Q6ah%t*H=DuyP@hcP) zLkzLLwr{kgYinDEpn(xJ#u!748l%J*5j7@=h(`Vf9WdA+I+b96C^5ts{38+-jV3bR zeeayxzSDPk!^8x&NxAQybME(j=bZcQd!x(C6-6mlO2!l^#U;jNNsaMOrSbI|-&u-B z8E1?|?Ql`tJ{3)K&@@Mw=Hf*M`Z|QK6HTo_G_|6sO_KnSBLEKE9HbfK?H z_*SAx6X+V6g2IFso?tlIEjVu!>9)XCN&D0mk+3k%$rQ)g|0rxiu?=QOm8-gfW;i8dvttkLlJ{B~sVwBpSa~C*{U= zipHbqh>qK~ET#7(q9HLKo7Q{ucqa01npT=A2L5M8>pYyLSf3*EyfkhberoWx8{bl1 zD&Aw`Yj)rGj{h6+xaObcsmjYoQ5&q0T~urzoU66=IJMEm?z7_vKfx7#8IIS*Pjm=8 zuS-5Y3EpV)L0tq__+|LGPQs%&@3SH@FLAKp{A%OT=7Bi6Ubb$u@%j1CHTl<0c+{3Z zr#}82Bk|>5mqOf?u=3|L-n?(vzVorC+MnOZcE(?LddZiw*}m7my63myGi>kucd$n; zJj3?XJ#kBM#R9hDwNvGHCAYG7W{2-<+*Zcko;m5213kysfu5oF?_BO>ecQ@zoBr)u zwy*haU4K6)c+u?i|Y+A|k0BY=4pqcrd50(IjL8XSt9y>^%^vxe%-pK|*&p^YonZ8vU*x_ok0PHy64srAP zKScZyleV56GSh2e1Dm;!kJ*qB{LsnEY|yBVp-{vPo&3|tYzrIYS#)k^M|m0N?-jg9 zyUY(e(zM(E{o_;E;Zr7n=*vZG<7D?(`m@h*l-h^c0UqA8oApCKPFnUX+F>sAp#3qj z{yVELc;m3`mOZ-*)}1z|%h)j3K?gg1iFfo@!FSf`3$Du|4(h!C`JD$p;+(*J0el$$ zE?@2cLClZCJ~({4`C+F32mIMSj={`V@@^QQ_%eqU9?$Zg}nUJ3W!1h9EwnYP3FdV$Z-jX@@`Bw5<7D8W&$G#Z59>FW_sQ5TII1zFrt}?+d2C;62S} z_p7)b>)TBHWp-S38u249;HuN@exVQf3BJ&W{1hM8BfktEb&~u9KI)|TQGJvjaZ#Te zzpVbqPxKY=%jz$y58}GjN7Da-`pfDg>p!Wxlpl4<`Y)?Lu9wtb_-md01^o8@cw~N} z!y~(o2wx75>bB>d!@KhR?L@**D5SHSC& z=%akiqtxftU(|=ZZvAzu^Of)4!v2o7&d&FkFe5U<~v7j+@8$U`Am#L;|vo%5BQ2d@29 zoO$Va;M!lsp10e1aMk@K`lS6O`lS6O`ef&Uw7-alBgsR09*{hw=Ks6=B-3+gfaVJ;L&q{4yB+9dvXTpcvtD+1AYMo;xn)gxKv{G%1B%{USur8gk&@miUdN5?(Rffi$ybQ!QqYRsjwD_ z1;WvEMvI3K!P~t$9Sudbc)$>sjwa&xOI<3F$OMx3yLo9c6iDkCbeb%@Ft%zLYSzra likbiK#*05PGBPYFdwE#PXfwJ}#u8rqzW{wAvROPJ00849% expect_equal(0) + pure_sonor(chord_1) %>% expect_equal(0) + multiplicity(chord_1) %>% expect_equal(0) + + expect_equal( + pitch_commonality( + chord_1, chord_2 + ), + as.numeric(NA) + ) + expect_equal( + pitch_distance( + chord_1, chord_2 + ), + 0 + ) +}) +context("test-complex-sonor") + +test_that("testing against legacy code", { + test <- function(x, y, ...) { + expect_equal(complex_sonor(x, ...), y, tolerance = 1e-5) + } + + test(c(60, 64, 67), 0.309965) + test(c(60, 61, 62), 0.0007320671) + test(c(80, 83, 86), 0.181884) + test(c(1, 4, 7), 0) + test(c(40, 54, 67), 0.44699) + + test(c(60, 64, 67), 0.3045464, par = parn94_params(unit_amplitude_in_dB = 50)) + test(c(61, 62, 63), 0.001448646, par = parn94_params(unit_amplitude_in_dB = 70)) + + test(c(60, 64, 67), 0.2271383, par = parn94_params(template_num_harmonics = 5)) + test(c(60, 64, 67), 0.1523711, par = parn94_params(template_roll_off = 2)) + + # HarmonyParncutt::get_parncutt_sonority_analysis( + # c(60, 64, 67), + # parncutt_params = HarmonyParncutt::get_parncutt_params( + # template_roll_off = 2), + # cache = FALSE) +}) +context("test-helpers") + +library(magrittr) + +test_that("get_pure_tone_height matches figures given in Parncutt & Strasburger (1994)", { + expect_equal(round(get_pure_tone_height(0)), 0) + expect_equal(round(get_pure_tone_height(16)), 36) +}) + +test_that("Replicate calculation on pg. 101 of Parncutt & Strasburger (1994)", { + expect_equal(get_partial_masking_level( + masker_auditory_level = 50, + masker_pure_tone_height = get_pure_tone_height(kHz = 0.4), + maskee_auditory_level = 60, + maskee_pure_tone_height = get_pure_tone_height(kHz = 0.5), + k_m = 12 + ) %>% as.numeric %>% round, + 33) + expect_equal(get_partial_masking_level( + maskee_auditory_level = 50, + maskee_pure_tone_height = get_pure_tone_height(kHz = 0.4), + masker_auditory_level = 60, + masker_pure_tone_height = get_pure_tone_height(kHz = 0.5), + k_m = 12 + ) %>% as.numeric %>% round, + 43) +}) + +test_that("Check matrix aspects of partial_masking_level", { + mat <- get_partial_masking_level( + masker_auditory_level = c(50, 60), + masker_pure_tone_height = get_pure_tone_height(c(0.4, 0.5)), + maskee_auditory_level = c(50, 60), + maskee_pure_tone_height = get_pure_tone_height(c(0.4, 0.5)), + k_m = 12 + ) + expect_equal( + mat[1, 2] %>% round, 43 + ) + expect_equal( + mat[2, 1] %>% round, 33 + ) +}) + +test_that("get_overall_masking_level", { + expect_equal(get_overall_masking_level( + auditory_level = 50, + pure_tone_height = get_pure_tone_height(0.5), + k_m = 12 + ), 0) +}) + +test_that("get_pure_tone_audible_level", { + expect_equal( + get_pure_tone_audible_level(auditory_level = 20, overall_masking_level = 10), + 10 + ) + expect_equal( + get_pure_tone_audible_level(auditory_level = 30, overall_masking_level = 50), + 0 + ) +}) + +test_that("get_pure_tone_audibility", { + expect_equal( + get_pure_tone_audibility(pure_tone_audible_level = 0, al_0 = 15), + 0 + ) + expect_gt( + get_pure_tone_audibility(pure_tone_audible_level = 20, al_0 = 15), + 0 + ) + expect_gt( + get_pure_tone_audibility(pure_tone_audible_level = 30, al_0 = 15), + get_pure_tone_audibility(pure_tone_audible_level = 20, al_0 = 15) + ) +}) +context("test-multiplicity") + +test_that("testing against legacy code", { + test <- function(x, y, ...) { + expect_equal(multiplicity(x, ...), y, tolerance = 1e-5) + } + + test(c(60, 64, 67), 2.843946) + test(c(60, 61, 62), 1.293558) + test(c(80, 83, 86), 3.242737) + test(c(40, 54, 67), 2.220517) + + test(c(60, 64, 67), 2.835933, par = parn94_params(unit_amplitude_in_dB = 50)) + test(c(61, 62, 63), 1.293558, par = parn94_params(unit_amplitude_in_dB = 70)) + + test(hrep::sparse_pi_spectrum(c(60, 64, 67), roll_off = 2, digits = 0), + 2.899631) + + # HarmonyParncutt::get_parncutt_sonority_analysis( + # c(60, 64, 67), + # # amplitude = 70, + # # parncutt_params = HarmonyParncutt::get_parncutt_params(), + # midi_params = HarmonyParncutt::get_midi_params(roll_off = 2), + # cache = FALSE)$multiplicity +}) +context("test-pitch_commonality") + +test_that("pitch_commonality", { + expect_equal( + pitch_commonality(c(60, 64, 67), c(60, 64, 67)), + 1 + ) + expect_gt( + # G major should be closer to C major than F# major is to C major + pitch_commonality(c(60, 64, 67), c(59, 62, 67)), + pitch_commonality(c(60, 64, 67), c(61, 66, 68)) + ) + expect_gt( + # G major vs C# major + pitch_commonality(c(60, 64, 67), c(59, 62, 67)), + pitch_commonality(c(60, 64, 67), c(61, 65, 68)) + ) + expect_gt( + # G major vs C transposed + pitch_commonality(c(60, 64, 67), c(48, 76, 79)), + pitch_commonality(c(60, 64, 67), c(59, 62, 67)) + ) + # These numbers are taken from previous versions of this package, + # and have not been compared to other literature/software + expect_equal( + pitch_commonality(c(60, 64, 67), c(48, 76, 79)), + 0.894901857522212, + tolerance = 1e-4 + ) + expect_equal( + pitch_commonality(c(60, 64, 67), c(59, 62, 67)), + 0.349625432417314 + ) +}) +context("test-pitch_distance") + +test_that("pitch_distance", { + expect_equal( + pitch_distance(c(60, 64, 67), c(60, 64, 67)), + 0 + ) + expect_gt( + # Presumably C# major should be closer in pitch to + # C major than e.g. F major + pitch_distance(c(60, 64, 67), c(65, 69, 72)), + pitch_distance(c(60, 64, 67), c(61, 65, 68)) + ) + # These numbers are taken from previous versions of this package, + # and have not been compared to other literature/software + expect_equal( + pitch_distance(c(60, 64, 67), c(65, 69, 72)), + 3.86723877405512 + ) + expect_equal( + pitch_distance(c(60, 64, 67), c(65, 63, 83)), + 37.8133050960468 + ) +}) +context("test-pitch_salience") + +library(magrittr) + +test_that("pitch_salience", { + x <- .parn94() + x$combined_spectrum <- data.frame(pitch = 1:5, salience = 1) + x$par <- list(min_midi = 0, max_midi = 10) + expect_equal( + pitch_salience(x) %>% as.numeric, + c(0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0) + ) +}) +context("test-pure-sonor") + +test_that("testing against legacy code", { + test <- function(x, y, ...) { + expect_equal(pure_sonor(x, ...), y, tolerance = 1e-5) + } + + test(c(60, 64, 67), 0.6157366) + test(c(60, 61, 62), 0.005490503) + test(c(80, 83, 86), 0.6535714) + test(c(1, 4, 7), 0) + test(c(40, 54, 67), 0.5373542) + + test(c(60, 64, 67), 0.6061362, par = parn94_params(unit_amplitude_in_dB = 50)) + test(c(61, 62, 63), 0.01086485, par = parn94_params(unit_amplitude_in_dB = 70)) + + test(hrep::sparse_pi_spectrum(c(60, 64, 67), roll_off = 2, digits = 0), + 0.6136948) + + # HarmonyParncutt::get_parncutt_sonority_analysis( + # c(60, 64, 67), + # parncutt_params = HarmonyParncutt::get_parncutt_params(), + # midi_params = HarmonyParncutt::get_midi_params(roll_off = 2), + # cache = FALSE)$pure_sonorousness +}) From c748b9d01f3342fa349803fde7c6726776ec6eac Mon Sep 17 00:00:00 2001 From: Peter Harrison Date: Tue, 23 Jan 2024 23:34:38 +0000 Subject: [PATCH 15/24] Draft consolidated package version --- DESCRIPTION | 5 +- NAMESPACE | 11 + R/model-bowl18.R | 8 + R/model-har-2019.R | 4 +- R/model-jl12.R | 8 +- R/model-parn88.R | 18 +- R/model-parn94.R | 2 +- R/model-stolz15.R | 122 ++ R/models.R | 58 +- R/top-level.R | 26 +- README.Rmd | 4 +- README.md | 61 +- data-raw/by-pc-chord.R | 2 +- inst/REFERENCES.bib | 133 ++ inst/stolz15/data-formatted.csv | 2049 ++++++++++++++++++++++++++++ inst/stolz15/readme.txt | 5 + man/complex_sonor.Rd | 2 +- man/get_free_field_threshold.Rd | 2 +- man/get_partial_masking_level.Rd | 4 +- man/get_pure_tone_audibility.Rd | 2 +- man/get_pure_tone_audible_level.Rd | 2 +- man/get_pure_tone_height.Rd | 2 +- man/get_tone_salience.Rd | 2 +- man/har_19_composite.Rd | 4 +- man/incon.Rd | 26 +- man/jl_rule_1.Rd | 2 +- man/jl_rule_2.Rd | 2 +- man/jl_rule_3.Rd | 2 +- man/jl_tonal_dissonance.Rd | 2 +- man/multiplicity.Rd | 4 +- man/parn88.Rd | 8 +- man/parn94.Rd | 2 +- man/parn94_params.Rd | 2 +- man/pitch_commonality.Rd | 2 +- man/pitch_distance.Rd | 2 +- man/pitch_salience.Rd | 2 +- man/pure_sonor.Rd | 2 +- man/root.Rd | 6 +- man/root_ambiguity.Rd | 2 +- man/root_support_weights.Rd | 2 +- man/smooth_log_periodicity.Rd | 38 + tests/testthat/test-stolz15.R | 69 + 42 files changed, 2575 insertions(+), 136 deletions(-) create mode 100644 R/model-stolz15.R create mode 100644 inst/stolz15/data-formatted.csv create mode 100644 inst/stolz15/readme.txt create mode 100644 man/smooth_log_periodicity.Rd create mode 100644 tests/testthat/test-stolz15.R diff --git a/DESCRIPTION b/DESCRIPTION index 833b3ec..669a779 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -11,6 +11,7 @@ Encoding: UTF-8 LazyData: true Imports: Rdpack (>= 0.11.0), + gmp (>= 0.5.13.2), hrep (>= 0.10.0.9001), hht, checkmate (>= 1.9.4), @@ -29,10 +30,12 @@ Suggests: knitr (>= 1.23), DT (>= 0.5), covr (>= 3.2.1), - ggplot2 (>= 3.1.0.9000) + ggplot2 (>= 3.1.0.9000), + hcorp RoxygenNote: 7.3.1 Remotes: pmcharrison/hrep + pmcharrison/hcorp VignetteBuilder: knitr Byte-Compile: yes LinkingTo: diff --git a/NAMESPACE b/NAMESPACE index a3db40a..52f30f0 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1,11 +1,17 @@ # Generated by roxygen2: do not edit by hand +S3method(as.character,fraction) S3method(bowl18_min_freq_dist,default) S3method(bowl18_min_freq_dist,fr_chord) S3method(complex_sonor,default) S3method(complex_sonor,parn94) S3method(corpus_dissonance_table,corpus) S3method(count_chords,corpus) +S3method(double,fraction) +S3method(expand_harmonics,fraction) +S3method(expand_harmonics,rational_chord) +S3method(gcd,rational_chord) +S3method(half,fraction) S3method(huron_1994,default) S3method(huron_1994,int_vec) S3method(huron_1994,pc_set) @@ -36,6 +42,8 @@ S3method(roughness_vass,default) S3method(roughness_vass,sparse_fr_spectrum) S3method(roughness_wang,default) S3method(roughness_wang,sparse_fr_spectrum) +S3method(smooth_log_periodicity,default) +S3method(smooth_log_periodicity,pi_chord) S3method(sweep_harmonic_template,milne_pc_spectrum) S3method(sweep_harmonic_template,pc_set) S3method(type,corpus_dissonance_table) @@ -46,6 +54,7 @@ export(corpus_dissonance_table) export(cosine_similarity) export(count_chords) export(demo_wang) +export(fraction.numeric) export(get_free_field_threshold) export(get_overall_masking_level) export(get_partial_masking_level) @@ -70,6 +79,7 @@ export(jl_rule_2) export(jl_rule_3) export(jl_tonal_dissonance) export(kl_div_from_uniform) +export(lcm.rational_chord) export(list_models) export(multiplicity) export(parn88) @@ -88,6 +98,7 @@ export(roughness_hutch) export(roughness_seth) export(roughness_vass) export(roughness_wang) +export(smooth_log_periodicity) export(sweep_harmonic_template) export(sweep_template) export(type) diff --git a/R/model-bowl18.R b/R/model-bowl18.R index 8172cb3..bf17a87 100644 --- a/R/model-bowl18.R +++ b/R/model-bowl18.R @@ -90,12 +90,14 @@ expand_harmonics <- function(x, max_freq) { UseMethod("expand_harmonics") } +#' @export expand_harmonics.rational_chord <- function(x, max_freq) { purrr::map(seq_len(ncol(x)), ~ expand_harmonics(fraction(x[, .]), max_freq)) %>% Reduce(union, .) } +#' @export expand_harmonics.fraction <- function(x, max_freq) { stopifnot(is(max_freq, "fraction")) n <- 0L @@ -116,12 +118,14 @@ fraction <- function(x, ...) { UseMethod("fraction") } +#' @export fraction.numeric <- function(x, ...) { checkmate::qassert(x, "X2") class(x) <- "fraction" x } +#' @export as.character.fraction <- function(x, ...) { paste0(x[1], "/", x[2]) } @@ -130,6 +134,7 @@ half <- function(x) { UseMethod("half") } +#' @export half.fraction <- function(x) { if (x[1] %% 2L == 0L) x[1] <- x[1] / 2L else x[2] <- x[2] * 2L x @@ -139,6 +144,7 @@ double <- function(x) { UseMethod("double") } +#' @export double.fraction <- function(x) { if (x[2] %% 2L == 0L) x[2] <- x[2] / 2L else x[1] <- x[1] * 2L x @@ -148,6 +154,7 @@ gcd <- function(x) { UseMethod("gcd") } +#' @export gcd.rational_chord <- function(x) { y <- c(numbers::mGCD(x[1, ]), numbers::mLCM(x[2, ])) @@ -158,6 +165,7 @@ lcm <- function(x) { UseMethod("lcm") } +#' @export lcm.rational_chord <- function(x) { y <- c(numbers::mLCM(x[1, ]), numbers::mGCD(x[2, ])) diff --git a/R/model-har-2019.R b/R/model-har-2019.R index bd73bfd..a6941f6 100644 --- a/R/model-har-2019.R +++ b/R/model-har-2019.R @@ -22,8 +22,8 @@ har_19_composite_coef <- tibble::tribble( #' #' The model combines several sub-models: #' - `hutch_78_roughness`, -#' the roughness model of \insertCite{Hutchinson1978;textual}{dycon} -#' (see \code{dycon::\link[dycon]{roughness_hutch}}); +#' the roughness model of \insertCite{Hutchinson1978;textual}{incon} +#' (see \code{incon::\link[incon]{roughness_hutch}}); #' - `har_18_harmonicity`, #' the harmonicity model of \insertCite{Harrison2018;textual}{incon} #' (see \code{incon::\link[incon]{pc_harmonicity}}); diff --git a/R/model-jl12.R b/R/model-jl12.R index e35cae5..3973499 100644 --- a/R/model-jl12.R +++ b/R/model-jl12.R @@ -1,7 +1,7 @@ #' Tonal dissonance #' #' Computes tonal dissonance using the algorithm -#' of \insertCite{Johnson-Laird2012;textual}{jl12}. +#' of \insertCite{Johnson-Laird2012;textual}{incon}. #' @param x Sonority to analyse. #' This will be coerced to an object of class \code{\link[hrep]{pc_set}}. #' @return Integer scalar identifying the chord's consonance rank, @@ -41,7 +41,7 @@ jl_tonal_dissonance.pc_set <- function(x) { #' than chords occurring only in a minor scale, #' which in turn should be less dissonant than chords #' occurring in neither sort of scale." -#' \insertCite{Johnson-Laird2012}{jl12}. +#' \insertCite{Johnson-Laird2012}{incon}. #' @param pc_set (Numeric vector) Pitch-class set to analyse. #' No input checking is performed. #' @return 1, 2, or 3, corresponding to increasing degrees of dissonance. @@ -58,7 +58,7 @@ jl_rule_1 <- function(pc_set) { #' #' "Chords that are consistent with a major triad are more consonant #' than chords that are not consistent with a major triad" -#' \insertCite{Johnson-Laird2012}{jl12}. +#' \insertCite{Johnson-Laird2012}{incon}. #' Consistency with the major triad means that a major triad #' must be contained within the chord. #' Additionally, all notes must be contained within a major scale. @@ -84,7 +84,7 @@ jl_rule_2 <- function(pc_set) { #' than chords that are not built from thirds." #' "The principle allows for just one missing third intervening #' between two pitch classes a fifth apart" -#' \insertCite{Johnson-Laird2012}{jl12}. +#' \insertCite{Johnson-Laird2012}{incon}. #' @param pc_set (Numeric vector) Pitch-class set to analyse. #' @return \code{FALSE} for consonant, \code{TRUE} for dissonant. #' If a consonant solution is found, diff --git a/R/model-parn88.R b/R/model-parn88.R index 603e235..d6a9a40 100644 --- a/R/model-parn88.R +++ b/R/model-parn88.R @@ -13,21 +13,21 @@ NULL #' Parncutt (1988) #' #' Analyses a pitch-class set using the root-finding model of -#' \insertCite{Parncutt1988;textual}{parn88}. +#' \insertCite{Parncutt1988;textual}{incon}. #' @param x Sonority to analyse. #' This will be coerced to an object of class \code{\link[hrep]{pc_set}}. #' @param root_support (Character scalar or data frame) #' Identifies the root support weights to use. #' * \code{"v2"} (default) uses the updated -#' weights from \insertCite{Parncutt2006;textual}{parn88}. -#' * \code{"v1"} uses the original weights from \insertCite{Parncutt2006;textual}{parn88}. +#' weights from \insertCite{Parncutt2006;textual}{incon}. +#' * \code{"v1"} uses the original weights from \insertCite{Parncutt2006;textual}{incon}. #' #' See \code{\link{root_support_weights}} for the values of these weights. #' Alternatively, root-support weights can be provided as a data frame, #' with one column (interval) identifying the ascending interval in semitones, #' and another column (weight) identifying the corresponding root support weight. #' @param exponent (Numeric scalar) Exponent to be used when computing -#' root ambiguities. Defaults to 0.5, after \insertCite{Parncutt1988;textual}{parn88}. +#' root ambiguities. Defaults to 0.5, after \insertCite{Parncutt1988;textual}{incon}. #' @return A list with three values: #' * \code{root}, the estimated chord root (integer scalar); #' * \code{root_ambiguity}, the root ambiguity (numeric scalar), @@ -71,9 +71,9 @@ parn88.pc_set <- function(x, root_support = "v2", exponent = 0.5) { #' Root #' #' Estimates the chord root of a pitch-class set using the root-finding model of -#' \insertCite{Parncutt1988;textual}{parn88}. -#' This function is a wrapper for \code{\link{parn88}}. -#' @param ... Arguments to pass to \code{\link{parn88}}. +#' \insertCite{Parncutt1988;textual}{incon}. +#' This function is a wrapper for \code{\link{incon}}. +#' @param ... Arguments to pass to \code{\link{incon}}. #' @return The estimated chord root (integer scalar). #' @references #' \insertAllCited{} @@ -85,7 +85,7 @@ root <- function(...) { #' Root ambiguity #' #' Estimates the root ambiguity of a pitch-class set using the root-finding model of -#' \insertCite{Parncutt1988;textual}{parn88}. +#' \insertCite{Parncutt1988;textual}{incon}. #' This function is a wrapper for \code{\link{parn88}}. #' @param ... Arguments to pass to \code{\link{parn88}}. #' @return The root ambiguity (numeric scalar). @@ -99,7 +99,7 @@ root_ambiguity <- function(...) { #' Root support weights #' #' A list of different root support weights that may be used -#' by the root-finding algorithm of \insertCite{Parncutt1988;textual}{parn88}. +#' by the root-finding algorithm of \insertCite{Parncutt1988;textual}{incon}. #' See \code{\link{parn88}} for more information. #' @references #' \insertAllCited{} diff --git a/R/model-parn94.R b/R/model-parn94.R index 8d34f05..c1dd13c 100644 --- a/R/model-parn94.R +++ b/R/model-parn94.R @@ -594,7 +594,7 @@ pitch_salience.parn94 <- function(x, ...) { #' Get pure sonorousness #' #' Computes the pure sonorousness of a sound, after -#' \insertCite{Parncutt1994;textual}{parn94}. +#' \insertCite{Parncutt1994;textual}{incon}. #' @param x Object to analyse. #' @param k_p Parncutt & Strasburger (1994) set this to 0.5 (p. 105). #' @param ... Further parameters to pass to \code{\link{parn94}()}. diff --git a/R/model-stolz15.R b/R/model-stolz15.R new file mode 100644 index 0000000..a88d9de --- /dev/null +++ b/R/model-stolz15.R @@ -0,0 +1,122 @@ +#' Smoothed log periodicity +#' +#' This function computes a chord's smoothed logarithmic periodicity, +#' after \insertCite{Stolzenburg2015;textual}{incon}. +#' @param x Sonority to analyse. +#' This will be coerced to an object of class \code{\link[hrep]{pi_chord}}. +#' Numeric inputs will be interpreted as MIDI note numbers. +#' @param d (numeric scalar): +#' Maximal allowed error in the algorithm's +#' interval approximation step, expressed as +#' a fraction of the original interval. +#' The default value, 0.011, corresponds to 'Rational Tuning II' +#' in Stolzenburg's paper. +#' @return A numeric scalar identifying the chord's periodicity. +#' High values mean a higher period length, lower periodicity, +#' and lower consonance. +#' @references +#' \insertAllCited{} +#' @rdname smooth_log_periodicity +#' @export +smooth_log_periodicity <- function(x, d = 0.011) { + UseMethod("smooth_log_periodicity") +} + +#' @rdname smooth_log_periodicity +#' @export +smooth_log_periodicity.default <- function(x, d = 0.011) { + x <- hrep::pi_chord(x) + do.call(smooth_log_periodicity, as.list(environment())) +} + +#' @rdname smooth_log_periodicity +#' @export +smooth_log_periodicity.pi_chord <- function(x, d = 0.011) { + checkmate::qassert(d, "N1(0,)") + chord <- as.numeric(x) + mean(vapply(seq_along(x), function(i) { + tmp_chord <- x - x[i] + log2(relative_periodicity(rationalise_chord(tmp_chord, d = d))) + }, numeric(1))) +} + +# See DOI: 10.1080/17459737.2015.1033024 +# @param x Number to approximate +# @param d Tolerance ratio +fraction <- function(x, d, verbose = FALSE) { + x_min <- (1 - d) * x + x_max <- (1 + d) * x + a_l <- floor(x) + b_l <- 1 + a_r <- floor(x) + 1 + b_r <- 1 + a <- round(x) + b <- 1 + while(a / b < x_min || x_max < a / b) { + x_0 <- 2 * x - a / b + if (x < a / b) { + a_r <- a + b_r <- b + k <- floor((x_0 * b_l - a_l) / (a_r - x_0 * b_r)) + a_l <- a_l + k * a_r + b_l <- b_l + k * b_r + } else { + a_l <- a + b_l <- b + k <- floor((a_r - x_0 * b_r) / (x_0 * b_l - a_l)) + a_r <- a_r + k * a_l + b_r <- b_r + k * b_l + } + a <- a_l + a_r + b <- b_l + b_r + if (verbose) message("a = ", a, ", b = ", b) + } + c(a, b) +} + +# Uses rational tuning system 2 +# Non-integer inputs permitted +# @param d = 0.011 corresponds to rational tuning 2 +get_rational_interval <- function(x, d) { + stopifnot(length(x) == 1L) + octave <- floor(x / 12) + pitch_class <- x %% 12 + res <- fraction(2 ^ (pitch_class / 12), d = 0.011) + while (octave != 0) { + if (octave < 0) { + res <- half_fraction(res) + octave <- octave + 1L + } else if (octave > 0) { + res <- double_fraction(res) + octave <- octave - 1L + } + } + res +} + +half_fraction <- function(x) { + stopifnot(length(x) == 2L) + if (x[1] %% 2L == 0L) x[1] <- x[1] / 2L else x[2] <- x[2] * 2L + x +} + +double_fraction <- function(x) { + stopifnot(length(x) == 2L) + if (x[2] %% 2L == 0L) x[2] <- x[2] / 2L else x[1] <- x[1] * 2L + x +} + +rationalise_chord <- function(x, d) { + sapply(x, function(y) get_rational_interval(y, d)) +} + +relative_periodicity <- function(x) { + stopifnot(is.matrix(x), nrow(x) == 2, ncol(x) > 0L) + lcm(x[2, ]) * x[1, 1] / x[2, 1] +} + +lcm <- function(x) { + if (length(x) == 1L) x else if (length(x) == 2L) { + gmp::lcm.default(x[1], x[2]) + } else lcm(c(x[1], lcm(x[-1]))) +} diff --git a/R/models.R b/R/models.R index 72d2a83..f7295ae 100644 --- a/R/models.R +++ b/R/models.R @@ -125,12 +125,12 @@ add_model("parn_94_complex", add_model("stolz_15_periodicity", "Stolzenburg (2015)", "Periodicity/harmonicity", - "stolz15", + "incon", consonance = FALSE, spectrum_sensitive = FALSE, continuous_pitch = TRUE, f = function(x, num_harmonics, roll_off, ...) - stolz15::smooth_log_periodicity(x, ...)) + smooth_log_periodicity(x, ...)) add_model("bowl_18_min_freq_dist", "Bowling et al. (2018)", @@ -155,54 +155,54 @@ add_model("huron_94_dyadic", add_model("hutch_78_roughness", "Hutchinson & Knopoff (1978)", "Interference", - "dycon", + "incon", consonance = FALSE, spectrum_sensitive = TRUE, continuous_pitch = TRUE, f = function(x, num_harmonics, roll_off, ...) - dycon::roughness_hutch(x, - num_harmonics = num_harmonics, - roll_off = roll_off, - ...)) + roughness_hutch(x, + num_harmonics = num_harmonics, + roll_off = roll_off, + ...)) add_model("parn_94_pure", "Parncutt & Strasburger (1994)", "Interference", - "parn94", + "incon", consonance = TRUE, spectrum_sensitive = TRUE, continuous_pitch = FALSE, f = function(x, num_harmonics, roll_off, ...) - parn94::pure_sonor(x, - num_harmonics = num_harmonics, - roll_off = roll_off, - ...)) + pure_sonor(x, + num_harmonics = num_harmonics, + roll_off = roll_off, + ...)) add_model("seth_93_roughness", "Sethares (1993)", "Interference", - "dycon", + "incon", consonance = FALSE, spectrum_sensitive = TRUE, continuous_pitch = TRUE, f = function(x, num_harmonics, roll_off, ...) - dycon::roughness_seth(x, - num_harmonics = num_harmonics, - roll_off = roll_off, - ...)) + roughness_seth(x, + num_harmonics = num_harmonics, + roll_off = roll_off, + ...)) add_model("vass_01_roughness", "Vassilakis (2001)", "Interference", - "dycon", + "incon", consonance = FALSE, spectrum_sensitive = TRUE, continuous_pitch = TRUE, f = function(x, num_harmonics, roll_off, ...) - dycon::roughness_vass(x, - num_harmonics = num_harmonics, - roll_off = roll_off, - ...)) + roughness_vass(x, + num_harmonics = num_harmonics, + roll_off = roll_off, + ...)) add_model("wang_13_roughness", "Wang et al. (2013)", @@ -221,12 +221,12 @@ add_model("wang_13_roughness", add_model("jl_12_tonal", "Johnson-Laird et al. (2012)", "Culture", - "jl12", + "incon", consonance = FALSE, spectrum_sensitive = FALSE, continuous_pitch = FALSE, f = function(x, num_harmonics, roll_off, ...) - jl12::jl_tonal_dissonance(x, ...)) + jl_tonal_dissonance(x, ...)) add_model("har_19_corpus", "Harrison & Pearce (2019)", @@ -241,15 +241,15 @@ add_model("har_19_corpus", add_model("parn_94_mult", "Parncutt & Strasburger (1994)", "Numerosity", - "parn94", + "incon", consonance = TRUE, spectrum_sensitive = TRUE, continuous_pitch = FALSE, f = function(x, num_harmonics, roll_off, ...) - parn94::multiplicity(x, - num_harmonics = num_harmonics, - roll_off = roll_off, - ...)) + multiplicity(x, + num_harmonics = num_harmonics, + roll_off = roll_off, + ...)) add_model("har_19_composite", "Harrison & Pearce (2019)", diff --git a/R/top-level.R b/R/top-level.R index 4ec5b9d..e80e1fd 100644 --- a/R/top-level.R +++ b/R/top-level.R @@ -67,15 +67,15 @@ #' the harmonicity model of \insertCite{Milne2013;textual}{incon} #' (see \code{incon::\link[incon]{pc_harmonicity}}). #' * `parn_88_root_ambig`: -#' the root ambiguity model of \insertCite{Parncutt1988;textual}{parn88} +#' the root ambiguity model of \insertCite{Parncutt1988;textual}{incon} #' (see \code{incon::\link[incon]{root_ambiguity}}). #' * `parn_94_complex`: #' the complex sonorousness feature of \insertCite{Parncutt1994;textual}{incon} #' (see \code{incon::\link[incon]{complex_sonor}}). #' * `stolz_15_periodicity`: #' smoothed logarithmic periodicity, -#' after \insertCite{Stolzenburg2015;textual}{stolz15} -#' (see \code{stolz15::\link[stolz15]{smooth_log_periodicity}}). +#' after \insertCite{Stolzenburg2015;textual}{incon} +#' (see \code{incon::\link[incon]{smooth_log_periodicity}}). #' * `bowl_18_min_freq_dist`: #' the minimum frequency distance feature of #' \insertCite{Bowling2018;textual}{incon} @@ -83,23 +83,23 @@ #' * `huron_94_dyadic`: #' aggregate dyadic consonance, after \insertCite{Huron1994;textual}{incon}. #' * `hutch_78_roughness`: -#' the roughness model of \insertCite{Hutchinson1978;textual}{dycon} -#' (see \code{dycon::\link[dycon]{roughness_hutch}}). +#' the roughness model of \insertCite{Hutchinson1978;textual}{incon} +#' (see \code{incon::\link[incon]{roughness_hutch}}). #' * `parn_94_pure`: #' the complex sonorousness feature of \insertCite{Parncutt1994;textual}{incon} #' (see \code{incon::\link[incon]{pure_sonor}}). #' * `seth_93_roughness`: -#' the roughness model of \insertCite{Sethares1993;textual}{dycon} -#' (see \code{dycon::\link[dycon]{roughness_seth}}). +#' the roughness model of \insertCite{Sethares1993;textual}{incon} +#' (see \code{incon::\link[incon]{roughness_seth}}). #' * `vass_01_roughness`: -#' the roughness model of \insertCite{Vassilakis2001;textual}{dycon} -#' (see \code{dycon::\link[dycon]{roughness_vass}}). +#' the roughness model of \insertCite{Vassilakis2001;textual}{incon} +#' (see \code{incon::\link[incon]{roughness_vass}}). #' * `wang_13_roughness`: -#' the roughness model of \insertCite{Wang2013;textual}{wang13} +#' the roughness model of \insertCite{Wang2013;textual}{incon} #' (see \code{incon::\link[incon]{roughness_wang}}). #' * `jl_12_tonal`: -#' the tonal dissonance model of \insertCite{Johnson-Laird2012;textual}{jl12} -#' (see \code{jl12::\link[jl12]{jl_tonal_dissonance}}). +#' the tonal dissonance model of \insertCite{Johnson-Laird2012;textual}{incon} +#' (see \code{incon::\link[incon]{jl_tonal_dissonance}}). #' * `har_19_corpus`: #' a corpus-based model of cultural familiarity #' \insertCite{Harrison2019}{incon} @@ -108,7 +108,7 @@ #' the multiplicity feature of \insertCite{Parncutt1994;textual}{incon} #' (see \code{incon::\link[incon]{multiplicity}}). #' * `har_19_composite`: -#' a model combining interference \insertCite{Hutchinson1978}{dycon}, +#' a model combining interference \insertCite{Hutchinson1978}{incon}, #' periodicity/harmonicity \insertCite{Harrison2018}{incon}, #' and cultural familiarity, #' as introduced by \insertCite{Harrison2019;textual}{incon}. diff --git a/README.Rmd b/README.Rmd index e7219f3..ff5001c 100644 --- a/README.Rmd +++ b/README.Rmd @@ -91,7 +91,7 @@ See `?incon` for more details. Certain `incon` models can be applied to full frequency spectra rather than just symbolically notated chords. One example is the set of interference models provided in the `dycon` package. In order to run such models on full frequency -spectra one must call `hrep` and `dycon` functions explicitly, as in the +spectra one must call lower-level functions explicitly, as in the following example, which computes the roughness of a chord using the Hutchinson-Knopoff dissonance model: @@ -102,7 +102,7 @@ spectrum <- amplitude = c(1, 0.7, 0.9, 0.6) )) -dycon::roughness_hutch(spectrum) +roughness_hutch(spectrum) ``` ## References diff --git a/README.md b/README.md index 22c035a..1a3b4c4 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,9 @@ simultaneous consonance perception. ## Citation -Harrison, P. M. C., & Pearce, M. T. (2020). Simultaneous consonance in music perception and composition. *Psychological Review, 127*(2), 216-244. http://dx.doi.org/10.1037/rev0000169 +Harrison, P. M. C., & Pearce, M. T. (2019). Instantaneous consonance in +the perception and composition of Western music. *PsyArXiv*. + ## Installation @@ -56,39 +58,38 @@ documentation, `?incon`, for further details. ## Models -Currently the following models are -implemented: - -| Label | Citation | Class | Package | -| :------------------------ | :---------------------------- | :---------------------- | :------- | -| gill\_09\_harmonicity | Gill & Purves (2009) | Periodicity/harmonicity | incon | -| har\_18\_harmonicity | Harrison & Pearce (2018) | Periodicity/harmonicity | har18 | -| milne\_13\_harmonicity | Milne (2013) | Periodicity/harmonicity | har18 | -| parn\_88\_root\_ambig | Parncutt (1988) | Periodicity/harmonicity | parn88 | -| parn\_94\_complex | Parncutt & Strasburger (1994) | Periodicity/harmonicity | parn94 | -| stolz\_15\_periodicity | Stolzenburg (2015) | Periodicity/harmonicity | stolz15 | -| bowl\_18\_min\_freq\_dist | Bowling et al. (2018) | Interference | incon | -| huron\_94\_dyadic | Huron (1994) | Interference | incon | -| hutch\_78\_roughness | Hutchinson & Knopoff (1978) | Interference | dycon | -| parn\_94\_pure | Parncutt & Strasburger (1994) | Interference | parn94 | -| seth\_93\_roughness | Sethares (1993) | Interference | dycon | -| vass\_01\_roughness | Vassilakis (2001) | Interference | dycon | -| wang\_13\_roughness | Wang et al. (2013) | Interference | wang13 | -| jl\_12\_tonal | Johnson-Laird et al. (2012) | Culture | jl12 | -| har\_19\_corpus | Harrison & Pearce (2019) | Culture | corpdiss | -| parn\_94\_mult | Parncutt & Strasburger (1994) | Numerosity | parn94 | -| har\_19\_composite | Harrison & Pearce (2019) | Composite | incon | +Currently the following models are implemented: + +| Label | Citation | Class | Package | +|:----------------------|:------------------------------|:------------------------|:--------| +| gill_09_harmonicity | Gill & Purves (2009) | Periodicity/harmonicity | incon | +| har_18_harmonicity | Harrison & Pearce (2018) | Periodicity/harmonicity | incon | +| milne_13_harmonicity | Milne (2013) | Periodicity/harmonicity | incon | +| parn_88_root_ambig | Parncutt (1988) | Periodicity/harmonicity | incon | +| parn_94_complex | Parncutt & Strasburger (1994) | Periodicity/harmonicity | incon | +| stolz_15_periodicity | Stolzenburg (2015) | Periodicity/harmonicity | incon | +| bowl_18_min_freq_dist | Bowling et al. (2018) | Interference | incon | +| huron_94_dyadic | Huron (1994) | Interference | incon | +| hutch_78_roughness | Hutchinson & Knopoff (1978) | Interference | incon | +| parn_94_pure | Parncutt & Strasburger (1994) | Interference | incon | +| seth_93_roughness | Sethares (1993) | Interference | incon | +| vass_01_roughness | Vassilakis (2001) | Interference | incon | +| wang_13_roughness | Wang et al. (2013) | Interference | incon | +| jl_12_tonal | Johnson-Laird et al. (2012) | Culture | incon | +| har_19_corpus | Harrison & Pearce (2019) | Culture | incon | +| parn_94_mult | Parncutt & Strasburger (1994) | Numerosity | incon | +| har_19_composite | Harrison & Pearce (2019) | Composite | incon | See `?incon` for more details. ## Spectral representations -Certain `incon` models can be applied to full frequency spectra rather than just -symbolically notated chords. One example is the set of interference models -provided in the `dycon` package. In order to run such models on full frequency -spectra one must call `hrep` and `dycon` functions explicitly, as in the -following example, which computes the roughness of a chord using the -Hutchinson-Knopoff dissonance model: +Certain `incon` models can be applied to full frequency spectra rather +than just symbolically notated chords. One example is the set of +interference models provided in the `dycon` package. In order to run +such models on full frequency spectra one must call lower-level +functions explicitly, as in the following example, which computes the +roughness of a chord using the Hutchinson-Knopoff dissonance model: ``` r spectrum <- @@ -97,7 +98,7 @@ spectrum <- amplitude = c(1, 0.7, 0.9, 0.6) )) -dycon::roughness_hutch(spectrum) +roughness_hutch(spectrum) ``` ## References diff --git a/data-raw/by-pc-chord.R b/data-raw/by-pc-chord.R index 1b25845..22b0135 100644 --- a/data-raw/by-pc-chord.R +++ b/data-raw/by-pc-chord.R @@ -4,7 +4,7 @@ root_by_pc_chord <- integer(n) pb <- utils::txtProgressBar(max = n, style = 3) for (i in seq_len(n)) { - root_by_pc_chord[i] <- parn88::root(chords[[i]]) + root_by_pc_chord[i] <- root(chords[[i]]) utils::setTxtProgressBar(pb, i) } diff --git a/inst/REFERENCES.bib b/inst/REFERENCES.bib index 0ad9b59..2f34f0f 100644 --- a/inst/REFERENCES.bib +++ b/inst/REFERENCES.bib @@ -105,3 +105,136 @@ @article{Parncutt1994 volume = {32}, year = {1994} } + +@article{Stolzenburg2015, +abstract = {ISSN: 1745-9737 (Print) 1745-9745 (Online) Journal homepage: http://www.tandfonline.com/loi/tmam20 The perception of consonance/dissonance of musical harmonies is strongly correlated with their peri-odicity. This is shown in this article by consistently applying recent results from psychophysics and neuroacoustics, namely that the just noticeable difference between two pitches for humans is about 1{\%} for the musically important low frequency range and that periodicities of complex chords can be detected in the human brain. Based thereon, the concepts of relative and logarithmic periodicity with smooth-ing are introduced as powerful measures of harmoniousness. The presented results correlate significantly with empirical investigations on the perception of chords. Even for scales, plausible results are obtained. For example, all classical church modes appear in the front ranks of all theoretically possible seven-tone scales.}, +archivePrefix = {arXiv}, +arxivId = {1306.6458}, +author = {Stolzenburg, F.}, +doi = {10.1080/17459737.2015.1033024}, +eprint = {1306.6458}, +journal = {Journal of Mathematics and Music}, +number = {3}, +pages = {215--238}, +title = {Harmony perception by periodicity detection}, +volume = {9}, +year = {2015} +} + +@article{Mashinter2006, +author = {Mashinter, Keith}, +journal = {Empirical Musicology Review}, +number = {2}, +pages = {65--84}, +title = {{Calculating sensory dissonance: Some discrepancies arising from the models of Kameoka & Kuriyagawa, and Hutchinson & Knopoff}}, +volume = {1}, +year = {2006} +} + +@article{Hutchinson1978, +author = {Hutchinson, William and Knopoff, Leon}, +doi = {10.1080/09298217808570246}, +journal = {Journal of New Music Research}, +number = {1}, +pages = {1--29}, +title = {{The acoustic component of Western consonance}}, +volume = {7}, +year = {1978} +} + +@article{Bigand1996, +abstract = {This study investigates the effect of four variables (tonal hierarchies, sensory chordal consonance, horizontal motion, and musical training) on perceived musical tension. Participants were asked to evaluate the tension created by a chord X in sequences of three chords [C major--{\textgreater}X--{\textgreater}C major] in a C major context key. The X chords could be major or minor triads major-minor seventh, or minor seventh chords built on the 12 notes of the chromatic scale. The data were compared with Krumhansl's (1990) harmonic hierarchy and with predictions of Lerdahl's (1988) cognitive theory, Hutchinson and Knopoff's (1978) and Parncutt's (1989) sensory-psychoacoustical theories, and the model of horizontal motion defined in the paper. As a main outcome, it appears that judgments of tension arose from a convergence of several cognitive and psychoacoustics influences, whose relative importance varies, depending on musical training.}, +author = {Bigand, E and Parncutt, R and Lerdahl, F}, +doi = {10.3758/BF03205482}, +journal = {Perception & psychophysics}, +number = {1}, +pages = {124--141}, +title = {{Perception of musical tension in short chord sequences: The influence of harmonic function, sensory dissonance, horizontal motion, and musical training}}, +volume = {58}, +year = {1996} +} + +@article{Plomp1965, +abstract = {Firstly, theories are reviewed on the explanation of tonal consonance as the singular nature of tone intervals with frequency ratios corresponding with small integer numbers. An evaluation of these explanations in the light of some experimental studies supports the hypothesis, as promoted by von Helmholtz, that the difference between consonant and dissonant intervals is related to beats of adjacent partials. This relation was studied more fully by experiments in which subjects had to judge simple?tone intervals as a function of test frequency and interval width. The results may be considered as a modification of von Helmholtz's conception and indicate that, as a function of frequency, the transition range between consonant and dissonant intervals is related to critical bandwidth. Simple?tone intervals are evaluated as consonant for frequency differences exceeding this bandwidth. whereas the most dissonant intervals correspond with frequency differences of about a quarter of this bandwidth. On the base of these results, some properties of consonant intervals consisting of complex tones are explained. To answer the question whether critical bandwidth also plays a r{\^{o}}le in music, the chords of two compositions (parts of a trio sonata of J. S. Bach and of a string quartet of A. Dvo?{\'{a}}k) were analyzed by computing interval distributions as a function of frequency and number of harmonics taken into account. The results strongly suggest that, indeed, critical bandwidth plays an important r{\^{o}}le in music: for a number of harmonics representative for musical instruments, the ?density? of simultaneous partials alters as a function of frequency in the same way as critical bandwidth does.}, +author = {Plomp, R. and Levelt, W. J. M.}, +doi = {http://dx.doi.org/10.1121/1.1909741}, +journal = {The Journal of the Acoustical Society of America}, +number = {4}, +pages = {548--560}, +title = {{Tonal consonance and critical bandwidth}}, +volume = {38}, +year = {1965} +} + +@inproceedings{Villegas2010, +address = {London, England}, +author = {Villegas, Julian and Cohen, Michael and Wilson, Ian and Martens, William}, +booktitle = {128th Audio Engineering Society Convention}, +keywords = {intonation,musical consonance,pleasantness,preference,roughness,tuning}, +pages = {1--11}, +title = {Influence of Psychoacoustic Roughness on Musical Intonation Preference}, +url = {http://www.aes.org/e-lib/browse.cfm?elib=15314}, +year = {2010} +} + +@phdthesis{Vassilakis2001, +address = {Los Angeles, CA}, +author = {Vassilakis, P. N.}, +school = {UCLA}, +title = {Perceptual and Physical Properties of Amplitude Fluctuation and their Musical Significance}, +year = {2001} +} + +@article{Sethares1993, +author = {Sethares, William A.}, +journal = {The Journal of the Acoustical Society of America}, +number = {3}, +pages = {1218--1228}, +title = {Local consonance and the relationship between timbre and scale}, +volume = {94}, +year = {1993} +} + +@inproceedings{Weisser2013, +address = {Amsterdam, The Netherlands}, +author = {Weisser, St{\'{e}}phanie and Lartillot, Olivier}, +booktitle = {Proceedings of the Third International Workshop on Folk Music Analysis}, +file = {:Users/peter/Dropbox/Academic/literature/Weisser, Lartillot/Weisser, Lartillot - 2013 - Investigating non-Western musical timbre A need for joint approaches.pdf:pdf}, +pages = {33--39}, +title = {{Investigating non-Western musical timbre: A need for joint approaches}}, +year = {2013} +} + +@book{Sethares2005, +address = {London, UK}, +author = {Sethares, William A.}, +publisher = {Springer}, +title = {{Tuning, timbre, spectrum, scale}}, +year = {2005} +} + +@article{Bowling2018, +author = {Bowling, Daniel L and Purves, Dale and Gill, Kamraan Z}, +doi = {10.1073/pnas.1713206115}, +journal = {Proceedings of the National Academy of Sciences}, +number = {1}, +pages = {216--221}, +title = {{Vocal similarity predicts the relative attraction of musical chords}}, +volume = {115}, +year = {2018} +} + +@article{Gill2009, +abstract = {Scales are collections of tones that divide octaves into specific intervals used to create music. Since humans can distinguish about 240 different pitches over an octave in the mid-range of hearing, in principle a very large number of tone combinations could have been used for this purpose. Nonetheless, compositions in Western classical, folk and popular music as well as in many other musical traditions are based on a relatively small number of scales that typically comprise only five to seven tones. Why humans employ only a few of the enormous number of possible tone combinations to create music is not known. Here we show that the component intervals of the most widely used scales throughout history and across cultures are those with the greatest overall spectral similarity to a harmonic series. These findings suggest that humans prefer tone combinations that reflect the spectral characteristics of conspecific vocalizations. The analysis also highlights the spectral similarity among the scales used by different cultures.}, +author = {Gill, Kamraan Z. and Purves, Dale}, +doi = {10.1371/journal.pone.0008144}, +journal = {PLoS ONE}, +mendeley-groups = {Consonance}, +number = {12}, +pages = {1--9}, +pmid = {19997506}, +title = {{A biological rationale for musical scales}}, +volume = {4}, +year = {2009} +} + diff --git a/inst/stolz15/data-formatted.csv b/inst/stolz15/data-formatted.csv new file mode 100644 index 0000000..274d0b5 --- /dev/null +++ b/inst/stolz15/data-formatted.csv @@ -0,0 +1,2049 @@ +chord,periodicity.stolz_smooth_t2_log,omega.stolz_smooth_t2,gradus.euler_smooth_t2,similarity.gill_purves_smooth_t2 +{0},0.000,0.000,1.000,1.000000 +"{0,7}",1.000,2.000,4.000,0.666667 +"{0,9}",1.585,2.000,7.000,0.466667 +"{0,5}",1.585,3.000,5.000,0.500000 +"{0,4}",2.000,3.000,7.000,0.400000 +"{0,3}",2.322,3.000,8.000,0.333333 +"{0,8}",2.322,4.000,8.000,0.300000 +"{0,6}",2.565,2.500,11.500,0.314286 +"{0,10}",2.746,4.500,9.000,0.288889 +"{0,11}",3.000,5.000,10.000,0.183333 +"{0,2}",3.085,4.500,9.000,0.222222 +"{0,1}",3.907,6.000,11.000,0.125000 +"{0,5,9}",1.585,4.000,9.000,0.455556 +"{0,4,7}",2.000,4.000,9.000,0.466667 +"{0,3,8}",2.322,5.000,10.000,0.377778 +"{0,4,11}",3.000,5.000,10.000,0.416667 +"{0,7,11}",3.000,5.000,10.000,0.416667 +"{0,7,9}",3.057,5.000,10.333,0.451852 +"{0,3,10}",3.133,5.333,10.667,0.429630 +"{0,5,7}",3.164,5.333,9.667,0.462963 +"{0,3,7}",3.322,4.000,9.000,0.466667 +"{0,7,8}",3.322,6.000,11.000,0.363889 +"{0,8,10}",3.379,6.333,12.333,0.270370 +"{0,5,10}",3.416,6.000,10.333,0.429630 +"{0,9,10}",3.416,6.333,13.667,0.293519 +"{0,4,9}",3.585,4.000,9.000,0.455556 +"{0,2,7}",3.585,5.667,10.000,0.462963 +"{0,2,11}",3.585,6.000,13.333,0.290741 +"{0,4,5}",3.585,6.000,11.000,0.341667 +"{0,6,9}",3.628,4.667,15.333,0.371429 +"{0,2,9}",3.642,5.000,11.333,0.451852 +"{0,2,5}",3.642,6.000,11.333,0.351852 +"{0,3,9}",3.712,4.667,15.333,0.371429 +"{0,2,4}",3.723,6.000,12.000,0.281481 +"{0,3,6}",3.786,5.000,15.667,0.326984 +"{0,7,10}",3.800,5.333,11.667,0.429630 +"{0,6,10}",3.874,6.000,15.667,0.334392 +"{0,2,10}",3.887,6.667,12.667,0.270370 +"{0,5,8}",3.907,5.000,10.000,0.377778 +"{0,1,5}",3.907,6.000,11.000,0.341667 +"{0,1,8}",3.907,6.000,11.000,0.363889 +"{0,6,8}",3.924,6.333,16.000,0.278836 +"{0,4,10}",3.938,6.000,15.667,0.334392 +"{0,4,6}",4.043,6.000,15.667,0.312169 +"{0,3,5}",4.045,5.667,12.000,0.351852 +"{0,2,6}",4.100,6.000,15.667,0.312169 +"{0,1,3}",4.153,6.667,14.000,0.226852 +"{0,5,11}",4.164,6.667,15.333,0.332540 +"{0,6,7}",4.238,6.667,15.333,0.368651 +"{0,4,8}",4.322,6.667,13.667,0.366667 +"{0,2,8}",4.379,6.667,16.333,0.278836 +"{0,9,11}",4.390,6.667,13.000,0.290741 +"{0,1,6}",4.402,7.000,15.667,0.313095 +"{0,8,11}",4.655,7.667,15.667,0.272222 +"{0,6,11}",4.905,7.000,16.667,0.332540 +"{0,1,9}",4.907,7.667,15.667,0.297222 +"{0,1,10}",4.964,7.333,13.667,0.293519 +"{0,3,4}",4.989,7.667,15.667,0.286111 +"{0,5,6}",5.069,7.333,17.000,0.313095 +"{0,2,3}",5.133,7.667,14.000,0.226852 +"{0,1,7}",5.153,7.000,16.667,0.368651 +"{0,8,9}",5.240,7.667,15.667,0.297222 +"{0,3,11}",5.322,7.667,15.667,0.272222 +"{0,1,4}",5.574,7.667,15.667,0.286111 +"{0,10,11}",6.466,10.000,18.333,0.199074 +"{0,1,11}",6.712,10.000,18.333,0.199074 +"{0,1,2}",6.964,10.333,18.667,0.157407 +"{0,4,7,11}",3.000,5.000,10.000,0.441667 +"{0,3,7,8}",3.322,6.000,11.000,0.387500 +"{0,5,9,10}",3.354,7.000,14.000,0.380093 +"{0,2,7,11}",3.439,6.500,13.500,0.406481 +"{0,3,8,10}",3.511,6.750,13.000,0.385185 +"{0,5,7,9}",3.519,6.500,12.750,0.412963 +"{0,2,5,9}",3.524,6.000,12.250,0.431481 +"{0,4,5,9}",3.585,6.000,11.000,0.398611 +"{0,3,7,10}",3.680,6.000,12.250,0.448148 +"{0,4,7,9}",3.689,5.750,12.000,0.431481 +"{0,4,5,7}",3.769,7.000,14.000,0.374537 +"{0,1,5,8}",3.907,6.000,11.000,0.387500 +"{0,2,4,11}",3.939,6.500,13.500,0.360185 +"{0,2,4,7}",3.939,6.750,13.000,0.390741 +"{0,3,5,8}",4.011,6.750,13.000,0.364815 +"{0,1,3,8}",4.091,7.250,14.250,0.357870 +"{0,2,9,10}",4.104,7.250,14.250,0.344907 +"{0,2,5,10}",4.104,7.500,13.750,0.357407 +"{0,3,5,9}",4.261,6.750,17.500,0.372751 +"{0,7,8,10}",4.261,7.500,14.500,0.322685 +"{0,5,9,11}",4.269,7.750,17.000,0.347751 +"{0,3,6,9}",4.278,6.500,20.500,0.349206 +"{0,7,9,11}",4.292,7.000,13.500,0.360185 +"{0,6,8,10}",4.316,7.750,17.750,0.291270 +"{0,6,9,10}",4.344,8.000,19.500,0.321362 +"{0,3,9,10}",4.369,7.500,18.000,0.365807 +"{0,2,7,9}",4.377,6.750,13.000,0.457407 +"{0,4,7,10}",4.430,6.750,17.500,0.389418 +"{0,4,9,11}",4.439,6.500,12.750,0.406481 +"{0,4,8,11}",4.491,7.750,15.750,0.380556 +"{0,3,6,8}",4.500,7.250,18.000,0.333862 +"{0,2,6,9}",4.514,6.750,17.500,0.400529 +"{0,5,7,10}",4.538,7.250,13.500,0.418519 +"{0,4,7,8}",4.572,8.000,16.000,0.370833 +"{0,2,4,6}",4.575,7.500,17.500,0.296825 +"{0,1,3,5}",4.591,7.250,14.250,0.300463 +"{0,3,5,7}",4.614,6.500,13.000,0.390741 +"{0,4,8,10}",4.614,8.250,18.500,0.320899 +"{0,6,7,9}",4.617,7.500,18.000,0.354696 +"{0,3,5,10}",4.619,7.250,13.500,0.418519 +"{0,4,6,10}",4.655,8.250,20.750,0.323280 +"{0,1,5,9}",4.657,8.000,16.000,0.365278 +"{0,2,8,10}",4.657,8.250,18.250,0.274603 +"{0,1,6,10}",4.674,8.250,17.500,0.349140 +"{0,4,6,7}",4.679,8.000,17.250,0.343585 +"{0,2,8,11}",4.680,8.250,19.750,0.303307 +"{0,4,6,8}",4.693,8.250,18.500,0.309788 +"{0,2,6,10}",4.698,8.250,18.500,0.320899 +"{0,5,8,10}",4.699,7.000,13.500,0.357407 +"{0,1,8,10}",4.699,7.500,14.000,0.344907 +"{0,5,7,11}",4.703,7.500,16.750,0.381085 +"{0,2,5,7}",4.708,7.250,13.500,0.407407 +"{0,2,5,11}",4.708,7.750,18.250,0.336640 +"{0,2,6,8}",4.736,8.500,21.000,0.295503 +"{0,2,4,10}",4.746,8.000,18.000,0.291270 +"{0,5,7,8}",4.761,7.500,13.750,0.357870 +"{0,4,5,11}",4.769,7.750,16.500,0.364881 +"{0,2,4,8}",4.784,8.250,18.500,0.309788 +"{0,3,4,6}",4.840,8.250,19.750,0.288029 +"{0,1,3,9}",4.841,8.250,19.750,0.293585 +"{0,5,6,9}",4.844,8.000,19.000,0.356548 +"{0,3,6,10}",4.859,6.750,16.750,0.389418 +"{0,1,3,6}",4.859,8.000,18.500,0.304696 +"{0,3,7,9}",4.864,6.500,16.500,0.400529 +"{0,2,4,9}",4.877,6.500,13.000,0.412963 +"{0,2,9,11}",4.877,7.250,15.000,0.371296 +"{0,2,4,5}",4.877,8.000,14.500,0.300463 +"{0,3,4,8}",4.902,8.250,16.250,0.343056 +"{0,5,8,9}",4.907,7.750,15.750,0.354167 +"{0,4,6,11}",4.929,7.750,17.000,0.381085 +"{0,6,7,11}",4.929,7.750,16.500,0.364881 +"{0,2,3,8}",5.011,8.750,18.000,0.299140 +"{0,2,7,10}",5.038,7.000,13.500,0.385185 +"{0,4,9,10}",5.038,7.500,17.500,0.349140 +"{0,7,9,10}",5.038,7.750,15.500,0.350463 +"{0,2,3,10}",5.038,8.000,14.500,0.322685 +"{0,4,5,10}",5.038,8.250,17.500,0.354696 +"{0,3,4,7}",5.072,7.500,15.500,0.376389 +"{0,7,8,11}",5.072,8.250,16.250,0.334722 +"{0,3,6,11}",5.090,8.250,19.250,0.327381 +"{0,1,5,7}",5.091,7.750,17.000,0.371362 +"{0,1,7,8}",5.091,8.000,16.750,0.366270 +"{0,3,8,9}",5.091,8.250,19.250,0.339881 +"{0,1,5,10}",5.096,7.250,13.500,0.380093 +"{0,1,6,8}",5.109,8.000,17.250,0.354696 +"{0,4,6,9}",5.117,6.750,16.750,0.372751 +"{0,2,6,11}",5.117,7.250,17.250,0.347751 +"{0,2,6,7}",5.117,8.000,17.250,0.371362 +"{0,3,8,11}",5.152,8.000,16.000,0.325000 +"{0,3,6,7}",5.170,7.750,18.000,0.362103 +"{0,1,5,6}",5.174,8.250,17.000,0.327381 +"{0,5,6,10}",5.174,8.250,17.500,0.354696 +"{0,2,3,7}",5.180,7.500,13.750,0.374537 +"{0,1,4,7}",5.261,8.000,19.000,0.362103 +"{0,5,8,11}",5.261,8.000,18.250,0.327381 +"{0,2,7,8}",5.261,8.500,17.750,0.354696 +"{0,1,3,7}",5.275,7.500,17.500,0.343585 +"{0,1,3,10}",5.280,7.750,15.500,0.350463 +"{0,1,4,9}",5.326,7.750,15.750,0.354167 +"{0,6,8,11}",5.340,9.250,20.000,0.308862 +"{0,5,6,8}",5.359,8.000,18.000,0.299140 +"{0,1,6,9}",5.424,8.000,18.250,0.339881 +"{0,3,4,9}",5.430,7.750,18.000,0.356548 +"{0,2,5,8}",5.449,7.500,17.500,0.333862 +"{0,1,8,9}",5.487,8.500,16.500,0.330556 +"{0,3,4,11}",5.491,8.250,16.250,0.334722 +"{0,1,7,9}",5.511,9.000,19.750,0.349140 +"{0,3,4,10}",5.511,9.000,19.750,0.354696 +"{0,6,8,9}",5.528,8.750,19.500,0.293585 +"{0,3,9,11}",5.534,8.500,19.250,0.303307 +"{0,3,7,11}",5.572,7.750,15.750,0.380556 +"{0,2,5,6}",5.594,9.250,20.000,0.315807 +"{0,4,10,11}",5.600,9.250,19.250,0.329696 +"{0,6,9,11}",5.617,8.000,18.500,0.336640 +"{0,2,3,5}",5.619,8.250,16.000,0.289352 +"{0,1,4,5}",5.657,8.500,16.500,0.313889 +"{0,6,7,8}",5.670,9.750,19.750,0.292196 +"{0,8,9,11}",5.680,9.500,18.750,0.271759 +"{0,8,9,10}",5.699,10.000,19.500,0.254630 +"{0,2,3,6}",5.778,8.750,19.500,0.288029 +"{0,6,7,10}",5.778,8.750,19.500,0.354696 +"{0,4,8,9}",5.826,8.000,16.000,0.365278 +"{0,1,9,10}",5.846,9.750,19.000,0.295370 +"{0,3,5,6}",5.859,8.500,19.000,0.304696 +"{0,3,5,11}",5.864,9.000,19.750,0.308862 +"{0,2,3,9}",5.869,8.250,18.750,0.354696 +"{0,2,8,9}",5.869,9.000,19.750,0.349140 +"{0,4,5,8}",5.907,8.250,16.250,0.343056 +"{0,2,3,11}",5.930,9.750,19.000,0.271759 +"{0,1,4,10}",5.949,8.750,19.500,0.321362 +"{0,1,4,8}",5.987,8.000,16.000,0.370833 +"{0,1,5,11}",6.011,9.500,19.500,0.301918 +"{0,3,10,11}",6.011,9.750,18.250,0.316204 +"{0,7,8,9}",6.011,9.750,18.250,0.317593 +"{0,8,10,11}",6.011,10.250,18.750,0.242130 +"{0,1,4,6}",6.028,9.000,19.750,0.315807 +"{0,1,7,10}",6.030,8.250,18.750,0.365807 +"{0,2,10,11}",6.038,10.000,19.500,0.264352 +"{0,1,3,4}",6.091,9.750,19.000,0.256481 +"{0,1,2,6}",6.174,9.750,19.750,0.281085 +"{0,1,4,11}",6.180,9.500,18.000,0.332870 +"{0,7,10,11}",6.180,9.500,18.750,0.332870 +"{0,1,9,11}",6.180,10.000,18.500,0.264352 +"{0,1,3,11}",6.195,10.000,19.500,0.242130 +"{0,6,10,11}",6.198,9.750,20.500,0.301918 +"{0,5,6,11}",6.198,10.000,22.500,0.322817 +"{0,1,6,7}",6.359,10.000,22.500,0.340873 +"{0,1,7,11}",6.364,9.500,20.250,0.329696 +"{0,1,2,4}",6.369,10.000,19.500,0.237963 +"{0,2,3,4}",6.430,10.250,18.750,0.237963 +"{0,4,5,6}",6.448,10.000,20.750,0.281085 +"{0,3,4,5}",6.511,10.000,19.250,0.284259 +"{0,5,6,7}",6.528,10.250,20.750,0.325529 +"{0,1,8,11}",6.591,9.750,19.000,0.316204 +"{0,1,2,5}",6.596,10.000,18.500,0.284259 +"{0,1,2,10}",6.596,10.500,19.000,0.254630 +"{0,5,10,11}",6.619,10.500,21.000,0.318585 +"{0,1,2,9}",6.765,10.000,19.250,0.317593 +"{0,1,6,11}",6.778,10.500,21.000,0.318585 +"{0,1,2,8}",6.780,10.000,20.750,0.292196 +"{0,9,10,11}",6.788,11.000,20.250,0.235185 +"{0,1,2,7}",6.949,10.500,21.000,0.325529 +"{0,1,10,11}",7.199,11.250,20.500,0.246296 +"{0,1,2,11}",7.369,11.250,20.500,0.235185 +"{0,1,2,3}",7.530,11.500,20.750,0.192130 +"{0,2,4,7,11}",3.751,6.800,13.600,0.406111 +"{0,2,5,9,10}",3.917,7.600,14.400,0.380278 +"{0,3,7,8,10}",4.073,7.600,14.400,0.383611 +"{0,4,5,7,9}",4.132,7.400,14.200,0.383611 +"{0,4,7,9,11}",4.351,6.800,13.200,0.406111 +"{0,1,3,5,8}",4.454,7.600,14.400,0.360278 +"{0,3,5,7,8}",4.673,7.800,14.200,0.360278 +"{0,2,7,9,11}",4.702,7.800,15.400,0.401667 +"{0,4,5,9,11}",4.732,8.200,16.800,0.377817 +"{0,2,5,7,9}",4.800,7.600,14.600,0.420000 +"{0,4,6,8,10}",4.853,9.400,21.800,0.308413 +"{0,1,5,8,10}",4.858,7.400,13.800,0.380278 +"{0,4,7,8,11}",4.858,8.200,16.200,0.380833 +"{0,5,7,9,10}",4.865,8.400,16.000,0.372500 +"{0,4,5,7,11}",4.880,8.000,17.200,0.381151 +"{0,5,7,9,11}",4.880,8.400,18.000,0.359762 +"{0,2,6,8,10}",4.887,9.600,22.000,0.298413 +"{0,2,6,9,10}",4.909,9.000,20.000,0.351706 +"{0,2,4,8,10}",4.926,9.600,22.000,0.298413 +"{0,2,4,5,9}",4.936,7.800,14.200,0.383611 +"{0,3,5,8,10}",4.941,8.000,15.000,0.386667 +"{0,4,6,7,11}",4.943,8.200,16.800,0.381151 +"{0,2,4,8,11}",4.944,8.800,19.800,0.350873 +"{0,2,4,6,10}",4.958,9.400,21.800,0.308413 +"{0,3,5,7,10}",4.959,7.800,14.800,0.413333 +"{0,5,6,9,10}",4.974,9.200,20.200,0.345317 +"{0,2,4,6,8}",4.989,9.400,21.800,0.301746 +"{0,3,5,9,10}",4.993,8.600,19.200,0.381706 +"{0,2,5,9,11}",5.000,8.200,18.200,0.377540 +"{0,1,3,8,10}",5.005,8.200,15.800,0.379167 +"{0,2,7,8,11}",5.009,9.200,20.200,0.351151 +"{0,3,6,8,10}",5.016,8.000,18.200,0.358095 +"{0,2,4,7,9}",5.019,7.600,14.600,0.420000 +"{0,2,5,7,11}",5.031,8.400,19.000,0.380873 +"{0,3,6,9,10}",5.038,8.600,22.000,0.357579 +"{0,1,5,7,8}",5.054,8.400,17.000,0.365317 +"{0,1,3,5,9}",5.054,9.000,20.000,0.328373 +"{0,4,5,9,10}",5.065,8.600,17.800,0.361984 +"{0,3,5,7,9}",5.073,7.800,18.000,0.374762 +"{0,4,7,8,10}",5.073,9.200,20.200,0.338373 +"{0,2,6,7,11}",5.094,8.200,17.400,0.377817 +"{0,2,3,7,10}",5.095,7.800,14.200,0.383611 +"{0,3,7,9,10}",5.159,8.400,18.400,0.381706 +"{0,1,5,8,9}",5.171,8.600,16.600,0.361667 +"{0,2,5,7,10}",5.182,8.200,15.200,0.386667 +"{0,1,6,8,10}",5.185,8.800,18.400,0.350595 +"{0,3,4,7,8}",5.186,8.600,16.600,0.358333 +"{0,3,4,6,8}",5.200,9.200,20.200,0.315040 +"{0,1,3,7,8}",5.202,8.400,17.600,0.365317 +"{0,4,5,7,10}",5.212,8.800,19.400,0.368373 +"{0,2,4,9,11}",5.219,7.600,15.200,0.401667 +"{0,1,5,6,10}",5.238,8.800,17.400,0.361984 +"{0,2,6,7,9}",5.245,8.600,19.200,0.391706 +"{0,3,5,8,9}",5.254,9.200,20.200,0.349484 +"{0,3,4,6,11}",5.272,9.200,20.200,0.337817 +"{0,4,6,8,11}",5.272,9.400,19.800,0.354206 +"{0,2,3,7,8}",5.273,9.000,17.600,0.348651 +"{0,2,4,5,7}",5.283,8.600,16.200,0.352500 +"{0,3,6,7,9}",5.287,8.600,22.000,0.350913 +"{0,2,4,6,11}",5.294,8.000,17.600,0.359762 +"{0,2,4,6,7}",5.294,8.800,18.400,0.340595 +"{0,1,3,6,10}",5.302,8.600,18.600,0.365040 +"{0,2,5,6,9}",5.309,9.000,20.000,0.376151 +"{0,1,3,8,9}",5.319,9.400,20.400,0.335317 +"{0,3,4,8,11}",5.322,8.600,16.600,0.354167 +"{0,5,8,9,11}",5.326,9.600,20.600,0.317817 +"{0,2,4,6,9}",5.328,7.800,18.000,0.374762 +"{0,1,3,6,8}",5.333,8.800,19.400,0.351706 +"{0,3,4,8,10}",5.337,9.400,19.800,0.355040 +"{0,2,7,9,10}",5.382,8.200,15.800,0.379167 +"{0,1,5,6,8}",5.385,8.600,17.800,0.348651 +"{0,5,6,8,10}",5.385,8.600,18.200,0.320595 +"{0,1,4,5,7}",5.390,9.200,20.200,0.341984 +"{0,1,5,7,9}",5.390,9.400,19.800,0.361706 +"{0,4,6,9,10}",5.392,9.400,22.200,0.336468 +"{0,3,6,8,11}",5.400,9.400,20.400,0.331984 +"{0,1,3,5,7}",5.402,8.200,17.800,0.340595 +"{0,2,4,7,8}",5.409,9.600,20.000,0.348373 +"{0,4,6,7,9}",5.411,8.400,18.400,0.358373 +"{0,2,6,8,11}",5.423,9.400,22.200,0.325635 +"{0,2,3,8,10}",5.424,9.200,18.800,0.327262 +"{0,1,6,9,10}",5.438,9.800,20.800,0.331984 +"{0,1,4,5,9}",5.443,8.800,16.800,0.355000 +"{0,2,3,5,10}",5.446,8.800,16.400,0.349167 +"{0,2,5,8,10}",5.458,8.600,18.800,0.331429 +"{0,2,4,9,10}",5.465,8.600,18.200,0.350595 +"{0,2,4,5,10}",5.465,9.400,19.000,0.320595 +"{0,3,4,7,10}",5.473,9.000,20.000,0.386151 +"{0,2,5,8,11}",5.476,9.000,22.400,0.330079 +"{0,2,4,5,11}",5.483,8.800,18.600,0.343373 +"{0,3,4,6,10}",5.487,9.400,22.200,0.339802 +"{0,6,7,9,11}",5.494,8.600,18.400,0.343373 +"{0,2,4,7,10}",5.495,8.200,18.400,0.358095 +"{0,3,5,6,9}",5.521,9.200,22.600,0.334246 +"{0,6,8,9,10}",5.521,10.400,21.800,0.279762 +"{0,1,4,7,9}",5.526,9.000,20.000,0.366151 +"{0,3,4,6,7}",5.536,9.600,20.600,0.325317 +"{0,1,3,7,9}",5.537,9.400,22.200,0.336468 +"{0,3,4,6,9}",5.540,8.800,22.200,0.334246 +"{0,5,7,8,10}",5.541,8.600,16.200,0.349167 +"{0,4,7,10,11}",5.544,9.000,19.400,0.371151 +"{0,2,5,6,10}",5.574,9.800,20.200,0.338373 +"{0,1,3,6,9}",5.585,9.000,22.400,0.324246 +"{0,3,7,8,11}",5.586,8.600,16.600,0.354167 +"{0,3,5,9,11}",5.609,9.800,22.600,0.325635 +"{0,4,8,10,11}",5.609,10.200,20.600,0.323373 +"{0,4,7,9,10}",5.612,8.400,19.000,0.365040 +"{0,3,6,9,11}",5.623,9.200,22.600,0.330079 +"{0,2,7,8,10}",5.624,9.000,18.600,0.327262 +"{0,2,6,9,11}",5.645,8.200,18.800,0.377540 +"{0,2,3,9,10}",5.646,9.000,18.800,0.350873 +"{0,3,4,7,11}",5.658,8.200,16.200,0.380833 +"{0,5,8,9,10}",5.658,9.600,19.000,0.326111 +"{0,4,8,9,11}",5.661,9.200,18.400,0.359722 +"{0,3,6,7,8}",5.665,9.600,20.000,0.331984 +"{0,3,6,8,9}",5.668,9.400,22.800,0.324246 +"{0,5,7,8,11}",5.673,9.200,19.600,0.337817 +"{0,2,3,8,11}",5.673,10.000,21.000,0.307817 +"{0,2,5,6,8}",5.721,9.800,22.600,0.306468 +"{0,5,6,8,9}",5.721,9.800,20.800,0.311984 +"{0,1,3,5,10}",5.722,8.200,15.800,0.372500 +"{0,4,5,8,11}",5.726,9.200,19.800,0.355595 +"{0,1,5,9,11}",5.726,10.200,20.600,0.320040 +"{0,3,6,7,11}",5.736,9.200,19.800,0.355595 +"{0,4,6,7,8}",5.736,10.400,20.800,0.310873 +"{0,3,8,10,11}",5.737,9.800,18.400,0.325278 +"{0,2,3,5,8}",5.741,9.200,19.200,0.318373 +"{0,3,6,7,10}",5.751,8.800,19.200,0.386151 +"{0,2,3,5,7}",5.759,8.400,16.000,0.352500 +"{0,2,8,10,11}",5.759,10.600,22.000,0.275595 +"{0,2,3,6,9}",5.774,9.200,22.600,0.350913 +"{0,6,7,9,10}",5.774,9.800,21.600,0.327540 +"{0,1,5,9,10}",5.775,9.400,18.600,0.357222 +"{0,1,3,5,6}",5.785,9.200,19.000,0.307540 +"{0,4,5,7,8}",5.790,9.600,18.800,0.340556 +"{0,1,3,4,9}",5.790,9.800,20.800,0.311984 +"{0,5,7,8,9}",5.790,9.800,18.400,0.336111 +"{0,2,3,5,9}",5.793,8.600,19.200,0.358373 +"{0,2,5,8,9}",5.793,9.000,19.400,0.366151 +"{0,2,8,9,10}",5.793,10.400,21.200,0.303095 +"{0,1,7,8,10}",5.805,9.000,18.800,0.350873 +"{0,3,8,9,10}",5.805,10.200,21.800,0.334206 +"{0,3,4,7,9}",5.809,8.600,19.000,0.376151 +"{0,2,3,7,11}",5.809,9.400,18.600,0.359722 +"{0,4,6,9,11}",5.811,8.400,18.400,0.380873 +"{0,2,8,9,11}",5.812,9.800,21.600,0.330040 +"{0,2,3,6,11}",5.823,9.600,20.600,0.317817 +"{0,4,6,7,10}",5.823,9.800,22.600,0.339802 +"{0,2,4,10,11}",5.831,10.000,20.800,0.318929 +"{0,1,5,6,9}",5.838,9.400,20.000,0.346429 +"{0,4,5,8,9}",5.843,8.800,16.800,0.355000 +"{0,1,4,7,8}",5.854,9.200,19.800,0.366429 +"{0,2,6,8,9}",5.857,10.000,22.800,0.336468 +"{0,1,4,9,11}",5.861,9.400,18.000,0.348611 +"{0,3,8,9,11}",5.873,9.800,20.800,0.307817 +"{0,1,3,9,11}",5.873,10.400,21.800,0.275595 +"{0,3,6,10,11}",5.887,9.600,20.000,0.344484 +"{0,3,7,9,11}",5.892,9.000,19.600,0.350873 +"{0,2,7,10,11}",5.895,9.400,18.800,0.348611 +"{0,1,4,8,9}",5.907,8.600,16.600,0.361667 +"{0,1,4,6,10}",5.921,10.000,22.800,0.336468 +"{0,1,3,9,10}",5.922,10.000,21.800,0.330873 +"{0,3,4,5,9}",5.926,9.600,20.000,0.338651 +"{0,3,5,6,8}",5.933,9.000,19.600,0.318373 +"{0,6,7,8,11}",5.936,10.400,20.800,0.316984 +"{0,3,5,8,11}",5.937,9.200,19.600,0.331984 +"{0,1,3,4,7}",5.937,9.600,20.600,0.325317 +"{0,3,7,8,9}",5.937,9.600,20.000,0.345317 +"{0,1,4,7,10}",5.941,9.200,22.600,0.357579 +"{0,3,4,9,11}",5.944,9.000,19.400,0.351151 +"{0,2,3,6,8}",5.951,10.200,23.000,0.306468 +"{0,6,7,8,10}",5.951,10.400,21.200,0.299762 +"{0,1,3,7,10}",5.953,8.600,19.200,0.381706 +"{0,3,5,7,11}",5.956,9.200,19.800,0.354206 +"{0,4,6,10,11}",5.958,10.200,22.400,0.341468 +"{0,2,3,7,9}",5.959,8.600,18.600,0.391706 +"{0,1,4,5,8}",5.971,8.600,16.600,0.358333 +"{0,1,6,8,9}",5.985,9.400,19.800,0.335317 +"{0,3,4,8,9}",5.990,9.400,20.000,0.346429 +"{0,1,4,9,10}",5.993,9.800,20.800,0.331984 +"{0,1,4,7,11}",6.009,9.200,19.600,0.371151 +"{0,3,4,10,11}",6.009,10.200,20.600,0.340317 +"{0,7,8,9,11}",6.009,10.200,19.600,0.304444 +"{0,2,3,4,6}",6.023,10.400,21.800,0.269762 +"{0,1,2,6,10}",6.038,10.600,21.000,0.314206 +"{0,1,5,8,11}",6.054,9.400,19.800,0.344484 +"{0,1,7,8,9}",6.054,10.400,20.800,0.331151 +"{0,1,4,6,9}",6.057,9.000,19.400,0.349484 +"{0,5,6,7,9}",6.057,10.200,21.800,0.337540 +"{0,3,4,5,7}",6.073,9.600,19.000,0.332778 +"{0,3,7,10,11}",6.073,9.800,19.000,0.369722 +"{0,1,5,7,11}",6.073,10.000,22.200,0.341468 +"{0,2,3,4,8}",6.073,10.600,21.000,0.294206 +"{0,7,8,10,11}",6.073,10.600,19.800,0.297778 +"{0,3,4,9,10}",6.076,10.000,22.800,0.353413 +"{0,4,8,9,10}",6.076,10.200,21.400,0.314206 +"{0,2,3,6,7}",6.087,9.400,19.800,0.341984 +"{0,2,6,7,8}",6.087,10.600,22.800,0.318968 +"{0,6,8,10,11}",6.087,10.600,21.400,0.288929 +"{0,3,5,6,10}",6.102,9.000,19.000,0.368373 +"{0,2,5,6,11}",6.109,10.000,22.800,0.335913 +"{0,2,6,10,11}",6.109,10.000,21.200,0.320040 +"{0,1,3,4,8}",6.119,9.600,18.800,0.340556 +"{0,1,5,7,10}",6.122,8.600,18.600,0.381706 +"{0,1,8,9,10}",6.122,10.400,19.800,0.308611 +"{0,4,7,8,9}",6.126,9.800,19.000,0.353889 +"{0,1,4,5,11}",6.126,10.200,20.600,0.333651 +"{0,1,3,5,11}",6.137,10.200,21.000,0.288929 +"{0,4,6,8,9}",6.140,9.400,20.000,0.328373 +"{0,2,5,7,8}",6.141,9.200,19.200,0.351706 +"{0,4,5,8,10}",6.141,9.600,20.200,0.338373 +"{0,2,3,5,11}",6.159,10.200,22.000,0.300040 +"{0,2,6,7,10}",6.174,9.400,20.000,0.355040 +"{0,1,2,6,9}",6.174,9.600,20.000,0.345317 +"{0,4,5,6,9}",6.192,9.800,20.200,0.338651 +"{0,5,6,9,11}",6.192,10.400,23.200,0.335913 +"{0,1,2,4,10}",6.193,10.600,22.000,0.279762 +"{0,1,3,8,11}",6.202,9.600,19.000,0.325278 +"{0,5,6,8,11}",6.204,10.400,23.200,0.312579 +"{0,1,4,8,10}",6.205,9.200,19.800,0.351706 +"{0,2,3,4,7}",6.209,9.800,18.400,0.332778 +"{0,1,7,9,11}",6.209,10.200,21.000,0.318929 +"{0,2,4,8,9}",6.212,9.400,20.000,0.361706 +"{0,1,3,6,7}",6.216,10.200,23.000,0.333413 +"{0,1,3,7,11}",6.220,10.000,21.200,0.323373 +"{0,2,3,6,10}",6.238,9.400,20.000,0.338373 +"{0,2,5,10,11}",6.246,10.600,22.200,0.323373 +"{0,1,2,4,6}",6.257,10.200,21.000,0.286429 +"{0,4,5,6,10}",6.257,10.600,22.800,0.318968 +"{0,1,4,5,10}",6.258,9.400,19.800,0.345317 +"{0,1,4,6,8}",6.268,9.400,20.000,0.348373 +"{0,1,3,4,6}",6.268,10.200,22.000,0.290873 +"{0,2,4,5,8}",6.276,9.600,20.200,0.315040 +"{0,2,3,9,11}",6.295,10.200,21.400,0.330040 +"{0,1,6,7,9}",6.321,10.200,23.000,0.336746 +"{0,1,2,4,9}",6.329,9.600,19.000,0.336111 +"{0,6,8,9,11}",6.340,10.400,21.600,0.300040 +"{0,2,3,4,11}",6.344,10.400,19.600,0.304444 +"{0,4,9,10,11}",6.348,10.400,21.000,0.329206 +"{0,1,3,6,11}",6.351,10.600,22.200,0.310040 +"{0,2,3,10,11}",6.359,10.600,20.000,0.301111 +"{0,3,9,10,11}",6.359,11.000,22.200,0.302540 +"{0,1,2,5,10}",6.375,10.000,18.600,0.326111 +"{0,2,9,10,11}",6.382,10.600,20.000,0.306667 +"{0,1,2,6,8}",6.385,10.400,22.600,0.318968 +"{0,1,4,8,11}",6.390,9.800,19.000,0.369722 +"{0,1,8,9,11}",6.390,10.600,19.800,0.301111 +"{0,2,4,5,6}",6.392,10.600,21.400,0.286429 +"{0,4,5,6,8}",6.404,10.400,21.600,0.294206 +"{0,6,7,10,11}",6.423,10.200,21.200,0.333651 +"{0,1,2,5,6}",6.438,10.600,21.000,0.304484 +"{0,3,4,5,8}",6.454,10.200,19.400,0.323889 +"{0,1,3,4,5}",6.454,10.600,19.800,0.278611 +"{0,1,7,9,10}",6.458,10.400,21.600,0.330873 +"{0,1,3,4,11}",6.473,10.400,19.800,0.297778 +"{0,1,2,4,7}",6.476,10.400,22.000,0.324206 +"{0,2,7,8,9}",6.476,10.600,21.200,0.360873 +"{0,3,5,6,11}",6.487,10.800,23.600,0.312579 +"{0,1,6,7,10}",6.502,10.600,23.400,0.353413 +"{0,6,9,10,11}",6.509,11.000,22.800,0.295873 +"{0,1,2,5,9}",6.510,10.000,19.200,0.353889 +"{0,1,2,9,10}",6.510,10.800,20.000,0.308611 +"{0,1,2,5,8}",6.522,9.800,20.200,0.331984 +"{0,1,2,8,10}",6.522,10.600,21.400,0.303095 +"{0,5,9,10,11}",6.529,11.200,22.400,0.312540 +"{0,1,7,8,11}",6.537,10.200,21.200,0.340317 +"{0,1,2,4,8}",6.541,10.200,21.400,0.310873 +"{0,5,8,10,11}",6.541,10.400,20.800,0.310040 +"{0,2,3,8,9}",6.541,10.800,23.600,0.336746 +"{0,7,8,9,10}",6.541,11.000,20.400,0.287500 +"{0,3,5,6,7}",6.551,10.200,20.600,0.324206 +"{0,2,3,4,10}",6.559,10.600,21.400,0.299762 +"{0,5,7,10,11}",6.559,10.600,21.800,0.353373 +"{0,1,2,4,5}",6.593,10.600,20.000,0.278611 +"{0,1,4,6,7}",6.604,10.600,23.400,0.333413 +"{0,6,7,8,9}",6.604,11.200,22.400,0.290040 +"{0,1,3,4,10}",6.605,10.400,21.600,0.327540 +"{0,3,4,5,11}",6.609,10.400,21.400,0.316984 +"{0,4,5,10,11}",6.612,11.000,23.200,0.341746 +"{0,5,6,7,11}",6.623,10.800,23.000,0.335079 +"{0,3,5,10,11}",6.624,10.800,21.400,0.343373 +"{0,2,3,5,6}",6.638,10.600,21.800,0.290873 +"{0,5,6,7,10}",6.638,10.600,21.200,0.347540 +"{0,1,6,9,11}",6.657,10.200,20.600,0.323373 +"{0,5,6,7,8}",6.668,11.000,21.600,0.293373 +"{0,4,5,6,11}",6.675,11.000,23.200,0.335079 +"{0,1,4,10,11}",6.676,11.000,22.200,0.319206 +"{0,1,3,10,11}",6.688,10.800,20.200,0.300000 +"{0,7,9,10,11}",6.695,10.600,20.000,0.303333 +"{0,1,4,5,6}",6.721,10.600,21.600,0.304484 +"{0,1,6,7,8}",6.733,11.000,23.200,0.335913 +"{0,1,4,6,11}",6.740,10.600,21.200,0.353373 +"{0,2,5,6,7}",6.774,10.800,22.000,0.340873 +"{0,5,6,10,11}",6.774,11.200,23.400,0.325079 +"{0,1,5,6,7}",6.785,11.000,23.200,0.329246 +"{0,2,3,4,9}",6.812,10.200,20.600,0.337540 +"{0,1,2,4,11}",6.812,10.600,20.000,0.303333 +"{0,1,7,10,11}",6.824,11.000,22.800,0.319206 +"{0,1,6,10,11}",6.838,11.400,22.600,0.319206 +"{0,1,2,5,7}",6.858,10.600,21.200,0.340873 +"{0,1,2,8,9}",6.858,10.600,21.600,0.331151 +"{0,1,5,10,11}",6.858,10.800,21.400,0.319206 +"{0,1,6,8,11}",6.868,10.800,22.000,0.343373 +"{0,8,9,10,11}",6.876,12.200,22.200,0.239167 +"{0,1,6,7,11}",6.887,11.000,23.200,0.341746 +"{0,1,5,6,11}",6.921,11.200,23.400,0.325079 +"{0,4,5,6,7}",6.940,11.400,22.600,0.303373 +"{0,3,4,5,10}",6.941,11.000,22.200,0.347540 +"{0,1,2,7,11}",6.959,11.200,22.400,0.329206 +"{0,1,2,6,11}",6.974,10.800,21.400,0.312540 +"{0,1,2,7,10}",6.975,10.400,20.800,0.334206 +"{0,1,2,7,9}",6.993,10.800,22.000,0.360873 +"{0,1,2,5,11}",6.993,11.200,22.400,0.295873 +"{0,1,9,10,11}",6.993,12.200,22.200,0.259167 +"{0,3,4,5,6}",7.004,11.400,23.200,0.270040 +"{0,1,8,10,11}",7.005,10.800,20.200,0.300000 +"{0,1,2,7,8}",7.005,11.200,23.400,0.335913 +"{0,1,2,6,7}",7.038,11.200,23.400,0.329246 +"{0,1,2,3,7}",7.088,11.000,21.600,0.303373 +"{0,1,2,3,6}",7.102,11.400,22.600,0.270040 +"{0,1,2,3,5}",7.122,11.000,20.400,0.260833 +"{0,1,2,9,11}",7.129,10.800,20.200,0.306667 +"{0,1,2,8,11}",7.141,11.200,23.000,0.302540 +"{0,1,2,3,9}",7.258,11.400,23.200,0.290040 +"{0,1,2,3,8}",7.270,11.600,22.800,0.293373 +"{0,2,3,4,5}",7.276,11.200,20.600,0.260833 +"{0,1,2,10,11}",7.310,12.400,22.400,0.259167 +"{0,1,2,3,11}",7.424,12.400,22.400,0.239167 +"{0,1,2,3,10}",7.439,11.200,20.600,0.287500 +"{0,1,2,3,4}",7.541,12.400,22.400,0.223333 +"{0,4,5,7,9,11}",4.997,8.500,17.500,0.374841 +"{0,2,4,7,9,11}",5.016,8.000,15.500,0.409259 +"{0,2,4,6,8,10}",5.072,10.500,24.500,0.302857 +"{0,2,4,7,8,11}",5.174,9.500,20.167,0.368915 +"{0,2,5,7,9,10}",5.180,8.667,16.167,0.383148 +"{0,2,4,6,7,11}",5.245,8.667,17.667,0.374841 +"{0,1,3,5,7,8}",5.319,8.833,17.833,0.355397 +"{0,3,5,7,8,10}",5.338,8.833,16.333,0.374259 +"{0,2,5,6,9,10}",5.340,9.833,20.500,0.358360 +"{0,2,5,7,9,11}",5.387,8.833,19.000,0.385767 +"{0,1,5,6,8,10}",5.403,9.000,18.000,0.357619 +"{0,2,4,5,9,10}",5.416,9.333,18.333,0.357619 +"{0,1,3,5,8,9}",5.417,9.833,20.500,0.347249 +"{0,1,3,5,8,10}",5.420,8.500,16.000,0.383148 +"{0,2,4,5,7,9}",5.431,8.667,16.167,0.385370 +"{0,2,4,5,7,11}",5.456,9.000,19.000,0.370397 +"{0,3,4,6,8,11}",5.500,9.833,20.500,0.342249 +"{0,3,4,7,8,10}",5.501,9.833,20.500,0.360582 +"{0,3,4,6,8,10}",5.513,9.833,22.000,0.336164 +"{0,2,4,6,8,11}",5.519,9.667,21.833,0.344497 +"{0,2,4,6,9,10}",5.522,9.833,22.000,0.345053 +"{0,2,6,7,9,11}",5.537,9.000,19.000,0.379286 +"{0,4,5,7,9,10}",5.538,9.333,19.333,0.359841 +"{0,3,5,7,9,10}",5.548,9.167,19.333,0.377434 +"{0,2,3,7,8,10}",5.574,9.333,18.333,0.353175 +"{0,1,4,5,7,9}",5.589,9.833,20.500,0.353915 +"{0,2,3,5,7,10}",5.592,8.833,16.333,0.374259 +"{0,2,4,5,9,11}",5.598,9.000,18.500,0.379286 +"{0,1,3,5,7,9}",5.599,9.833,22.000,0.340608 +"{0,2,3,5,9,10}",5.620,9.333,19.333,0.364286 +"{0,1,3,6,8,10}",5.623,9.167,19.333,0.370767 +"{0,3,4,7,8,11}",5.655,9.000,17.000,0.364444 +"{0,4,6,7,9,11}",5.676,8.833,18.333,0.370397 +"{0,2,5,6,8,10}",5.683,10.167,22.333,0.318386 +"{0,5,6,8,9,10}",5.683,10.500,21.500,0.310397 +"{0,2,4,7,8,10}",5.687,10.167,22.333,0.329497 +"{0,4,5,8,9,11}",5.702,10.000,20.667,0.351323 +"{0,4,7,8,10,11}",5.727,10.500,21.167,0.339471 +"{0,2,5,8,9,10}",5.743,10.000,20.500,0.339841 +"{0,2,4,7,10,11}",5.746,9.500,20.000,0.361508 +"{0,3,6,7,9,10}",5.752,10.000,23.000,0.354868 +"{0,2,3,7,9,10}",5.759,9.167,18.667,0.375397 +"{0,2,5,8,9,11}",5.759,10.000,23.000,0.345423 +"{0,1,3,7,8,10}",5.778,9.333,19.333,0.375397 +"{0,3,4,6,7,11}",5.780,9.833,20.500,0.353545 +"{0,3,5,6,9,10}",5.780,10.000,23.000,0.354868 +"{0,1,5,6,9,10}",5.780,10.167,20.833,0.349656 +"{0,4,6,7,8,11}",5.780,10.333,20.500,0.346138 +"{0,2,3,7,8,11}",5.781,10.167,20.833,0.344656 +"{0,3,4,8,10,11}",5.781,10.333,20.500,0.343915 +"{0,2,5,7,8,11}",5.784,10.000,23.000,0.347646 +"{0,2,6,8,9,10}",5.796,11.167,23.833,0.313386 +"{0,2,4,8,10,11}",5.800,11.000,23.667,0.317275 +"{0,2,4,6,7,9}",5.801,9.167,19.333,0.372989 +"{0,4,5,7,8,11}",5.825,9.833,20.500,0.353545 +"{0,5,7,8,9,11}",5.825,10.500,21.500,0.320952 +"{0,1,3,6,9,10}",5.834,10.167,23.167,0.345979 +"{0,3,6,8,10,11}",5.846,10.167,20.667,0.334841 +"{0,2,7,8,10,11}",5.853,10.667,21.667,0.318730 +"{0,1,3,5,6,8}",5.859,9.500,19.500,0.339841 +"{0,1,4,5,7,8}",5.863,10.000,20.667,0.349656 +"{0,1,5,7,8,9}",5.863,10.500,20.667,0.344471 +"{0,1,4,5,9,11}",5.869,10.333,20.500,0.348360 +"{0,3,5,8,9,11}",5.879,10.667,23.333,0.323201 +"{0,1,3,5,9,11}",5.879,11.000,23.667,0.306164 +"{0,3,4,6,7,8}",5.887,10.667,21.333,0.320026 +"{0,3,5,7,9,11}",5.894,10.000,22.333,0.344497 +"{0,3,6,7,8,10}",5.900,10.000,20.500,0.348730 +"{0,3,6,8,9,10}",5.903,10.500,23.500,0.329683 +"{0,1,6,8,9,10}",5.903,10.667,21.667,0.323730 +"{0,3,4,6,10,11}",5.906,10.500,22.667,0.344868 +"{0,2,6,7,8,11}",5.906,10.667,22.833,0.338201 +"{0,4,6,8,10,11}",5.906,10.833,23.000,0.326164 +"{0,1,4,5,8,9}",5.907,9.333,17.333,0.358333 +"{0,1,5,7,8,10}",5.920,9.167,18.667,0.364286 +"{0,3,5,7,8,9}",5.932,10.167,20.667,0.342063 +"{0,1,4,7,9,11}",5.938,9.667,20.167,0.361508 +"{0,4,7,8,9,11}",5.938,9.833,19.167,0.356296 +"{0,3,4,6,9,11}",5.950,9.833,22.833,0.347646 +"{0,3,4,6,7,10}",5.959,10.500,23.167,0.350423 +"{0,2,3,6,8,11}",5.959,10.667,23.333,0.323201 +"{0,4,6,7,8,10}",5.959,11.167,23.833,0.311164 +"{0,3,4,6,9,10}",5.962,10.500,25.167,0.342487 +"{0,1,4,6,9,10}",5.962,10.667,23.333,0.339312 +"{0,2,5,6,8,9}",5.962,10.667,23.333,0.337090 +"{0,2,3,5,8,10}",5.964,9.667,19.833,0.344101 +"{0,2,4,6,9,11}",5.968,8.667,18.833,0.385767 +"{0,2,4,5,7,10}",5.969,9.667,19.833,0.350767 +"{0,1,5,8,9,11}",5.976,10.667,21.333,0.330582 +"{0,1,3,7,8,9}",5.986,10.667,22.833,0.338757 +"{0,3,4,5,7,9}",5.992,9.833,20.333,0.348730 +"{0,1,4,5,7,11}",5.992,10.333,22.500,0.351534 +"{0,1,5,7,9,11}",5.992,10.667,22.833,0.335053 +"{0,1,3,5,6,10}",6.001,9.500,19.000,0.359841 +"{0,3,7,8,10,11}",6.001,10.333,19.500,0.345185 +"{0,3,4,6,7,9}",6.003,10.000,23.000,0.339312 +"{0,2,3,5,7,8}",6.004,9.667,19.167,0.339841 +"{0,1,5,8,9,10}",6.017,10.000,19.333,0.347963 +"{0,2,3,4,6,11}",6.019,10.333,21.333,0.320952 +"{0,1,3,6,8,9}",6.026,10.167,23.167,0.337090 +"{0,1,3,4,8,9}",6.030,10.167,20.833,0.340767 +"{0,2,3,6,7,9}",6.032,10.000,23.000,0.361534 +"{0,2,6,8,10,11}",6.032,11.000,23.833,0.306164 +"{0,4,6,8,9,10}",6.032,11.000,23.833,0.308942 +"{0,1,3,4,7,9}",6.045,10.500,23.167,0.337090 +"{0,2,3,5,7,9}",6.048,9.000,19.167,0.372989 +"{0,3,6,7,8,11}",6.054,10.500,21.167,0.335767 +"{0,1,2,6,9,10}",6.060,10.833,21.500,0.335582 +"{0,3,4,7,10,11}",6.061,10.167,20.833,0.369101 +"{0,2,3,4,8,11}",6.061,10.667,21.333,0.326138 +"{0,2,7,8,9,11}",6.064,10.667,22.167,0.347619 +"{0,1,5,6,8,9}",6.070,10.167,20.833,0.340767 +"{0,2,3,6,7,11}",6.072,10.000,20.667,0.351323 +"{0,3,6,7,9,11}",6.072,10.000,22.833,0.340979 +"{0,2,4,6,7,8}",6.072,11.000,23.167,0.317831 +"{0,2,3,8,10,11}",6.074,10.833,21.833,0.312063 +"{0,4,5,6,9,10}",6.075,10.833,23.000,0.334312 +"{0,3,5,8,9,10}",6.086,10.500,22.000,0.353175 +"{0,4,5,7,8,9}",6.089,10.333,19.500,0.341296 +"{0,2,4,6,10,11}",6.091,10.500,22.833,0.335053 +"{0,2,4,8,9,10}",6.092,10.833,23.167,0.324497 +"{0,1,3,4,5,7}",6.099,10.500,21.500,0.314841 +"{0,1,3,8,9,11}",6.099,10.667,21.667,0.312063 +"{0,2,6,7,9,10}",6.104,10.167,21.667,0.357619 +"{0,2,4,8,9,11}",6.108,10.000,21.500,0.365952 +"{0,2,3,4,7,8}",6.114,10.667,20.833,0.331138 +"{0,1,3,7,9,11}",6.114,10.833,23.667,0.317275 +"{0,2,5,6,9,11}",6.119,10.167,22.667,0.363201 +"{0,3,5,6,8,10}",6.123,9.333,19.500,0.350767 +"{0,2,3,4,6,8}",6.126,11.167,23.833,0.295608 +"{0,2,5,6,8,11}",6.129,10.667,25.333,0.324153 +"{0,1,2,4,6,10}",6.129,11.167,23.833,0.308942 +"{0,2,4,7,9,10}",6.136,9.167,19.333,0.370767 +"{0,1,4,6,8,10}",6.139,10.167,22.500,0.345053 +"{0,2,3,6,8,10}",6.139,10.333,22.667,0.329497 +"{0,3,5,6,8,9}",6.139,10.667,23.667,0.321534 +"{0,1,3,8,9,10}",6.140,10.833,22.333,0.341508 +"{0,1,4,7,8,9}",6.143,10.333,21.000,0.351878 +"{0,1,3,4,5,9}",6.143,10.833,21.500,0.317804 +"{0,2,4,6,7,10}",6.145,10.167,22.500,0.336164 +"{0,2,4,6,8,9}",6.145,10.167,22.500,0.340608 +"{0,2,3,6,9,11}",6.145,10.333,23.333,0.345423 +"{0,2,4,5,8,10}",6.146,10.500,22.833,0.318386 +"{0,1,3,5,8,11}",6.153,9.833,20.333,0.334841 +"{0,1,3,4,7,8}",6.153,10.000,20.667,0.349656 +"{0,3,5,6,7,9}",6.154,10.500,23.500,0.331905 +"{0,2,3,6,9,10}",6.157,10.167,23.000,0.352646 +"{0,3,4,8,9,11}",6.158,10.000,20.667,0.344656 +"{0,1,3,4,9,11}",6.158,10.500,21.500,0.318730 +"{0,2,4,5,8,11}",6.161,10.167,23.000,0.340979 +"{0,2,3,5,9,11}",6.161,10.500,23.333,0.338016 +"{0,3,5,7,8,11}",6.168,9.833,20.167,0.342249 +"{0,1,3,5,7,11}",6.168,10.500,22.833,0.326164 +"{0,2,3,5,8,11}",6.171,10.500,23.500,0.318757 +"{0,3,4,7,9,11}",6.174,9.333,19.667,0.368915 +"{0,2,3,4,7,11}",6.174,10.000,19.167,0.356296 +"{0,1,3,5,9,10}",6.184,10.167,21.667,0.355397 +"{0,4,6,7,10,11}",6.185,10.500,22.667,0.351534 +"{0,2,3,5,7,11}",6.187,10.167,21.667,0.345952 +"{0,2,3,7,10,11}",6.187,10.167,19.500,0.354074 +"{0,2,4,5,6,9}",6.188,10.167,20.667,0.348730 +"{0,1,2,4,9,10}",6.190,10.667,21.667,0.323730 +"{0,3,6,9,10,11}",6.198,11.000,24.000,0.327090 +"{0,1,4,5,7,10}",6.199,10.000,23.000,0.354868 +"{0,1,3,6,7,9}",6.208,10.667,25.333,0.331376 +"{0,1,3,5,7,10}",6.209,9.000,19.167,0.377434 +"{0,3,6,8,9,11}",6.224,10.667,23.667,0.318757 +"{0,2,3,5,6,9}",6.227,10.500,23.500,0.339312 +"{0,5,6,7,9,10}",6.227,10.833,22.333,0.334841 +"{0,2,5,9,10,11}",6.233,10.833,21.833,0.340952 +"{0,1,3,5,6,9}",6.236,10.333,23.167,0.325979 +"{0,1,2,6,8,10}",6.236,11.000,23.167,0.324497 +"{0,2,3,4,6,7}",6.239,10.667,21.667,0.314841 +"{0,2,3,4,8,10}",6.240,11.000,23.167,0.315608 +"{0,1,2,4,6,9}",6.242,9.833,20.333,0.342063 +"{0,2,6,8,9,11}",6.242,10.500,23.333,0.338016 +"{0,4,6,7,9,10}",6.242,10.500,23.333,0.336349 +"{0,2,4,5,6,10}",6.242,11.167,23.333,0.311164 +"{0,1,4,5,9,10}",6.243,10.167,20.833,0.349656 +"{0,2,5,8,10,11}",6.243,10.833,23.833,0.315794 +"{0,1,3,4,6,9}",6.252,10.333,23.333,0.321534 +"{0,4,5,6,8,10}",6.252,10.833,23.167,0.311164 +"{0,2,6,7,8,10}",6.252,11.000,23.333,0.315608 +"{0,1,4,8,9,11}",6.256,10.167,19.333,0.354074 +"{0,2,5,7,10,11}",6.259,10.500,22.000,0.357063 +"{0,3,4,5,7,8}",6.266,10.333,19.667,0.332407 +"{0,1,5,7,8,11}",6.266,10.500,22.667,0.344868 +"{0,4,5,7,8,10}",6.269,10.500,22.000,0.337619 +"{0,3,4,5,9,11}",6.271,10.667,22.833,0.338201 +"{0,1,2,5,6,10}",6.280,10.833,21.000,0.333360 +"{0,3,7,8,9,11}",6.281,10.500,21.333,0.326138 +"{0,3,4,7,9,10}",6.284,10.333,22.833,0.365979 +"{0,3,6,7,10,11}",6.293,10.333,21.000,0.357989 +"{0,2,5,7,8,10}",6.297,9.667,19.833,0.344101 +"{0,2,3,7,9,11}",6.300,10.167,21.167,0.365952 +"{0,1,4,5,8,11}",6.309,10.333,21.000,0.357989 +"{0,3,4,5,8,9}",6.309,10.667,21.333,0.336323 +"{0,2,6,7,10,11}",6.311,10.167,21.000,0.348360 +"{0,1,4,7,9,10}",6.312,10.500,23.500,0.345979 +"{0,4,5,8,9,10}",6.312,10.500,21.333,0.333360 +"{0,3,4,6,8,9}",6.321,10.333,23.167,0.325979 +"{0,1,3,6,9,11}",6.321,10.667,23.667,0.315794 +"{0,1,4,7,8,10}",6.322,10.167,23.000,0.352646 +"{0,1,3,7,9,10}",6.322,10.667,23.500,0.349683 +"{0,2,5,6,7,9}",6.340,10.500,22.000,0.366508 +"{0,1,2,5,9,10}",6.341,10.333,19.500,0.347963 +"{0,4,7,9,10,11}",6.343,10.167,20.667,0.349841 +"{0,2,3,6,7,8}",6.346,11.000,23.167,0.325423 +"{0,1,2,5,8,10}",6.351,10.167,20.667,0.339841 +"{0,2,3,4,7,10}",6.353,10.167,20.667,0.348730 +"{0,1,3,6,7,10}",6.359,10.500,23.000,0.365979 +"{0,2,3,6,10,11}",6.365,10.500,21.333,0.330582 +"{0,2,3,4,6,10}",6.365,11.000,23.833,0.311164 +"{0,2,4,5,6,8}",6.365,11.167,24.000,0.295608 +"{0,5,6,8,9,11}",6.365,11.333,24.333,0.311534 +"{0,1,4,8,9,10}",6.366,10.500,21.333,0.335582 +"{0,2,3,5,8,9}",6.366,10.667,23.167,0.343757 +"{0,1,2,4,8,10}",6.366,11.000,23.833,0.313386 +"{0,2,7,9,10,11}",6.372,10.333,19.833,0.345926 +"{0,1,4,7,8,11}",6.379,10.167,20.833,0.369101 +"{0,3,4,7,8,9}",6.379,10.333,21.000,0.347434 +"{0,4,6,8,9,11}",6.380,10.333,21.333,0.345952 +"{0,2,6,9,10,11}",6.383,10.833,22.333,0.334286 +"{0,1,3,7,8,11}",6.388,10.167,21.000,0.343915 +"{0,1,3,4,7,10}",6.391,10.500,23.500,0.354868 +"{0,3,7,8,9,10}",6.391,11.000,22.000,0.339286 +"{0,1,2,5,6,9}",6.393,10.500,21.167,0.347434 +"{0,3,4,5,7,11}",6.394,10.167,21.000,0.346138 +"{0,4,5,7,10,11}",6.397,10.833,23.333,0.358201 +"{0,1,3,4,6,10}",6.403,10.667,23.500,0.336349 +"{0,1,2,5,6,8}",6.403,10.833,23.000,0.325423 +"{0,2,3,4,6,9}",6.409,10.500,23.500,0.331905 +"{0,1,2,4,5,10}",6.410,10.833,21.833,0.310397 +"{0,2,4,9,10,11}",6.416,10.667,21.333,0.344656 +"{0,1,3,4,5,8}",6.417,10.333,19.500,0.332407 +"{0,1,4,6,8,9}",6.419,9.833,20.167,0.347249 +"{0,2,3,6,7,10}",6.419,9.833,20.167,0.360582 +"{0,1,2,4,6,8}",6.419,10.667,23.000,0.317831 +"{0,1,4,5,8,10}",6.420,9.667,20.000,0.358360 +"{0,2,4,5,8,9}",6.425,10.000,20.333,0.353915 +"{0,1,3,4,6,8}",6.428,10.333,21.833,0.333175 +"{0,1,3,4,8,11}",6.432,10.167,19.500,0.345185 +"{0,3,5,6,9,11}",6.434,11.333,26.000,0.324153 +"{0,3,4,8,9,10}",6.435,11.167,24.000,0.338757 +"{0,3,5,6,8,11}",6.444,10.833,23.333,0.320979 +"{0,3,6,7,8,9}",6.444,11.167,24.167,0.318757 +"{0,1,6,7,9,10}",6.447,11.333,24.333,0.332090 +"{0,1,3,4,7,11}",6.448,10.333,21.167,0.339471 +"{0,1,4,7,10,11}",6.451,10.833,23.833,0.349312 +"{0,2,3,8,9,11}",6.451,11.333,24.333,0.324868 +"{0,6,7,8,10,11}",6.459,11.500,22.500,0.302619 +"{0,1,5,7,9,10}",6.464,10.333,21.333,0.355397 +"{0,1,3,6,10,11}",6.472,11.000,22.000,0.336508 +"{0,2,5,6,7,11}",6.478,10.833,23.333,0.351534 +"{0,2,5,7,8,9}",6.479,10.500,21.000,0.359841 +"{0,2,3,5,10,11}",6.479,11.167,22.667,0.325397 +"{0,1,3,6,8,11}",6.498,10.667,22.167,0.341508 +"{0,3,4,5,7,10}",6.504,10.667,22.167,0.362063 +"{0,3,8,9,10,11}",6.504,11.833,23.333,0.300397 +"{0,1,2,6,8,9}",6.516,10.833,23.000,0.338757 +"{0,1,4,5,6,10}",6.516,11.000,23.167,0.334312 +"{0,3,7,9,10,11}",6.520,11.000,22.000,0.336508 +"{0,4,6,9,10,11}",6.522,11.167,24.000,0.337460 +"{0,1,2,4,5,9}",6.523,10.333,19.667,0.341296 +"{0,2,8,9,10,11}",6.523,12.000,23.667,0.290767 +"{0,1,4,6,7,9}",6.532,10.500,23.000,0.343757 +"{0,4,5,6,8,9}",6.532,10.833,21.667,0.317804 +"{0,2,3,5,6,11}",6.532,11.333,24.333,0.311534 +"{0,5,7,8,9,10}",6.533,10.833,20.333,0.322037 +"{0,1,3,4,9,10}",6.533,11.333,24.333,0.332090 +"{0,4,5,9,10,11}",6.538,11.333,23.333,0.342090 +"{0,1,3,4,8,10}",6.543,10.333,21.333,0.357619 +"{0,3,4,5,8,11}",6.545,10.500,21.167,0.335767 +"{0,1,7,8,9,11}",6.545,11.333,22.333,0.315952 +"{0,5,6,7,9,11}",6.547,11.000,23.500,0.333016 +"{0,2,4,5,7,8}",6.548,10.667,21.667,0.333175 +"{0,4,7,8,9,10}",6.548,11.167,22.667,0.321508 +"{0,3,5,9,10,11}",6.548,11.667,24.500,0.330794 +"{0,1,3,6,7,8}",6.551,11.000,23.500,0.343201 +"{0,1,3,4,6,11}",6.557,11.000,22.500,0.323175 +"{0,3,5,8,10,11}",6.558,10.667,21.167,0.341508 +"{0,1,2,4,7,11}",6.564,10.667,21.667,0.349841 +"{0,3,4,9,10,11}",6.564,11.333,23.833,0.335423 +"{0,2,3,5,6,8}",6.570,11.000,23.833,0.305238 +"{0,3,5,7,10,11}",6.574,10.833,21.833,0.363730 +"{0,1,2,4,7,10}",6.577,10.667,23.667,0.329683 +"{0,2,7,8,9,10}",6.577,11.167,21.833,0.325212 +"{0,4,5,6,9,11}",6.591,11.000,23.000,0.351534 +"{0,1,2,4,7,9}",6.592,10.500,22.000,0.359841 +"{0,2,3,9,10,11}",6.592,11.333,22.333,0.320397 +"{0,1,4,9,10,11}",6.592,11.667,23.167,0.320397 +"{0,4,5,6,8,11}",6.601,11.333,24.167,0.329312 +"{0,1,3,9,10,11}",6.602,12.000,23.667,0.295212 +"{0,2,5,6,10,11}",6.604,11.333,24.167,0.331534 +"{0,1,3,4,6,7}",6.611,11.333,24.333,0.316534 +"{0,1,3,8,10,11}",6.612,10.500,20.000,0.334815 +"{0,1,3,7,10,11}",6.627,11.000,22.500,0.340952 +"{0,1,4,5,6,9}",6.629,10.667,21.333,0.336323 +"{0,1,2,5,8,9}",6.630,10.500,21.167,0.351878 +"{0,2,3,8,9,10}",6.630,11.333,23.833,0.331349 +"{0,1,2,8,9,10}",6.630,11.500,22.500,0.314286 +"{0,2,3,4,10,11}",6.633,11.333,22.333,0.315952 +"{0,2,4,5,10,11}",6.636,11.333,23.833,0.330794 +"{0,1,4,5,6,8}",6.639,10.500,21.333,0.331138 +"{0,3,5,6,7,10}",6.639,10.500,21.000,0.362063 +"{0,1,4,6,9,11}",6.645,10.333,20.833,0.357063 +"{0,4,5,6,7,9}",6.645,11.000,22.000,0.330397 +"{0,6,7,9,10,11}",6.645,11.167,22.667,0.313730 +"{0,1,2,4,8,9}",6.646,10.333,21.167,0.344471 +"{0,1,2,4,5,7}",6.646,11.000,22.500,0.321508 +"{0,3,4,5,9,10}",6.646,11.167,23.667,0.352090 +"{0,5,8,9,10,11}",6.646,11.833,23.333,0.295952 +"{0,2,4,7,8,9}",6.661,10.833,21.833,0.364286 +"{0,5,7,9,10,11}",6.661,11.167,22.333,0.331323 +"{0,4,8,9,10,11}",6.661,11.833,23.333,0.311508 +"{0,3,5,6,7,8}",6.664,10.833,21.333,0.314841 +"{0,6,7,8,9,11}",6.670,11.333,22.333,0.302619 +"{0,2,3,7,8,9}",6.671,11.167,23.167,0.352090 +"{0,5,7,8,10,11}",6.671,11.167,22.167,0.323175 +"{0,5,6,9,10,11}",6.673,11.833,24.833,0.319868 +"{0,1,3,6,7,11}",6.680,11.333,24.167,0.333757 +"{0,5,6,8,10,11}",6.683,11.167,23.167,0.310794 +"{0,1,4,6,7,10}",6.683,11.333,26.000,0.342487 +"{0,1,5,6,7,9}",6.683,11.333,24.167,0.334312 +"{0,2,3,6,8,9}",6.683,11.500,26.167,0.331376 +"{0,6,7,8,9,10}",6.683,12.167,23.833,0.284656 +"{0,2,4,5,6,11}",6.688,10.833,22.833,0.333016 +"{0,3,4,5,6,9}",6.698,11.333,24.333,0.314312 +"{0,1,2,4,5,8}",6.699,10.667,21.500,0.320026 +"{0,1,2,4,9,11}",6.705,10.333,19.833,0.345926 +"{0,2,3,5,6,10}",6.711,10.667,21.667,0.337619 +"{0,1,3,4,5,11}",6.712,11.333,22.333,0.302619 +"{0,4,5,8,10,11}",6.715,11.333,23.667,0.333757 +"{0,5,6,7,8,11}",6.724,11.667,24.167,0.310979 +"{0,1,6,9,10,11}",6.727,11.833,23.333,0.309286 +"{0,2,3,4,7,9}",6.730,10.333,20.833,0.366508 +"{0,5,6,7,8,10}",6.736,11.167,21.833,0.311878 +"{0,1,5,9,10,11}",6.743,11.833,23.333,0.313730 +"{0,3,5,6,10,11}",6.752,11.333,23.333,0.340423 +"{0,5,6,7,8,9}",6.752,12.000,23.500,0.298730 +"{0,1,5,8,10,11}",6.753,10.500,21.000,0.336508 +"{0,1,7,8,9,10}",6.753,11.500,22.500,0.316508 +"{0,1,3,5,6,7}",6.762,11.000,23.000,0.320238 +"{0,1,6,7,9,11}",6.767,10.833,22.833,0.330794 +"{0,4,6,7,8,9}",6.767,11.167,22.167,0.317063 +"{0,1,4,8,10,11}",6.769,11.000,22.000,0.340952 +"{0,1,5,7,10,11}",6.769,11.167,24.000,0.341905 +"{0,1,2,4,7,8}",6.769,11.333,24.167,0.336534 +"{0,4,5,6,7,11}",6.783,11.333,23.333,0.339868 +"{0,1,6,7,8,10}",6.790,11.333,23.833,0.338016 +"{0,3,5,6,7,11}",6.793,11.333,23.667,0.329312 +"{0,1,5,6,9,11}",6.796,11.333,23.667,0.331534 +"{0,1,2,4,5,6}",6.796,11.500,22.500,0.289841 +"{0,1,4,6,10,11}",6.796,11.667,24.500,0.341905 +"{0,2,6,7,8,9}",6.796,11.833,24.667,0.333571 +"{0,6,8,9,10,11}",6.796,12.167,23.833,0.277434 +"{0,1,5,6,8,11}",6.806,11.167,23.667,0.340423 +"{0,1,6,7,8,9}",6.806,11.667,24.167,0.320423 +"{0,1,4,5,10,11}",6.812,11.500,24.000,0.335423 +"{0,1,4,6,8,11}",6.821,10.833,21.833,0.363730 +"{0,1,3,5,10,11}",6.822,11.000,21.667,0.329101 +"{0,3,4,5,8,10}",6.822,11.000,22.000,0.348730 +"{0,2,5,6,7,10}",6.824,11.000,22.000,0.348730 +"{0,1,5,6,7,10}",6.834,11.167,23.167,0.352090 +"{0,1,4,6,7,11}",6.837,11.000,23.000,0.358201 +"{0,1,3,4,10,11}",6.838,11.333,22.333,0.322619 +"{0,1,2,6,9,11}",6.840,10.500,21.000,0.340952 +"{0,2,3,4,9,11}",6.843,10.833,21.833,0.347619 +"{0,2,5,6,7,8}",6.849,11.667,24.500,0.313571 +"{0,1,2,5,9,11}",6.856,11.000,22.000,0.334286 +"{0,2,3,4,9,10}",6.856,11.000,23.000,0.338016 +"{0,1,2,4,10,11}",6.856,12.000,23.667,0.301878 +"{0,1,5,6,7,8}",6.859,11.500,23.500,0.329312 +"{0,1,2,5,8,11}",6.866,11.167,24.167,0.327090 +"{0,1,4,6,7,8}",6.875,11.333,23.667,0.336534 +"{0,3,4,5,6,8}",6.875,11.333,22.833,0.297063 +"{0,1,3,5,6,11}",6.875,11.500,24.000,0.310794 +"{0,1,2,4,8,11}",6.882,11.000,22.500,0.336508 +"{0,1,2,5,7,11}",6.882,11.500,24.333,0.337460 +"{0,1,7,9,10,11}",6.882,12.000,23.667,0.301878 +"{0,1,7,8,10,11}",6.891,11.333,22.833,0.322619 +"{0,1,2,6,7,9}",6.893,11.167,23.667,0.352090 +"{0,1,2,5,7,10}",6.894,10.500,21.000,0.353175 +"{0,1,6,8,10,11}",6.903,11.333,22.500,0.329101 +"{0,1,2,4,6,11}",6.909,10.833,21.500,0.331323 +"{0,1,2,5,7,9}",6.910,10.833,21.833,0.364286 +"{0,1,6,8,9,11}",6.919,11.167,22.167,0.325397 +"{0,2,3,5,6,7}",6.919,11.167,22.167,0.321508 +"{0,1,6,7,10,11}",6.919,11.833,24.833,0.335423 +"{0,1,2,5,7,8}",6.920,11.167,23.167,0.343201 +"{0,2,3,4,5,9}",6.925,10.833,21.333,0.330397 +"{0,1,2,4,5,11}",6.925,11.333,22.333,0.313730 +"{0,1,2,3,6,9}",6.947,11.333,24.333,0.318757 +"{0,1,5,6,10,11}",6.947,11.667,23.667,0.335423 +"{0,2,3,4,5,7}",6.951,10.833,20.333,0.315370 +"{0,7,8,9,10,11}",6.951,12.333,22.333,0.275926 +"{0,1,2,4,6,7}",6.962,11.333,23.833,0.320238 +"{0,4,5,6,7,10}",6.962,11.833,24.667,0.324683 +"{0,1,2,3,5,9}",6.964,11.167,22.667,0.317063 +"{0,1,2,3,5,8}",6.973,11.167,22.167,0.314841 +"{0,1,2,7,10,11}",6.979,11.833,23.333,0.320397 +"{0,4,5,6,7,8}",6.988,12.167,23.667,0.294286 +"{0,1,2,3,5,7}",6.989,11.000,21.667,0.314101 +"{0,1,2,6,10,11}",6.991,12.000,23.500,0.313730 +"{0,1,2,7,9,11}",6.995,11.167,22.333,0.344656 +"{0,3,4,5,6,11}",7.003,12.000,25.000,0.310979 +"{0,1,2,7,8,11}",7.004,11.833,24.833,0.335423 +"{0,1,2,7,9,10}",7.007,11.167,22.167,0.341508 +"{0,1,2,5,10,11}",7.007,12.000,23.500,0.309286 +"{0,1,2,6,8,11}",7.016,11.333,24.167,0.330794 +"{0,3,4,5,6,10}",7.016,11.667,24.500,0.324683 +"{0,1,2,7,8,10}",7.017,11.167,23.167,0.331349 +"{0,1,2,6,7,11}",7.032,11.500,23.500,0.342090 +"{0,5,6,7,10,11}",7.032,11.833,24.333,0.333201 +"{0,1,8,9,10,11}",7.033,12.333,22.333,0.282593 +"{0,1,2,6,7,10}",7.044,11.500,23.833,0.338757 +"{0,2,3,4,8,9}",7.048,11.500,23.833,0.334312 +"{0,3,4,5,6,7}",7.057,12.000,23.500,0.296508 +"{0,1,2,5,6,11}",7.060,11.667,24.167,0.319868 +"{0,2,3,4,5,11}",7.064,11.500,23.000,0.302619 +"{0,1,2,3,7,11}",7.074,12.000,23.500,0.311508 +"{0,2,4,5,6,7}",7.075,11.500,22.667,0.314101 +"{0,4,5,6,10,11}",7.075,12.167,25.833,0.331931 +"{0,1,2,3,6,11}",7.085,12.000,23.500,0.295952 +"{0,1,4,5,6,7}",7.085,12.000,25.000,0.313757 +"{0,1,2,3,7,10}",7.086,10.833,21.333,0.339286 +"{0,1,3,4,5,10}",7.086,11.167,22.167,0.334841 +"{0,1,2,3,6,10}",7.098,11.333,22.333,0.321508 +"{0,1,2,3,7,9}",7.102,11.500,24.333,0.333571 +"{0,2,3,4,5,8}",7.102,11.500,22.500,0.297063 +"{0,1,2,3,5,11}",7.102,12.167,23.833,0.277434 +"{0,1,6,7,8,11}",7.111,11.833,24.333,0.337646 +"{0,1,2,3,7,8}",7.112,11.667,23.667,0.329312 +"{0,1,2,3,5,10}",7.115,10.833,20.333,0.322037 +"{0,3,4,5,10,11}",7.117,12.000,24.500,0.337646 +"{0,1,2,9,10,11}",7.120,12.333,22.333,0.291481 +"{0,1,2,3,6,8}",7.123,11.833,24.667,0.313571 +"{0,1,2,8,10,11}",7.130,12.167,23.833,0.295212 +"{0,1,3,4,5,6}",7.139,11.667,23.167,0.285397 +"{0,1,2,3,6,7}",7.139,11.833,24.333,0.313757 +"{0,1,2,8,9,11}",7.146,11.500,23.000,0.320397 +"{0,1,5,6,7,11}",7.154,12.000,25.667,0.331931 +"{0,1,2,3,5,6}",7.167,11.667,22.667,0.285397 +"{0,1,2,3,4,7}",7.171,12.000,23.500,0.296508 +"{0,1,2,3,4,6}",7.183,12.167,23.833,0.266878 +"{0,1,4,5,6,11}",7.198,12.000,24.500,0.333201 +"{0,1,2,7,8,9}",7.199,12.000,24.500,0.342646 +"{0,1,2,3,9,11}",7.215,12.167,23.833,0.290767 +"{0,1,2,3,8,11}",7.225,12.000,23.500,0.300397 +"{0,1,2,3,9,10}",7.228,11.667,23.167,0.316508 +"{0,1,2,6,7,8}",7.236,12.167,25.833,0.328042 +"{0,1,2,3,8,10}",7.238,11.500,22.667,0.325212 +"{0,2,3,4,5,10}",7.243,11.667,22.833,0.311878 +"{0,1,2,3,8,9}",7.253,12.167,25.167,0.320423 +"{0,1,2,5,6,7}",7.280,12.000,24.500,0.324868 +"{0,2,3,4,5,6}",7.296,12.333,24.000,0.266878 +"{0,1,2,3,4,9}",7.312,12.000,23.500,0.298730 +"{0,1,2,3,4,8}",7.322,12.167,23.667,0.294286 +"{0,1,2,3,10,11}",7.366,12.500,22.500,0.282593 +"{0,1,2,3,4,11}",7.451,12.333,22.333,0.275926 +"{0,1,2,3,4,10}",7.464,12.333,24.000,0.284656 +"{0,1,2,3,4,5}",7.533,12.500,22.500,0.254259 +"{0,2,4,5,7,9,11}",5.701,9.286,19.000,0.382559 +"{0,2,4,6,7,9,11}",5.830,9.286,19.000,0.382559 +"{0,2,3,5,7,9,10}",5.863,9.571,19.286,0.371844 +"{0,3,4,6,8,10,11}",5.868,10.714,22.429,0.340514 +"{0,2,4,7,8,10,11}",5.874,11.000,23.143,0.338927 +"{0,2,5,6,8,9,10}",5.895,11.143,23.286,0.328212 +"{0,2,4,6,7,8,11}",5.919,10.714,22.429,0.348450 +"{0,1,4,5,7,9,11}",5.933,10.571,22.286,0.351625 +"{0,4,5,7,8,9,11}",5.933,10.571,21.286,0.344728 +"{0,1,3,5,7,8,9}",5.975,10.857,22.571,0.340911 +"{0,1,5,6,8,9,10}",5.987,10.714,21.429,0.338776 +"{0,2,4,5,7,9,10}",5.998,9.857,19.571,0.363908 +"{0,3,4,7,8,10,11}",6.001,10.714,21.143,0.353193 +"{0,1,3,5,7,8,10}",6.023,9.571,19.286,0.371844 +"{0,2,4,6,8,10,11}",6.027,11.286,25.000,0.324660 +"{0,1,3,5,6,8,10}",6.033,9.714,19.429,0.363908 +"{0,3,4,6,7,8,11}",6.046,10.714,21.143,0.342082 +"{0,2,4,6,8,9,10}",6.051,11.429,25.143,0.321882 +"{0,3,4,6,7,8,10}",6.057,11.143,23.286,0.334562 +"{0,1,3,5,8,9,11}",6.071,11.143,23.286,0.326228 +"{0,1,3,5,7,9,11}",6.085,11.286,25.000,0.324660 +"{0,2,4,5,6,9,10}",6.089,11.000,22.714,0.340911 +"{0,2,3,4,7,8,11}",6.098,10.714,21.143,0.348432 +"{0,2,3,4,6,8,11}",6.108,11.000,23.143,0.327816 +"{0,1,4,5,7,8,9}",6.109,10.857,21.286,0.345654 +"{0,2,3,7,8,10,11}",6.109,10.857,21.571,0.338379 +"{0,2,5,7,8,9,11}",6.125,10.857,23.571,0.348450 +"{0,1,2,4,6,9,10}",6.135,11.143,23.286,0.334562 +"{0,2,3,5,7,8,10}",6.158,10.000,19.714,0.352797 +"{0,1,3,4,5,7,9}",6.168,11.143,23.286,0.329800 +"{0,1,3,6,8,9,10}",6.176,11.000,23.714,0.344085 +"{0,2,4,5,7,8,11}",6.184,10.714,23.286,0.350567 +"{0,2,3,4,6,7,11}",6.205,10.571,21.286,0.344728 +"{0,1,4,5,8,9,11}",6.206,10.857,21.286,0.351606 +"{0,2,4,6,7,8,10}",6.216,11.571,25.286,0.315533 +"{0,3,5,7,8,9,11}",6.228,11.143,23.429,0.327816 +"{0,2,4,5,6,8,10}",6.240,11.571,25.286,0.307596 +"{0,3,4,6,7,10,11}",6.251,11.000,23.143,0.353874 +"{0,4,6,7,8,10,11}",6.251,11.571,23.714,0.327419 +"{0,2,3,4,7,8,10}",6.252,11.000,22.714,0.337736 +"{0,1,3,4,5,7,8}",6.260,10.714,21.429,0.335601 +"{0,2,3,6,8,10,11}",6.262,11.143,23.429,0.326228 +"{0,2,3,4,6,8,10}",6.262,11.429,25.143,0.315533 +"{0,1,2,5,6,9,10}",6.265,11.000,21.429,0.347241 +"{0,2,4,6,7,10,11}",6.267,10.571,22.429,0.351625 +"{0,1,2,5,6,8,10}",6.273,11.000,22.714,0.336149 +"{0,3,5,6,8,9,10}",6.273,11.143,23.857,0.334562 +"{0,3,4,5,7,9,11}",6.278,10.571,22.429,0.348450 +"{0,2,5,6,8,9,11}",6.278,11.286,25.429,0.336961 +"{0,2,4,7,8,9,11}",6.281,10.714,22.000,0.368405 +"{0,3,5,6,7,9,10}",6.286,11.000,23.714,0.347260 +"{0,1,4,6,8,9,10}",6.286,11.143,23.429,0.334562 +"{0,1,2,4,6,8,10}",6.286,11.429,25.143,0.321882 +"{0,3,4,6,7,9,11}",6.289,10.429,23.000,0.350567 +"{0,2,4,5,8,9,10}",6.292,11.000,22.857,0.336149 +"{0,2,3,6,7,8,11}",6.297,11.286,23.429,0.336413 +"{0,1,3,4,5,8,9}",6.298,11.000,21.429,0.337717 +"{0,3,4,6,7,9,10}",6.300,11.286,25.429,0.343707 +"{0,2,4,5,8,9,11}",6.305,10.714,23.286,0.356916 +"{0,1,3,4,8,9,11}",6.311,10.714,21.429,0.338379 +"{0,1,4,5,7,8,11}",6.311,11.000,23.143,0.353874 +"{0,1,5,7,8,9,11}",6.311,11.571,23.714,0.329006 +"{0,2,3,6,7,9,11}",6.313,10.714,23.286,0.356916 +"{0,2,3,5,8,9,11}",6.314,11.429,25.571,0.332200 +"{0,1,3,5,7,8,11}",6.320,10.571,22.429,0.340514 +"{0,2,3,6,7,9,10}",6.324,10.714,23.286,0.358900 +"{0,1,3,4,7,9,11}",6.324,10.857,23.143,0.338927 +"{0,2,3,5,7,9,11}",6.327,10.571,23.000,0.356255 +"{0,1,3,6,7,9,10}",6.332,11.429,25.571,0.345295 +"{0,1,3,5,8,9,10}",6.333,10.857,22.143,0.356104 +"{0,2,3,5,7,8,11}",6.335,10.857,23.429,0.337868 +"{0,3,6,7,8,10,11}",6.343,11.143,21.857,0.336791 +"{0,2,3,5,6,9,10}",6.348,10.857,23.429,0.350964 +"{0,2,3,4,8,10,11}",6.349,11.571,23.714,0.324244 +"{0,1,3,5,6,9,10}",6.356,10.857,23.429,0.349376 +"{0,1,3,4,7,8,9}",6.357,11.143,23.286,0.341572 +"{0,2,6,7,8,10,11}",6.359,11.571,23.857,0.321070 +"{0,1,3,4,6,9,10}",6.370,11.429,25.571,0.337358 +"{0,1,2,6,8,9,10}",6.370,11.714,23.857,0.327816 +"{0,1,2,4,5,9,10}",6.376,10.857,21.571,0.338776 +"{0,2,5,8,9,10,11}",6.376,11.714,24.429,0.321070 +"{0,1,3,5,6,8,9}",6.378,11.000,23.571,0.335091 +"{0,4,5,6,8,9,10}",6.383,11.571,23.857,0.315117 +"{0,2,5,7,9,10,11}",6.389,10.857,21.857,0.355178 +"{0,3,4,6,9,10,11}",6.396,11.571,25.714,0.339739 +"{0,2,5,7,8,10,11}",6.397,11.286,24.000,0.332577 +"{0,2,4,7,9,10,11}",6.402,10.429,21.000,0.363114 +"{0,1,4,7,8,9,11}",6.408,10.857,21.571,0.351077 +"{0,3,4,5,8,9,11}",6.408,11.286,23.429,0.336413 +"{0,1,3,4,5,9,11}",6.408,11.571,23.714,0.321070 +"{0,1,3,7,8,9,11}",6.416,11.429,23.714,0.324244 +"{0,3,4,6,8,9,11}",6.418,10.857,23.429,0.337868 +"{0,3,4,6,8,9,10}",6.429,11.429,25.429,0.328893 +"{0,1,3,6,9,10,11}",6.429,11.714,24.429,0.324244 +"{0,2,4,6,8,9,11}",6.434,10.571,23.000,0.356255 +"{0,2,5,6,7,9,11}",6.434,10.857,23.143,0.361149 +"{0,2,3,5,9,10,11}",6.435,11.571,24.000,0.336943 +"{0,2,3,4,6,7,8}",6.440,11.714,23.857,0.313530 +"{0,2,3,5,8,10,11}",6.443,11.286,24.000,0.327816 +"{0,2,5,6,7,9,10}",6.445,11.000,22.286,0.354516 +"{0,2,3,4,7,10,11}",6.446,10.857,21.571,0.351077 +"{0,2,4,5,7,10,11}",6.448,11.000,23.286,0.353212 +"{0,2,3,6,7,8,10}",6.451,11.000,22.857,0.337736 +"{0,3,4,5,7,8,9}",6.454,11.143,21.857,0.334014 +"{0,2,3,4,6,10,11}",6.456,11.286,23.571,0.329006 +"{0,2,3,5,7,10,11}",6.457,11.000,22.286,0.352532 +"{0,2,4,6,7,9,10}",6.458,10.714,23.143,0.350302 +"{0,3,4,7,8,9,11}",6.467,10.571,21.143,0.348432 +"{0,1,4,5,6,9,10}",6.467,11.429,23.571,0.339985 +"{0,1,2,5,8,9,10}",6.468,11.000,21.714,0.341950 +"{0,2,3,5,8,9,10}",6.468,11.143,23.429,0.345673 +"{0,1,4,5,6,8,10}",6.475,10.857,22.714,0.340911 +"{0,2,4,5,6,8,9}",6.480,11.286,23.571,0.329800 +"{0,2,3,5,6,9,11}",6.480,11.571,25.714,0.336961 +"{0,1,4,5,7,9,10}",6.481,10.857,23.429,0.349376 +"{0,1,2,4,8,9,10}",6.481,11.429,23.714,0.327816 +"{0,2,3,5,6,8,11}",6.488,11.571,25.714,0.317914 +"{0,3,6,8,9,10,11}",6.488,11.857,24.571,0.314720 +"{0,1,3,5,7,9,10}",6.489,10.714,23.143,0.356652 +"{0,1,4,5,7,8,10}",6.489,10.714,23.286,0.350964 +"{0,2,3,4,6,9,11}",6.493,10.857,23.571,0.348450 +"{0,2,3,6,7,10,11}",6.502,10.714,21.286,0.351606 +"{0,1,3,4,6,9,11}",6.502,11.000,23.714,0.332577 +"{0,2,3,4,6,7,10}",6.502,11.143,23.429,0.334562 +"{0,3,6,7,9,10,11}",6.502,11.429,24.000,0.335884 +"{0,4,5,7,9,10,11}",6.507,11.143,23.000,0.348054 +"{0,1,3,4,7,8,10}",6.511,10.857,23.429,0.358900 +"{0,3,4,5,7,8,11}",6.513,10.714,21.286,0.342082 +"{0,1,2,5,6,8,9}",6.513,11.286,23.429,0.341572 +"{0,1,3,4,5,7,11}",6.513,11.286,23.571,0.327419 +"{0,2,6,7,9,10,11}",6.518,11.000,22.286,0.345786 +"{0,1,3,4,6,8,10}",6.521,10.714,23.143,0.350302 +"{0,1,2,4,6,8,9}",6.526,10.714,22.571,0.340911 +"{0,2,3,5,6,7,9}",6.526,11.143,23.857,0.344085 +"{0,1,4,5,8,9,10}",6.527,10.714,21.286,0.347241 +"{0,1,2,4,5,8,10}",6.527,11.143,23.429,0.328212 +"{0,1,3,4,6,8,9}",6.534,10.857,23.429,0.335091 +"{0,3,6,7,8,9,10}",6.534,11.857,24.571,0.324641 +"{0,2,3,4,6,7,9}",6.539,11.000,23.714,0.344085 +"{0,2,6,7,8,9,11}",6.539,11.571,24.000,0.338530 +"{0,2,5,6,9,10,11}",6.542,11.571,24.143,0.342234 +"{0,1,3,4,6,7,9}",6.548,11.429,25.571,0.327834 +"{0,3,5,6,8,9,11}",6.548,11.857,26.000,0.317914 +"{0,3,5,7,8,9,10}",6.549,11.143,22.143,0.346051 +"{0,2,5,6,8,10,11}",6.550,11.571,25.571,0.318972 +"{0,2,3,7,9,10,11}",6.553,11.143,22.000,0.348961 +"{0,1,4,7,9,10,11}",6.553,11.571,24.286,0.336943 +"{0,2,4,6,9,10,11}",6.555,11.143,23.571,0.349509 +"{0,1,3,4,7,8,11}",6.559,10.571,21.143,0.353193 +"{0,2,5,7,8,9,10}",6.564,11.000,21.571,0.341289 +"{0,2,3,6,9,10,11}",6.564,11.429,24.000,0.337472 +"{0,2,4,5,9,10,11}",6.569,11.429,23.286,0.349641 +"{0,2,3,7,8,9,11}",6.575,11.571,24.143,0.342234 +"{0,1,3,6,8,10,11}",6.580,11.000,22.000,0.347241 +"{0,2,3,6,8,9,11}",6.585,11.714,25.857,0.332200 +"{0,2,5,6,7,8,11}",6.585,11.857,26.000,0.328628 +"{0,1,3,6,8,9,11}",6.594,11.286,24.000,0.327816 +"{0,1,3,7,8,9,10}",6.595,11.714,24.143,0.342101 +"{0,3,4,5,7,9,10}",6.599,11.143,23.429,0.355197 +"{0,1,4,5,7,10,11}",6.599,11.571,25.714,0.347676 +"{0,1,3,6,7,9,11}",6.607,11.429,25.429,0.326909 +"{0,3,4,5,7,8,10}",6.608,11.143,22.429,0.348167 +"{0,1,4,6,7,9,10}",6.610,11.714,25.857,0.337358 +"{0,1,2,4,5,6,10}",6.610,11.857,24.000,0.315117 +"{0,2,4,5,6,9,11}",6.614,10.857,22.714,0.361149 +"{0,2,4,5,6,8,11}",6.623,11.429,25.429,0.328496 +"{0,2,4,5,7,8,10}",6.624,11.143,23.571,0.331255 +"{0,1,3,6,7,8,10}",6.626,11.143,23.429,0.358371 +"{0,1,3,4,6,10,11}",6.631,11.571,24.000,0.338530 +"{0,1,3,4,8,9,10}",6.632,11.571,24.143,0.342630 +"{0,4,6,7,9,10,11}",6.636,11.286,23.714,0.340117 +"{0,1,2,4,7,10,11}",6.637,11.571,24.286,0.336943 +"{0,2,7,8,9,10,11}",6.637,12.143,23.571,0.314701 +"{0,1,3,6,7,8,9}",6.640,11.857,26.000,0.329025 +"{0,2,3,5,6,8,10}",6.642,11.000,23.429,0.331255 +"{0,1,3,4,5,8,11}",6.643,11.000,21.714,0.336791 +"{0,2,3,5,7,8,9}",6.645,11.143,23.000,0.347260 +"{0,1,3,4,7,9,10}",6.645,11.714,25.857,0.345295 +"{0,3,5,8,9,10,11}",6.645,12.143,25.000,0.322657 +"{0,2,3,4,6,9,10}",6.647,11.429,25.429,0.336829 +"{0,2,6,8,9,10,11}",6.647,12.143,25.000,0.311017 +"{0,1,2,4,7,9,11}",6.650,10.714,21.714,0.363114 +"{0,1,3,4,6,8,11}",6.653,11.000,22.286,0.346183 +"{0,2,3,5,6,8,9}",6.656,11.857,26.000,0.327834 +"{0,4,6,7,8,9,11}",6.658,11.143,22.000,0.336262 +"{0,3,5,7,9,10,11}",6.659,11.571,24.000,0.343159 +"{0,4,5,7,8,10,11}",6.659,11.714,24.286,0.337472 +"{0,3,4,8,9,10,11}",6.659,12.000,24.571,0.329138 +"{0,1,2,4,7,9,10}",6.661,11.143,23.857,0.344085 +"{0,3,6,7,8,9,11}",6.666,11.571,24.143,0.320011 +"{0,3,5,7,8,10,11}",6.667,11.143,22.000,0.346183 +"{0,3,5,6,9,10,11}",6.669,12.143,26.286,0.331803 +"{0,3,4,7,9,10,11}",6.672,11.286,23.429,0.351757 +"{0,2,4,8,9,10,11}",6.675,12.143,25.000,0.326890 +"{0,3,5,6,8,10,11}",6.677,11.286,23.143,0.334165 +"{0,1,3,5,6,7,9}",6.677,11.571,25.571,0.325718 +"{0,1,3,4,6,7,10}",6.677,11.714,25.857,0.343707 +"{0,4,5,6,8,9,11}",6.682,11.714,24.286,0.332710 +"{0,4,5,7,8,9,10}",6.683,11.429,22.714,0.328723 +"{0,2,3,8,9,10,11}",6.683,12.286,25.000,0.314324 +"{0,1,3,4,5,7,10}",6.691,11.143,23.857,0.347260 +"{0,1,3,8,9,10,11}",6.691,12.143,23.571,0.313114 +"{0,4,5,6,7,9,11}",6.695,11.286,23.143,0.344879 +"{0,1,2,4,5,7,11}",6.696,11.429,23.857,0.340117 +"{0,3,5,6,7,9,11}",6.704,11.714,25.714,0.328496 +"{0,1,4,7,8,10,11}",6.705,11.429,24.000,0.346995 +"{0,3,4,7,8,9,10}",6.705,11.714,24.286,0.341043 +"{0,1,3,7,9,10,11}",6.705,12.143,25.000,0.322128 +"{0,1,2,4,5,6,9}",6.706,11.143,21.857,0.334014 +"{0,4,5,6,7,9,10}",6.706,11.714,24.143,0.329403 +"{0,1,4,6,9,10,11}",6.706,12.000,24.857,0.336943 +"{0,1,2,4,5,7,10}",6.707,11.143,23.857,0.334562 +"{0,1,3,7,8,10,11}",6.713,11.143,22.429,0.345786 +"{0,1,4,5,6,8,9}",6.715,11.000,21.571,0.337717 +"{0,1,2,4,5,6,8}",6.715,11.571,23.857,0.313530 +"{0,1,5,7,8,9,10}",6.716,11.286,22.143,0.338246 +"{0,1,2,4,5,7,9}",6.721,11.000,22.286,0.351342 +"{0,2,4,7,8,9,10}",6.721,11.571,24.000,0.335620 +"{0,2,4,5,8,10,11}",6.721,11.857,25.857,0.326909 +"{0,1,4,5,9,10,11}",6.721,12.000,24.571,0.335488 +"{0,3,5,6,7,8,10}",6.723,11.000,21.571,0.339701 +"{0,1,4,7,8,9,10}",6.729,11.571,24.143,0.334694 +"{0,2,3,7,8,9,10}",6.729,11.571,23.429,0.343689 +"{0,1,3,5,9,10,11}",6.729,12.143,25.000,0.322128 +"{0,1,3,5,8,10,11}",6.737,10.714,21.286,0.347241 +"{0,1,3,6,7,10,11}",6.737,11.714,24.286,0.346995 +"{0,3,5,6,7,8,9}",6.737,12.000,24.714,0.313530 +"{0,1,5,6,7,9,10}",6.739,11.714,24.286,0.339456 +"{0,2,3,6,8,9,10}",6.739,11.857,25.857,0.332067 +"{0,1,4,6,7,9,11}",6.741,10.857,22.714,0.353212 +"{0,1,3,4,9,10,11}",6.742,12.143,24.857,0.322260 +"{0,3,4,6,7,8,9}",6.750,11.571,24.143,0.321995 +"{0,1,3,4,8,10,11}",6.751,11.143,22.000,0.345786 +"{0,1,3,5,7,10,11}",6.751,11.286,23.714,0.346334 +"{0,2,3,5,6,10,11}",6.752,11.714,24.286,0.331122 +"{0,3,4,5,6,9,10}",6.752,12.000,26.143,0.333787 +"{0,5,6,8,9,10,11}",6.752,12.286,25.000,0.301625 +"{0,4,7,8,9,10,11}",6.756,12.000,23.286,0.321580 +"{0,1,2,4,9,10,11}",6.758,12.000,23.429,0.324225 +"{0,1,3,4,7,10,11}",6.764,11.429,24.000,0.346995 +"{0,3,7,8,9,10,11}",6.764,12.143,23.429,0.316818 +"{0,4,6,8,9,10,11}",6.766,12.143,25.000,0.317366 +"{0,1,2,4,5,8,9}",6.767,10.857,21.429,0.345654 +"{0,2,3,4,7,9,11}",6.769,10.714,21.571,0.368405 +"{0,1,3,5,6,9,11}",6.774,11.857,25.857,0.318972 +"{0,2,4,5,6,7,11}",6.779,11.286,23.143,0.344879 +"{0,2,3,4,7,9,10}",6.780,11.000,22.857,0.358371 +"{0,2,4,5,7,8,9}",6.780,11.143,22.000,0.351342 +"{0,4,5,8,9,10,11}",6.780,12.143,24.714,0.325964 +"{0,1,3,5,6,8,11}",6.783,11.286,23.571,0.334165 +"{0,2,3,5,6,7,11}",6.787,11.714,24.286,0.332710 +"{0,3,4,5,6,7,9}",6.787,11.857,24.571,0.318292 +"{0,5,6,7,8,9,11}",6.787,12.286,25.000,0.306387 +"{0,5,6,7,8,9,10}",6.798,12.286,23.714,0.302400 +"{0,1,2,4,7,8,11}",6.802,11.571,24.143,0.351757 +"{0,1,2,4,6,9,11}",6.803,10.571,21.143,0.355178 +"{0,2,4,5,6,7,9}",6.803,11.143,22.143,0.347638 +"{0,1,3,5,6,7,10}",6.807,11.143,23.000,0.355197 +"{0,1,3,4,6,7,11}",6.809,11.571,24.143,0.337472 +"{0,1,6,7,9,10,11}",6.812,12.143,24.857,0.319085 +"{0,4,6,7,8,9,10}",6.812,12.286,25.143,0.306652 +"{0,1,2,4,7,8,10}",6.813,11.571,25.571,0.332067 +"{0,1,5,8,9,10,11}",6.813,12.000,23.286,0.318405 +"{0,2,3,4,5,7,11}",6.815,11.143,22.429,0.336262 +"{0,1,2,4,5,9,11}",6.817,11.143,22.000,0.345786 +"{0,1,4,8,9,10,11}",6.826,12.000,23.286,0.326342 +"{0,1,5,7,9,10,11}",6.826,12.000,24.857,0.326890 +"{0,1,3,5,6,7,8}",6.828,11.429,23.286,0.330990 +"{0,1,5,7,8,10,11}",6.834,11.429,23.857,0.338530 +"{0,1,5,6,9,10,11}",6.836,12.143,24.714,0.327551 +"{0,2,6,7,8,9,10}",6.836,12.429,25.286,0.317763 +"{0,2,3,4,5,7,9}",6.839,10.857,21.429,0.347638 +"{0,1,3,4,6,7,8}",6.842,11.714,24.286,0.331519 +"{0,1,5,6,8,10,11}",6.844,11.429,23.286,0.338530 +"{0,1,6,7,8,9,10}",6.844,12.429,25.143,0.316308 +"{0,4,5,6,7,8,11}",6.847,12.143,24.714,0.324376 +"{0,1,2,4,6,7,9}",6.849,11.143,23.429,0.347260 +"{0,2,5,6,7,10,11}",6.849,11.714,24.286,0.343821 +"{0,3,5,6,7,8,11}",6.855,11.714,23.857,0.321599 +"{0,1,4,6,8,10,11}",6.858,11.571,24.000,0.346334 +"{0,1,5,6,8,9,11}",6.858,11.857,24.429,0.331122 +"{0,3,4,5,7,10,11}",6.861,11.714,24.286,0.353345 +"{0,1,4,6,8,9,11}",6.871,11.000,21.857,0.352532 +"{0,1,4,6,7,10,11}",6.871,12.000,26.143,0.347676 +"{0,1,4,5,8,10,11}",6.872,11.429,23.571,0.346995 +"{0,1,2,4,5,7,8}",6.872,11.714,24.286,0.331519 +"{0,3,4,5,8,9,10}",6.872,11.857,24.429,0.341043 +"{0,1,2,6,9,10,11}",6.874,12.000,23.286,0.324754 +"{0,2,5,6,7,8,10}",6.882,11.714,24.143,0.321334 +"{0,3,4,5,6,9,11}",6.884,12.143,26.286,0.328628 +"{0,5,7,8,9,10,11}",6.885,12.286,23.714,0.305178 +"{0,1,2,5,9,10,11}",6.888,12.000,23.286,0.324754 +"{0,1,5,6,7,8,10}",6.890,11.571,23.429,0.340514 +"{0,3,4,5,6,8,11}",6.893,11.857,24.429,0.321599 +"{0,2,5,6,7,8,9}",6.895,12.286,25.143,0.329403 +"{0,1,2,5,8,10,11}",6.896,11.857,24.571,0.324244 +"{0,2,3,4,8,9,11}",6.898,11.714,24.286,0.342234 +"{0,1,3,6,7,8,11}",6.901,11.714,24.286,0.340646 +"{0,3,4,5,6,8,10}",6.904,11.571,24.000,0.326096 +"{0,1,3,5,6,10,11}",6.904,11.714,23.571,0.338530 +"{0,1,4,6,7,8,10}",6.904,11.857,25.857,0.336829 +"{0,1,5,6,7,8,9}",6.904,12.286,24.857,0.323186 +"{0,1,2,6,7,9,11}",6.909,11.286,23.143,0.349641 +"{0,1,2,5,8,9,11}",6.909,11.571,24.143,0.337472 +"{0,2,4,6,7,8,9}",6.909,11.714,24.143,0.337207 +"{0,1,2,5,7,10,11}",6.909,12.000,24.857,0.336943 +"{0,1,2,4,8,10,11}",6.909,12.143,25.000,0.322128 +"{0,5,6,7,9,10,11}",6.909,12.143,24.857,0.322260 +"{0,1,4,6,7,8,9}",6.917,11.571,23.714,0.333107 +"{0,3,4,5,6,8,9}",6.917,11.857,24.429,0.314059 +"{0,2,3,6,7,8,9}",6.917,12.286,26.429,0.333787 +"{0,1,2,6,7,9,10}",6.920,11.857,24.429,0.342630 +"{0,1,2,4,8,9,11}",6.923,11.143,22.429,0.348961 +"{0,1,2,5,7,9,11}",6.923,11.429,23.857,0.349509 +"{0,1,2,5,7,8,11}",6.931,12.000,26.143,0.339739 +"{0,1,2,5,6,9,11}",6.933,11.429,23.571,0.342234 +"{0,1,2,4,6,10,11}",6.933,12.143,25.000,0.326890 +"{0,1,2,5,7,9,10}",6.934,11.000,21.857,0.356104 +"{0,2,3,4,5,9,11}",6.936,11.571,24.000,0.338530 +"{0,2,3,5,6,7,10}",6.941,11.143,22.000,0.348167 +"{0,1,2,5,6,8,11}",6.941,11.857,26.000,0.331803 +"{0,1,6,8,9,10,11}",6.941,12.286,23.714,0.309940 +"{0,1,2,5,7,8,10}",6.942,11.143,23.000,0.345673 +"{0,2,3,4,5,8,11}",6.944,11.714,24.286,0.320011 +"{0,4,5,6,9,10,11}",6.946,12.429,26.143,0.334580 +"{0,2,3,4,5,9,10}",6.947,11.571,23.429,0.340514 +"{0,1,2,4,5,10,11}",6.947,12.286,25.000,0.319085 +"{0,1,2,4,6,8,11}",6.955,11.286,23.714,0.343159 +"{0,1,2,3,6,9,11}",6.955,11.857,24.571,0.321070 +"{0,1,3,4,5,9,10}",6.955,11.857,24.429,0.339456 +"{0,1,4,5,6,7,9}",6.955,11.857,24.429,0.329932 +"{0,2,3,4,6,8,9}",6.955,11.857,25.857,0.325718 +"{0,4,5,6,8,10,11}",6.955,12.143,25.714,0.323337 +"{0,2,3,5,6,7,8}",6.963,11.857,24.286,0.315117 +"{0,1,3,4,5,8,10}",6.964,11.000,21.857,0.354516 +"{0,1,2,3,6,9,10}",6.965,11.714,24.286,0.334694 +"{0,1,2,4,6,7,11}",6.968,11.429,23.286,0.348054 +"{0,2,3,4,5,7,10}",6.969,11.286,22.286,0.339701 +"{0,1,2,4,5,8,11}",6.969,11.571,24.143,0.335884 +"{0,1,2,4,7,8,9}",6.969,11.714,24.286,0.348980 +"{0,1,2,3,5,9,11}",6.969,12.143,25.000,0.311017 +"{0,3,5,6,7,10,11}",6.976,11.857,24.000,0.345408 +"{0,1,2,3,5,8,11}",6.977,11.857,24.571,0.314720 +"{0,1,2,4,6,7,10}",6.979,11.857,25.857,0.328893 +"{0,1,2,3,5,9,10}",6.980,11.286,22.571,0.338246 +"{0,3,4,5,9,10,11}",6.982,12.429,26.143,0.337755 +"{0,1,2,3,5,8,10}",6.988,11.143,22.143,0.341289 +"{0,2,3,4,5,7,8}",6.990,11.571,22.429,0.319199 +"{0,3,4,5,8,10,11}",6.990,11.857,24.000,0.340646 +"{0,1,2,3,5,7,11}",6.990,12.143,25.000,0.317366 +"{0,2,3,4,5,6,9}",6.992,12.000,24.714,0.318292 +"{0,1,2,3,5,7,10}",7.001,10.857,21.429,0.346051 +"{0,1,2,3,5,8,9}",7.001,11.857,24.429,0.333107 +"{0,1,3,4,5,6,9}",7.001,11.857,24.429,0.314059 +"{0,1,2,3,6,7,9}",7.001,12.000,26.143,0.333787 +"{0,4,5,6,7,8,10}",7.001,12.429,25.286,0.308239 +"{0,1,2,7,9,10,11}",7.006,12.143,23.571,0.324225 +"{0,1,3,4,5,6,8}",7.009,11.429,22.714,0.319199 +"{0,1,5,6,7,9,11}",7.014,12.000,25.571,0.329686 +"{0,4,5,6,7,8,9}",7.014,12.286,23.571,0.307691 +"{0,6,7,8,9,10,11}",7.014,12.714,24.143,0.287320 +"{0,1,2,3,5,7,9}",7.015,11.429,23.857,0.337207 +"{0,1,2,7,8,10,11}",7.015,12.286,25.000,0.322260 +"{0,2,3,4,9,10,11}",7.020,12.000,24.286,0.334958 +"{0,1,2,3,5,7,8}",7.023,11.571,23.429,0.330990 +"{0,1,2,3,5,6,9}",7.025,11.714,24.286,0.321995 +"{0,1,2,6,8,10,11}",7.025,12.143,25.000,0.322128 +"{0,2,4,5,6,10,11}",7.030,12.143,25.714,0.329686 +"{0,1,2,3,5,6,8}",7.033,11.857,24.286,0.315117 +"{0,1,4,6,7,8,11}",7.036,11.714,23.857,0.353345 +"{0,1,2,6,8,9,11}",7.038,11.571,24.000,0.336943 +"{0,1,2,3,4,6,9}",7.038,11.857,24.571,0.313530 +"{0,1,2,6,7,10,11}",7.038,12.143,24.714,0.335488 +"{0,3,4,5,6,7,11}",7.049,12.143,24.714,0.324376 +"{0,1,4,5,6,9,11}",7.051,11.857,24.000,0.343821 +"{0,2,3,4,8,9,10}",7.052,12.143,25.714,0.330083 +"{0,1,4,5,6,8,11}",7.060,11.857,24.429,0.345408 +"{0,3,4,5,6,7,10}",7.060,12.286,25.143,0.330990 +"{0,5,6,7,8,10,11}",7.060,12.286,24.571,0.311149 +"{0,1,2,5,6,10,11}",7.062,12.286,24.857,0.327551 +"{0,1,2,3,7,10,11}",7.074,12.143,23.429,0.326342 +"{0,1,7,8,9,10,11}",7.074,12.714,24.143,0.300019 +"{0,2,4,5,6,7,10}",7.076,11.857,24.286,0.326096 +"{0,1,3,5,6,7,11}",7.082,12.143,25.714,0.323337 +"{0,3,4,5,6,7,8}",7.082,12.286,23.571,0.301342 +"{0,1,2,3,6,10,11}",7.084,12.143,23.429,0.318405 +"{0,1,4,5,6,7,10}",7.084,12.286,26.429,0.333787 +"{0,2,3,4,7,8,9}",7.087,11.857,24.000,0.345805 +"{0,1,2,3,7,9,11}",7.087,12.143,25.000,0.326890 +"{0,1,2,3,7,8,11}",7.096,12.143,24.714,0.329138 +"{0,2,4,5,6,7,8}",7.097,12.429,25.286,0.305064 +"{0,1,2,3,7,9,10}",7.098,11.714,24.143,0.342101 +"{0,1,2,5,7,8,9}",7.098,11.857,24.000,0.348980 +"{0,2,3,4,5,8,10}",7.098,11.857,24.286,0.321334 +"{0,1,2,3,5,10,11}",7.098,12.286,23.714,0.309940 +"{0,1,2,3,6,8,11}",7.106,12.143,25.000,0.322657 +"{0,1,4,5,6,7,8}",7.106,12.286,24.857,0.321599 +"{0,1,6,7,8,10,11}",7.106,12.286,25.000,0.328609 +"{0,1,2,3,7,8,10}",7.107,11.571,23.429,0.343689 +"{0,2,3,4,5,6,11}",7.111,12.286,25.000,0.306387 +"{0,4,5,6,7,10,11}",7.111,12.429,26.143,0.336168 +"{0,2,3,4,5,8,9}",7.112,11.857,24.000,0.329932 +"{0,1,2,3,6,8,10}",7.117,11.714,24.143,0.335620 +"{0,1,6,7,8,9,11}",7.119,12.143,24.429,0.320673 +"{0,1,2,3,6,7,11}",7.119,12.286,24.857,0.325964 +"{0,1,2,5,6,7,9}",7.122,11.857,24.429,0.345805 +"{0,1,2,3,6,7,10}",7.130,11.714,23.857,0.341043 +"{0,1,3,4,5,6,10}",7.130,11.857,24.286,0.329403 +"{0,1,2,3,6,8,9}",7.130,12.286,26.429,0.329025 +"{0,1,2,8,9,10,11}",7.136,12.714,24.143,0.301606 +"{0,1,2,4,6,7,8}",7.143,12.143,25.714,0.325321 +"{0,1,2,3,5,6,11}",7.143,12.429,25.143,0.301625 +"{0,1,5,6,7,10,11}",7.143,12.429,26.143,0.337755 +"{0,2,3,4,5,6,8}",7.143,12.429,25.286,0.289191 +"{0,1,2,3,4,7,11}",7.147,12.000,23.286,0.321580 +"{0,1,2,3,5,6,10}",7.154,11.571,22.429,0.328723 +"{0,1,2,3,4,6,11}",7.157,12.143,23.571,0.305178 +"{0,3,4,5,6,10,11}",7.157,12.571,26.286,0.331406 +"{0,1,2,3,4,7,10}",7.158,12.000,24.714,0.324641 +"{0,1,3,4,5,6,7}",7.165,12.429,25.143,0.302022 +"{0,1,5,6,7,8,11}",7.165,12.429,26.143,0.331406 +"{0,1,2,3,4,6,10}",7.168,12.286,25.143,0.306652 +"{0,1,2,3,4,7,9}",7.171,12.143,25.000,0.329403 +"{0,1,2,7,8,9,11}",7.171,12.286,25.000,0.334958 +"{0,1,2,3,4,7,8}",7.179,12.286,24.857,0.321599 +"{0,1,4,5,6,10,11}",7.181,12.571,26.286,0.337755 +"{0,1,2,7,8,9,10}",7.182,12.286,24.571,0.327419 +"{0,1,2,3,4,6,8}",7.189,12.286,25.143,0.305064 +"{0,1,2,3,9,10,11}",7.195,12.714,24.143,0.301606 +"{0,1,2,3,4,6,7}",7.203,12.429,25.143,0.302022 +"{0,1,2,6,7,8,11}",7.203,12.429,26.143,0.337755 +"{0,1,2,3,8,10,11}",7.204,12.286,23.714,0.313114 +"{0,2,3,4,5,10,11}",7.208,12.429,25.143,0.320673 +"{0,1,2,6,7,8,10}",7.214,12.286,25.857,0.330083 +"{0,1,4,5,6,7,11}",7.216,12.429,26.143,0.336168 +"{0,1,3,4,5,10,11}",7.217,12.143,24.429,0.328609 +"{0,1,2,3,4,5,7}",7.217,12.286,23.714,0.297638 +"{0,1,2,3,8,9,11}",7.217,12.429,25.143,0.314324 +"{0,1,2,6,7,8,9}",7.227,12.571,26.286,0.334977 +"{0,1,2,3,8,9,10}",7.228,12.429,25.143,0.327419 +"{0,1,2,5,6,7,11}",7.240,12.429,26.143,0.334580 +"{0,1,2,5,6,7,10}",7.251,12.000,24.143,0.341043 +"{0,1,3,4,5,6,11}",7.262,12.429,25.143,0.311149 +"{0,1,2,3,7,8,9}",7.263,12.571,26.286,0.334977 +"{0,2,3,4,5,6,10}",7.265,12.429,25.286,0.308239 +"{0,1,2,3,4,9,11}",7.268,12.143,23.571,0.314701 +"{0,1,2,5,6,7,8}",7.273,12.571,26.286,0.325454 +"{0,1,2,3,4,8,11}",7.276,12.143,23.429,0.316818 +"{0,1,2,4,5,6,11}",7.278,12.143,24.429,0.322260 +"{0,1,2,3,4,9,10}",7.279,12.429,25.143,0.316308 +"{0,1,2,3,4,8,10}",7.287,12.286,25.143,0.317763 +"{0,1,2,3,6,7,8}",7.295,12.571,26.286,0.325454 +"{0,1,2,3,4,8,9}",7.300,12.286,24.857,0.323186 +"{0,2,3,4,5,6,7}",7.300,12.429,23.857,0.297638 +"{0,1,2,4,5,6,7}",7.324,12.429,25.143,0.309958 +"{0,1,2,3,5,6,7}",7.332,12.286,24.571,0.309958 +"{0,1,2,3,4,5,9}",7.338,12.286,23.571,0.307691 +"{0,1,2,3,4,5,8}",7.346,12.286,23.571,0.301342 +"{0,1,2,3,4,10,11}",7.397,12.714,24.143,0.300019 +"{0,1,2,3,4,5,11}",7.457,12.714,24.143,0.287320 +"{0,1,2,3,4,5,10}",7.468,12.429,23.857,0.302400 +"{0,1,2,3,4,5,6}",7.513,12.857,24.286,0.271844 +"{0,3,4,6,7,8,10,11}",6.300,11.500,23.250,0.340505 +"{0,2,4,6,7,8,10,11}",6.314,11.750,24.875,0.332185 +"{0,1,4,5,7,8,9,11}",6.345,11.500,23.250,0.345266 +"{0,2,3,4,7,8,10,11}",6.345,11.500,23.250,0.341695 +"{0,1,3,5,7,8,9,11}",6.353,11.750,24.875,0.328614 +"{0,2,3,4,6,8,10,11}",6.354,11.625,24.750,0.328614 +"{0,2,4,5,6,8,9,10}",6.356,11.875,25.000,0.324150 +"{0,2,3,4,6,7,8,11}",6.385,11.500,23.250,0.336933 +"{0,1,2,5,6,8,9,10}",6.385,11.625,23.375,0.338421 +"{0,1,3,4,5,7,9,11}",6.397,11.625,24.750,0.332185 +"{0,1,2,4,6,8,9,10}",6.397,11.750,24.875,0.331293 +"{0,1,3,4,5,7,8,9}",6.426,11.625,23.375,0.334850 +"{0,2,4,5,7,8,9,11}",6.432,11.250,23.625,0.355882 +"{0,1,3,5,6,8,9,10}",6.466,11.375,23.750,0.346655 +"{0,1,3,4,5,8,9,11}",6.511,11.625,23.375,0.335743 +"{0,2,3,6,7,8,10,11}",6.519,11.625,23.500,0.335743 +"{0,2,3,4,6,7,8,10}",6.519,11.875,25.000,0.325340 +"{0,3,4,5,7,8,9,11}",6.522,11.500,23.375,0.336933 +"{0,2,3,5,8,9,10,11}",6.534,12.250,26.000,0.328713 +"{0,2,4,5,7,9,10,11}",6.538,11.250,23.000,0.357866 +"{0,2,3,5,7,9,10,11}",6.546,11.500,23.625,0.350723 +"{0,2,3,5,7,8,10,11}",6.553,11.500,23.875,0.340405 +"{0,1,2,4,5,6,9,10}",6.555,11.750,23.500,0.336040 +"{0,1,4,5,6,8,9,10}",6.562,11.625,23.500,0.336040 +"{0,1,2,4,5,6,8,10}",6.562,11.875,25.000,0.324150 +"{0,1,3,4,7,8,9,11}",6.563,11.375,23.250,0.341695 +"{0,2,3,4,6,7,10,11}",6.564,11.375,23.250,0.345266 +"{0,2,3,5,7,8,9,11}",6.565,11.750,25.375,0.339725 +"{0,1,3,4,6,9,10,11}",6.574,12.125,25.875,0.334666 +"{0,2,3,5,6,8,9,11}",6.574,12.250,27.250,0.328727 +"{0,2,3,5,6,7,9,10}",6.595,11.375,23.750,0.351417 +"{0,2,3,4,6,7,9,11}",6.597,11.125,23.500,0.355882 +"{0,1,3,4,6,8,9,10}",6.602,11.750,25.375,0.340023 +"{0,1,3,4,5,7,8,11}",6.603,11.375,23.250,0.340505 +"{0,1,2,4,5,8,9,10}",6.607,11.500,23.375,0.338421 +"{0,1,3,4,6,7,9,10}",6.614,12.250,27.250,0.338549 +"{0,2,5,7,8,9,10,11}",6.619,12.125,24.625,0.328997 +"{0,3,4,6,8,9,10,11}",6.625,12.125,25.750,0.329904 +"{0,2,5,6,8,9,10,11}",6.628,12.250,25.875,0.322761 +"{0,3,4,6,7,9,10,11}",6.637,11.875,25.500,0.343396 +"{0,2,4,6,7,9,10,11}",6.651,11.250,23.375,0.354294 +"{0,1,3,6,8,9,10,11}",6.654,12.125,24.625,0.327806 +"{0,2,3,6,7,9,10,11}",6.658,11.625,23.875,0.347647 +"{0,2,5,6,7,8,9,11}",6.658,12.250,26.000,0.333475 +"{0,1,3,6,7,9,10,11}",6.666,12.250,25.875,0.331094 +"{0,2,4,6,7,8,9,11}",6.670,11.500,23.625,0.351913 +"{0,1,4,5,7,9,10,11}",6.671,12.000,25.625,0.340618 +"{0,2,4,5,7,8,10,11}",6.671,12.000,25.625,0.337344 +"{0,2,3,5,6,9,10,11}",6.680,12.125,25.750,0.338634 +"{0,2,3,5,6,8,10,11}",6.687,11.875,25.500,0.327820 +"{0,2,4,5,6,8,9,11}",6.691,11.750,25.375,0.343296 +"{0,1,3,6,7,8,9,10}",6.694,12.375,26.125,0.336154 +"{0,2,3,5,7,8,9,10}",6.699,11.500,23.250,0.347449 +"{0,2,4,5,6,7,9,11}",6.703,11.250,23.000,0.359056 +"{0,1,2,4,7,9,10,11}",6.704,11.875,24.375,0.342092 +"{0,1,3,4,6,8,10,11}",6.706,11.500,23.625,0.348342 +"{0,1,3,5,7,8,9,10}",6.707,11.625,23.750,0.348639 +"{0,2,3,5,6,8,9,10}",6.708,12.000,25.625,0.335261 +"{0,2,3,5,6,7,9,11}",6.710,11.875,25.500,0.343296 +"{0,1,3,4,6,8,9,11}",6.718,11.375,23.750,0.340405 +"{0,1,3,5,6,7,9,10}",6.727,11.875,25.500,0.343594 +"{0,1,3,4,6,7,9,11}",6.729,11.625,25.250,0.337344 +"{0,2,3,4,6,7,9,10}",6.731,11.750,25.375,0.344785 +"{0,3,5,6,8,9,10,11}",6.739,12.500,26.250,0.321570 +"{0,1,2,4,5,7,10,11}",6.744,12.125,25.875,0.338237 +"{0,1,2,4,5,7,9,11}",6.756,11.375,23.500,0.354294 +"{0,2,4,7,8,9,10,11}",6.756,12.250,24.750,0.337330 +"{0,1,3,4,5,7,8,10}",6.759,11.375,23.750,0.351417 +"{0,2,3,7,8,9,10,11}",6.763,12.375,24.750,0.330286 +"{0,2,5,6,7,9,10,11}",6.764,11.875,24.250,0.344870 +"{0,2,4,6,8,9,10,11}",6.764,12.250,26.000,0.330598 +"{0,1,2,4,5,7,9,10}",6.765,11.375,23.750,0.346655 +"{0,1,2,4,5,6,8,9}",6.772,11.625,23.500,0.334850 +"{0,2,3,6,8,9,10,11}",6.772,12.375,26.000,0.325142 +"{0,2,4,5,8,9,10,11}",6.777,12.375,26.000,0.334666 +"{0,3,5,6,7,8,9,10}",6.779,12.250,24.750,0.325723 +"{0,3,4,6,7,8,9,11}",6.781,11.625,23.875,0.334552 +"{0,1,4,6,7,9,10,11}",6.783,12.250,26.000,0.338237 +"{0,1,3,5,8,9,10,11}",6.791,12.250,24.750,0.330187 +"{0,3,4,6,7,8,9,10}",6.791,12.375,26.000,0.325439 +"{0,2,3,6,7,8,9,11}",6.802,12.250,25.875,0.336253 +"{0,1,4,5,7,8,10,11}",6.803,12.000,25.625,0.345777 +"{0,1,3,4,8,9,10,11}",6.803,12.250,24.625,0.332667 +"{0,1,3,5,7,9,10,11}",6.803,12.250,26.000,0.332979 +"{0,1,3,5,7,8,10,11}",6.810,11.375,23.500,0.348342 +"{0,1,3,5,6,9,10,11}",6.812,12.375,26.000,0.331094 +"{0,1,3,4,7,9,10,11}",6.815,12.250,25.875,0.337046 +"{0,2,3,4,6,9,10,11}",6.816,12.000,25.625,0.340618 +"{0,2,4,5,7,8,9,10}",6.817,11.750,23.875,0.337925 +"{0,1,3,5,6,8,10,11}",6.819,11.500,23.250,0.345961 +"{0,1,3,4,7,8,10,11}",6.822,11.625,23.875,0.351219 +"{0,1,4,5,7,8,9,10}",6.824,11.750,24.000,0.339612 +"{0,3,4,5,6,7,9,10}",6.824,12.375,26.125,0.332582 +"{0,1,3,5,6,8,9,11}",6.831,12.125,25.750,0.327820 +"{0,2,3,4,6,8,9,11}",6.835,11.875,25.500,0.339725 +"{0,2,4,5,6,7,8,11}",6.835,12.250,25.875,0.331094 +"{0,1,2,4,7,8,10,11}",6.836,12.250,25.875,0.337046 +"{0,2,4,5,6,7,9,10}",6.837,11.750,23.875,0.341497 +"{0,1,2,4,6,9,10,11}",6.837,12.125,24.625,0.338520 +"{0,1,3,4,6,7,10,11}",6.843,12.125,25.750,0.345777 +"{0,1,3,4,7,8,9,10}",6.843,12.250,25.875,0.342503 +"{0,2,3,5,6,7,8,11}",6.843,12.250,25.875,0.325539 +"{0,3,6,7,8,9,10,11}",6.843,12.500,24.875,0.317191 +"{0,4,5,7,8,9,10,11}",6.848,12.375,24.750,0.326715 +"{0,1,2,4,5,9,10,11}",6.850,12.250,24.625,0.335048 +"{0,3,5,7,8,9,10,11}",6.855,12.375,24.875,0.326616 +"{0,2,6,7,8,9,10,11}",6.856,12.625,25.125,0.317985 +"{0,2,3,4,5,7,9,11}",6.859,11.375,23.500,0.351913 +"{0,1,3,5,6,7,8,10}",6.860,11.500,23.250,0.351020 +"{0,2,3,4,5,7,8,11}",6.867,11.750,24.000,0.334552 +"{0,3,4,7,8,9,10,11}",6.867,12.250,24.500,0.335445 +"{0,1,2,4,6,7,9,11}",6.868,11.250,23.000,0.357866 +"{0,2,3,4,5,7,9,10}",6.869,11.500,23.250,0.351020 +"{0,1,3,4,6,7,8,10}",6.871,12.000,25.625,0.344785 +"{0,1,3,5,6,7,8,9}",6.871,12.375,26.000,0.325439 +"{0,3,5,6,7,9,10,11}",6.875,12.375,26.000,0.333475 +"{0,1,3,4,5,7,9,10}",6.876,12.000,25.625,0.343594 +"{0,1,2,4,6,7,9,10}",6.878,12.000,25.625,0.340023 +"{0,1,3,4,6,7,8,9}",6.883,12.125,25.750,0.329408 +"{0,1,5,6,8,9,10,11}",6.885,12.375,24.750,0.324334 +"{0,4,5,6,7,8,9,11}",6.887,12.375,24.750,0.324334 +"{0,1,2,4,5,7,8,11}",6.888,12.125,25.750,0.343396 +"{0,1,4,7,8,9,10,11}",6.888,12.375,24.750,0.330286 +"{0,3,5,6,7,8,9,11}",6.894,12.500,26.125,0.315618 +"{0,1,3,7,8,9,10,11}",6.895,12.625,25.125,0.322747 +"{0,1,2,4,5,7,8,10}",6.897,11.875,25.500,0.335261 +"{0,1,4,6,8,9,10,11}",6.897,12.250,24.750,0.333759 +"{0,2,5,6,7,8,10,11}",6.897,12.375,26.000,0.323951 +"{0,3,4,5,7,9,10,11}",6.900,12.125,25.375,0.345380 +"{0,3,4,5,7,8,10,11}",6.907,12.000,24.250,0.344076 +"{0,1,4,5,8,9,10,11}",6.909,12.250,24.500,0.336635 +"{0,2,5,6,7,8,9,10}",6.918,12.500,25.000,0.324532 +"{0,2,4,5,6,9,10,11}",6.922,12.250,25.500,0.344189 +"{0,1,3,6,7,8,10,11}",6.923,12.000,24.375,0.344870 +"{0,1,5,6,7,8,9,10}",6.925,12.500,24.875,0.325822 +"{0,3,4,5,6,8,9,11}",6.927,12.375,26.000,0.325539 +"{0,3,4,5,7,8,9,10}",6.928,12.125,24.500,0.338024 +"{0,1,2,6,7,9,10,11}",6.930,12.250,24.625,0.335048 +"{0,1,2,5,8,9,10,11}",6.930,12.375,24.750,0.325524 +"{0,2,4,5,6,8,10,11}",6.930,12.375,27.125,0.323172 +"{0,2,4,6,7,8,9,10}",6.930,12.500,26.250,0.323753 +"{0,2,3,4,7,9,10,11}",6.932,11.750,23.750,0.353203 +"{0,1,3,6,7,8,9,11}",6.935,12.250,25.875,0.326332 +"{0,3,4,5,6,8,9,10}",6.937,12.375,26.000,0.325439 +"{0,1,4,6,7,8,9,10}",6.937,12.500,26.125,0.326630 +"{0,2,3,6,7,8,9,10}",6.937,12.500,26.125,0.333773 +"{0,3,4,5,6,7,9,11}",6.939,12.250,25.875,0.331094 +"{0,1,2,5,7,9,10,11}",6.942,12.125,24.625,0.338520 +"{0,1,2,4,8,9,10,11}",6.942,12.500,25.000,0.327509 +"{0,2,3,5,6,7,10,11}",6.949,12.000,24.250,0.342885 +"{0,1,2,5,7,8,10,11}",6.949,12.375,26.125,0.334666 +"{0,2,3,4,7,8,9,11}",6.951,11.875,24.125,0.351219 +"{0,1,2,5,6,9,10,11}",6.951,12.250,24.500,0.335445 +"{0,1,2,5,6,8,10,11}",6.958,12.250,25.875,0.331094 +"{0,4,6,7,8,9,10,11}",6.960,12.625,25.125,0.316794 +"{0,1,3,5,6,7,9,11}",6.968,12.375,27.125,0.323172 +"{0,1,2,5,6,8,9,11}",6.970,12.125,25.750,0.338634 +"{0,1,2,4,6,8,10,11}",6.970,12.250,26.000,0.332979 +"{0,1,2,3,6,9,10,11}",6.970,12.375,24.750,0.325524 +"{0,1,4,5,6,7,9,10}",6.970,12.375,26.000,0.334170 +"{0,2,3,4,6,8,9,10}",6.970,12.375,27.125,0.327041 +"{0,1,2,4,7,8,9,11}",6.973,11.875,24.250,0.353203 +"{0,2,3,4,5,7,10,11}",6.973,12.000,24.375,0.342489 +"{0,2,3,4,5,8,9,11}",6.973,12.250,25.875,0.336253 +"{0,2,3,5,6,7,8,10}",6.977,11.750,23.875,0.333163 +"{0,1,3,4,5,7,10,11}",6.980,12.125,25.750,0.341808 +"{0,1,2,4,6,8,9,11}",6.981,11.375,23.500,0.350723 +"{0,1,2,4,6,7,10,11}",6.981,12.250,25.875,0.340618 +"{0,4,5,6,8,9,10,11}",6.981,12.750,26.375,0.320082 +"{0,1,2,4,7,8,9,10}",6.982,12.250,25.875,0.334963 +"{0,1,2,4,5,8,10,11}",6.982,12.375,26.000,0.331094 +"{0,1,2,3,5,9,10,11}",6.982,12.500,25.000,0.321556 +"{0,1,3,4,6,7,8,11}",6.987,11.875,24.125,0.344076 +"{0,2,3,5,6,7,8,9}",6.989,12.625,26.375,0.326630 +"{0,1,3,4,5,8,9,10}",6.990,12.000,24.250,0.345564 +"{0,1,2,3,5,8,10,11}",6.990,12.125,24.625,0.327806 +"{0,2,3,4,5,6,9,11}",6.993,12.375,26.125,0.333475 +"{0,4,5,6,7,9,10,11}",6.993,12.500,25.875,0.333177 +"{0,1,2,4,5,8,9,11}",6.994,11.750,24.000,0.347647 +"{0,1,2,3,6,7,9,11}",7.000,12.250,25.875,0.334666 +"{0,2,3,4,5,6,8,11}",7.000,12.375,26.000,0.315618 +"{0,2,3,4,5,7,8,10}",7.001,11.875,24.000,0.333163 +"{0,1,2,3,5,7,10,11}",7.001,12.250,24.750,0.333759 +"{0,1,2,3,5,8,9,11}",7.001,12.375,26.000,0.325142 +"{0,1,5,7,8,9,10,11}",7.001,12.625,25.125,0.320366 +"{0,2,3,4,5,6,9,10}",7.003,12.375,26.000,0.331392 +"{0,3,5,6,7,8,10,11}",7.008,12.125,24.125,0.331774 +"{0,1,2,3,6,7,9,10}",7.010,12.250,25.875,0.342503 +"{0,1,3,4,5,6,9,10}",7.010,12.375,26.000,0.334170 +"{0,1,2,3,5,8,9,10}",7.011,12.000,24.375,0.341596 +"{0,1,2,3,5,7,9,11}",7.013,12.250,26.000,0.330598 +"{0,3,4,5,8,9,10,11}",7.013,12.750,26.375,0.332384 +"{0,1,3,4,5,6,8,10}",7.017,11.625,23.750,0.341497 +"{0,1,2,3,5,7,8,11}",7.020,12.250,25.875,0.329904 +"{0,1,2,3,5,7,9,10}",7.022,11.500,23.625,0.348639 +"{0,1,2,3,5,6,9,11}",7.022,12.375,26.000,0.322761 +"{0,1,5,6,7,9,10,11}",7.022,12.625,26.250,0.327225 +"{0,4,5,6,7,8,9,10}",7.022,12.750,25.250,0.308759 +"{0,1,3,4,5,6,8,9}",7.029,12.000,24.250,0.326516 +"{0,1,2,3,5,6,8,11}",7.029,12.375,26.125,0.321570 +"{0,1,2,3,5,7,8,10}",7.030,11.500,23.250,0.347449 +"{0,1,2,3,5,6,9,10}",7.031,11.875,24.125,0.339612 +"{0,1,2,3,4,6,9,11}",7.033,12.000,24.500,0.328997 +"{0,2,3,4,5,6,7,9}",7.033,12.250,24.750,0.326913 +"{0,3,4,5,6,9,10,11}",7.033,13.000,28.000,0.332894 +"{0,1,2,4,5,7,8,9}",7.034,12.000,24.250,0.344374 +"{0,1,2,3,5,6,8,10}",7.039,11.750,23.875,0.337925 +"{0,1,4,6,7,8,10,11}",7.041,12.375,26.000,0.341808 +"{0,3,4,5,6,8,10,11}",7.041,12.375,25.625,0.331094 +"{0,1,3,4,5,6,7,9}",7.041,12.500,26.125,0.317106 +"{0,1,2,3,4,6,9,10}",7.043,12.375,26.000,0.326630 +"{0,1,2,6,8,9,10,11}",7.043,12.625,25.125,0.321556 +"{0,2,3,4,8,9,10,11}",7.046,12.750,26.375,0.329606 +"{0,1,2,3,5,6,8,9}",7.050,12.375,26.000,0.329408 +"{0,1,4,6,7,8,9,11}",7.052,11.875,23.875,0.342489 +"{0,1,4,5,6,9,10,11}",7.055,12.750,26.375,0.337146 +"{0,1,4,5,6,8,10,11}",7.062,12.250,25.500,0.341808 +"{0,2,4,5,6,7,10,11}",7.066,12.250,25.500,0.339427 +"{0,3,4,5,6,7,8,11}",7.071,12.375,24.625,0.322350 +"{0,1,4,5,6,8,9,11}",7.074,12.125,24.375,0.342885 +"{0,1,2,3,4,6,7,9}",7.074,12.375,26.125,0.326630 +"{0,5,6,7,8,9,10,11}",7.074,13.000,25.500,0.301020 +"{0,2,3,4,5,9,10,11}",7.079,12.625,26.000,0.336749 +"{0,1,3,5,6,7,10,11}",7.081,12.375,25.625,0.341808 +"{0,3,4,5,6,7,8,10}",7.081,12.500,25.000,0.323342 +"{0,1,4,5,6,7,9,11}",7.085,12.250,25.500,0.339427 +"{0,1,2,5,7,8,9,11}",7.086,12.375,26.000,0.340618 +"{0,2,3,4,7,8,9,10}",7.086,12.375,25.625,0.339725 +"{0,2,3,4,5,8,10,11}",7.086,12.500,26.125,0.326332 +"{0,1,2,3,7,9,10,11}",7.086,12.625,25.125,0.327509 +"{0,1,3,4,5,9,10,11}",7.086,12.750,26.375,0.329606 +"{0,1,3,4,5,8,10,11}",7.093,11.875,23.875,0.344870 +"{0,1,2,3,7,8,10,11}",7.093,12.375,24.750,0.332667 +"{0,3,4,5,6,7,8,9}",7.093,12.625,25.000,0.310346 +"{0,2,4,5,6,7,8,10}",7.095,12.625,26.375,0.313039 +"{0,1,2,5,7,8,9,10}",7.096,12.000,24.000,0.341596 +"{0,1,3,5,6,7,8,11}",7.100,12.250,25.500,0.331094 +"{0,1,2,3,6,8,10,11}",7.102,12.250,24.750,0.330187 +"{0,1,4,5,6,7,8,10}",7.102,12.500,26.125,0.331392 +"{0,1,2,5,6,7,9,11}",7.106,12.125,25.375,0.344189 +"{0,2,4,5,6,7,8,9}",7.106,12.500,25.000,0.325723 +"{0,2,3,4,5,8,9,10}",7.107,12.375,25.625,0.333773 +"{0,1,2,3,6,7,10,11}",7.114,12.375,24.625,0.336635 +"{0,1,2,3,6,8,9,11}",7.114,12.500,26.250,0.328713 +"{0,1,4,5,6,7,8,9}",7.114,12.500,24.750,0.325028 +"{0,1,6,7,8,9,10,11}",7.114,13.000,25.500,0.310544 +"{0,1,2,5,6,7,9,10}",7.116,12.125,24.375,0.345564 +"{0,1,2,3,6,8,9,10}",7.123,12.500,26.125,0.334963 +"{0,1,2,4,6,7,8,11}",7.125,12.250,25.500,0.345380 +"{0,1,3,4,5,6,9,11}",7.125,12.500,26.125,0.323951 +"{0,2,3,4,6,7,8,9}",7.125,12.500,26.125,0.329011 +"{0,4,5,6,7,8,10,11}",7.125,12.875,26.500,0.321273 +"{0,1,3,4,5,6,8,11}",7.133,12.125,24.500,0.331774 +"{0,1,2,3,5,6,10,11}",7.135,12.500,24.875,0.324334 +"{0,1,2,4,6,7,8,10}",7.135,12.500,27.250,0.327041 +"{0,2,3,4,5,6,8,10}",7.135,12.500,26.250,0.313039 +"{0,2,3,4,5,6,7,11}",7.137,12.375,24.750,0.324334 +"{0,2,3,4,5,7,8,9}",7.138,12.125,24.125,0.335643 +"{0,1,2,3,4,7,10,11}",7.138,12.375,24.750,0.330286 +"{0,1,2,4,5,6,9,11}",7.139,11.875,23.875,0.344870 +"{0,1,2,4,5,6,8,11}",7.147,12.250,25.875,0.333475 +"{0,1,2,4,6,7,8,9}",7.147,12.250,25.500,0.337344 +"{0,1,2,3,4,6,10,11}",7.147,12.500,25.000,0.320366 +"{0,2,3,4,5,6,8,9}",7.147,12.625,26.250,0.317106 +"{0,1,2,3,4,7,9,11}",7.150,12.125,24.625,0.337330 +"{0,1,3,4,5,6,7,10}",7.154,12.625,26.375,0.332582 +"{0,1,5,6,7,8,10,11}",7.154,12.625,26.000,0.331987 +"{0,1,2,3,4,7,8,11}",7.157,12.250,24.500,0.335445 +"{0,1,2,3,4,7,9,10}",7.159,12.500,26.250,0.336154 +"{0,1,2,7,8,9,10,11}",7.159,13.000,25.500,0.317687 +"{0,1,2,3,4,6,8,11}",7.166,12.250,24.750,0.326616 +"{0,1,2,3,4,7,8,10}",7.166,12.375,26.000,0.333773 +"{0,1,2,3,5,7,8,9}",7.166,12.375,25.625,0.337344 +"{0,1,5,6,7,8,9,11}",7.166,12.875,26.500,0.322463 +"{0,1,3,4,5,6,7,8}",7.173,12.500,24.875,0.317489 +"{0,1,2,3,4,6,8,10}",7.175,12.375,26.125,0.323753 +"{0,1,2,3,4,6,7,11}",7.177,12.375,24.750,0.326715 +"{0,3,4,5,6,7,10,11}",7.177,12.750,26.375,0.337146 +"{0,1,2,4,5,6,7,9}",7.180,12.125,24.500,0.335643 +"{0,1,2,3,4,6,8,9}",7.187,12.375,26.000,0.325439 +"{0,1,2,3,5,6,7,9}",7.187,12.375,26.000,0.329011 +"{0,1,2,3,4,6,7,10}",7.187,12.500,26.125,0.325439 +"{0,1,2,6,7,8,10,11}",7.187,12.750,26.375,0.329606 +"{0,1,2,3,4,5,7,11}",7.190,12.500,25.000,0.316794 +"{0,1,2,3,4,5,7,10}",7.199,12.250,24.750,0.325723 +"{0,1,2,6,7,8,9,11}",7.199,12.625,26.000,0.336749 +"{0,1,2,3,8,9,10,11}",7.199,13.000,25.500,0.314116 +"{0,1,4,5,6,7,10,11}",7.199,13.000,28.000,0.338846 +"{0,1,2,6,7,8,9,10}",7.208,13.000,26.625,0.327523 +"{0,1,2,3,4,5,7,9}",7.211,12.375,24.875,0.325723 +"{0,1,2,3,4,5,7,8}",7.218,12.500,24.875,0.317489 +"{0,1,4,5,6,7,8,11}",7.218,12.750,26.375,0.337146 +"{0,1,2,5,6,7,10,11}",7.220,12.750,26.375,0.337146 +"{0,1,2,3,7,8,9,11}",7.230,12.750,26.375,0.329606 +"{0,2,3,4,5,6,10,11}",7.231,12.750,26.375,0.322463 +"{0,1,3,4,5,6,10,11}",7.239,12.750,26.125,0.331987 +"{0,1,2,5,6,7,8,11}",7.239,13.000,28.000,0.332894 +"{0,1,2,3,7,8,9,10}",7.240,12.750,26.125,0.337046 +"{0,1,2,3,4,9,10,11}",7.244,12.875,25.375,0.317687 +"{0,1,2,5,6,7,8,10}",7.248,12.500,25.750,0.333773 +"{0,1,2,3,4,8,10,11}",7.251,12.625,25.125,0.322747 +"{0,1,2,4,5,6,10,11}",7.253,12.875,26.500,0.327225 +"{0,1,2,3,6,7,8,11}",7.258,12.750,26.375,0.332384 +"{0,1,2,5,6,7,8,9}",7.260,12.875,26.500,0.333872 +"{0,1,2,3,4,8,9,11}",7.263,12.375,24.750,0.330286 +"{0,1,2,3,6,7,8,10}",7.267,12.375,25.625,0.339725 +"{0,1,3,4,5,6,7,11}",7.269,12.750,26.375,0.321273 +"{0,2,3,4,5,6,7,10}",7.272,12.500,25.000,0.323342 +"{0,1,2,3,4,8,9,10}",7.272,12.750,26.375,0.327523 +"{0,1,2,3,6,7,8,9}",7.279,13.125,28.125,0.330811 +"{0,1,2,4,5,6,7,11}",7.283,12.625,26.000,0.333177 +"{0,1,2,3,5,6,7,11}",7.291,12.875,26.500,0.320082 +"{0,2,3,4,5,6,7,8}",7.291,12.875,25.375,0.301616 +"{0,1,2,4,5,6,7,10}",7.293,12.625,26.250,0.325439 +"{0,1,2,3,4,5,9,11}",7.296,12.625,25.125,0.317985 +"{0,1,2,3,5,6,7,10}",7.300,12.125,24.125,0.338024 +"{0,1,2,3,4,5,8,11}",7.303,12.500,24.875,0.317191 +"{0,1,2,3,4,7,8,9}",7.303,12.750,26.375,0.333872 +"{0,1,2,3,4,5,9,10}",7.305,12.500,24.875,0.325822 +"{0,1,2,4,5,6,7,8}",7.312,12.875,26.500,0.316808 +"{0,1,2,3,4,5,8,10}",7.313,12.375,24.875,0.324532 +"{0,1,2,3,5,6,7,8}",7.319,12.750,26.125,0.320380 +"{0,1,2,3,4,5,8,9}",7.324,12.500,24.750,0.325028 +"{0,1,2,3,4,6,7,8}",7.331,12.875,26.500,0.316808 +"{0,1,2,3,4,5,6,9}",7.345,12.625,25.000,0.310346 +"{0,1,2,3,4,5,6,8}",7.352,12.750,25.250,0.301616 +"{0,1,2,3,4,5,10,11}",7.409,13.000,25.500,0.310544 +"{0,1,2,3,4,5,6,11}",7.449,13.000,25.500,0.301020 +"{0,1,2,3,4,5,6,10}",7.458,12.875,25.375,0.308759 +"{0,1,2,3,4,5,6,7}",7.489,13.125,25.625,0.295366 +"{0,2,3,4,6,7,8,10,11}",6.573,12.000,24.667,0.335681 +"{0,1,3,4,5,7,8,9,11}",6.601,12.000,24.667,0.335681 +"{0,1,2,4,5,6,8,9,10}",6.629,12.111,24.778,0.333135 +"{0,2,3,5,7,8,9,10,11}",6.733,12.444,25.778,0.335373 +"{0,2,3,5,6,8,9,10,11}",6.741,12.667,27.111,0.328671 +"{0,1,3,4,6,8,9,10,11}",6.757,12.333,25.667,0.337224 +"{0,1,3,4,6,7,9,10,11}",6.768,12.556,27.000,0.337930 +"{0,1,2,4,5,7,9,10,11}",6.791,12.222,25.556,0.343706 +"{0,2,4,5,7,8,9,10,11}",6.837,12.556,25.889,0.338150 +"{0,1,3,5,6,8,9,10,11}",6.858,12.556,25.889,0.332595 +"{0,2,3,5,6,7,9,10,11}",6.862,12.333,25.556,0.343089 +"{0,2,3,4,6,7,9,10,11}",6.872,12.111,25.333,0.347718 +"{0,2,4,5,6,7,8,9,11}",6.872,12.444,25.778,0.339076 +"{0,2,3,5,6,7,8,9,11}",6.879,12.778,27.222,0.329597 +"{0,1,2,4,6,7,9,10,11}",6.891,12.333,25.667,0.343706 +"{0,1,3,5,6,7,8,9,10}",6.894,12.556,25.889,0.336530 +"{0,1,3,4,6,7,8,9,10}",6.904,12.778,27.222,0.334458 +"{0,1,2,4,5,7,8,10,11}",6.909,12.667,27.111,0.337930 +"{0,3,4,6,7,8,9,10,11}",6.925,12.667,25.889,0.328968 +"{0,2,5,6,7,8,9,10,11}",6.927,12.889,26.222,0.323104 +"{0,2,4,6,7,8,9,10,11}",6.937,12.667,26.000,0.332286 +"{0,2,3,6,7,8,9,10,11}",6.944,12.778,26.000,0.329894 +"{0,1,3,6,7,8,9,10,11}",6.950,12.889,26.222,0.326808 +"{0,1,4,5,7,8,9,10,11}",6.955,12.667,25.889,0.334524 +"{0,2,4,5,6,8,9,10,11}",6.956,12.778,27.111,0.330214 +"{0,1,3,5,7,8,9,10,11}",6.962,12.667,26.000,0.331360 +"{0,2,4,5,6,7,9,10,11}",6.967,12.333,25.333,0.345558 +"{0,1,3,4,7,8,9,10,11}",6.972,12.667,25.889,0.336376 +"{0,2,3,4,6,8,9,10,11}",6.973,12.667,27.000,0.332066 +"{0,2,3,5,6,7,8,10,11}",6.980,12.444,25.667,0.331978 +"{0,1,3,5,6,7,9,10,11}",6.980,12.778,27.111,0.332066 +"{0,1,2,5,6,8,9,10,11}",6.982,12.667,25.889,0.330820 +"{0,1,2,4,7,8,9,10,11}",6.984,12.778,26.111,0.335141 +"{0,1,2,4,6,8,9,10,11}",6.992,12.556,25.889,0.335064 +"{0,2,3,4,5,7,9,10,11}",6.995,12.333,25.333,0.347410 +"{0,1,3,4,6,7,8,10,11}",6.997,12.333,25.556,0.345866 +"{0,2,3,5,6,7,8,9,10}",6.999,12.667,26.000,0.332826 +"{0,2,3,4,6,7,8,9,11}",7.000,12.333,25.556,0.341237 +"{0,2,3,4,5,7,8,10,11}",7.001,12.444,25.667,0.336607 +"{0,1,3,4,5,7,9,10,11}",7.001,12.667,27.000,0.336695 +"{0,1,2,4,5,8,9,10,11}",7.003,12.667,25.889,0.335450 +"{0,1,3,4,6,7,8,9,11}",7.007,12.222,25.444,0.336607 +"{0,1,3,4,5,7,8,10,11}",7.008,12.222,25.444,0.345866 +"{0,1,2,3,6,7,9,10,11}",7.009,12.667,25.889,0.335450 +"{0,1,2,3,5,8,9,10,11}",7.010,12.778,26.111,0.327734 +"{0,2,3,4,5,7,8,9,11}",7.011,12.333,25.556,0.341237 +"{0,2,3,4,5,6,8,9,11}",7.019,12.778,27.222,0.329597 +"{0,1,2,3,5,7,9,10,11}",7.020,12.556,25.889,0.335064 +"{0,3,5,6,7,8,9,10,11}",7.026,13.000,26.333,0.319400 +"{0,1,2,3,5,7,8,10,11}",7.027,12.444,25.778,0.337224 +"{0,1,3,4,5,7,8,9,10}",7.027,12.444,25.667,0.341468 +"{0,1,2,3,5,6,9,10,11}",7.028,12.667,25.889,0.330820 +"{0,1,2,4,5,7,8,9,11}",7.030,12.333,25.556,0.347718 +"{0,2,3,4,5,6,7,9,11}",7.030,12.444,25.778,0.339076 +"{0,1,2,3,5,6,8,10,11}",7.034,12.444,25.778,0.332595 +"{0,1,3,4,5,6,8,9,10}",7.034,12.444,25.667,0.336839 +"{0,2,3,4,5,6,7,9,10}",7.038,12.556,25.889,0.335604 +"{0,1,2,3,4,6,9,10,11}",7.038,12.667,26.000,0.330511 +"{0,1,2,4,5,7,8,9,10}",7.039,12.333,25.556,0.338690 +"{0,1,2,3,5,6,8,9,11}",7.045,12.778,27.222,0.328671 +"{0,1,3,4,5,6,7,9,10}",7.045,12.889,27.333,0.332606 +"{0,3,4,5,7,8,9,10,11}",7.047,12.778,26.000,0.333598 +"{0,1,2,3,5,6,8,9,10}",7.053,12.444,25.667,0.338690 +"{0,1,4,6,7,8,9,10,11}",7.055,12.889,26.222,0.329586 +"{0,3,4,5,6,8,9,10,11}",7.055,13.111,27.556,0.326587 +"{0,1,2,3,4,6,7,9,11}",7.065,12.333,25.667,0.338150 +"{0,3,4,5,6,7,9,10,11}",7.065,13.000,27.444,0.333995 +"{0,1,2,3,4,6,7,9,10}",7.074,12.778,27.222,0.334458 +"{0,1,4,5,6,8,9,10,11}",7.074,12.778,26.000,0.334524 +"{0,2,3,4,7,8,9,10,11}",7.076,12.778,26.000,0.339153 +"{0,3,4,5,6,7,8,9,11}",7.082,12.778,26.000,0.322487 +"{0,2,4,5,6,7,8,10,11}",7.084,12.889,27.222,0.326510 +"{0,1,4,5,6,7,9,10,11}",7.084,13.000,27.444,0.334921 +"{0,1,2,5,7,8,9,10,11}",7.085,12.889,26.222,0.330511 +"{0,3,4,5,6,7,8,9,10}",7.091,13.000,26.333,0.320558 +"{0,2,3,4,5,8,9,10,11}",7.095,13.111,27.556,0.332143 +"{0,1,3,5,6,7,8,10,11}",7.097,12.444,25.444,0.340928 +"{0,1,3,4,5,8,9,10,11}",7.102,12.778,26.000,0.336376 +"{0,1,2,5,6,7,9,10,11}",7.103,12.667,25.889,0.338228 +"{0,2,4,5,6,7,8,9,10}",7.103,12.889,26.222,0.322332 +"{0,1,3,5,6,7,8,9,11}",7.108,12.889,27.222,0.323732 +"{0,1,2,3,6,8,9,10,11}",7.110,12.889,26.222,0.327734 +"{0,1,4,5,6,7,8,9,10}",7.110,12.889,26.111,0.326422 +"{0,2,3,4,5,6,9,10,11}",7.114,13.000,27.444,0.334921 +"{0,1,2,4,6,7,8,10,11}",7.120,12.778,27.111,0.336695 +"{0,2,3,4,5,6,8,10,11}",7.120,12.778,27.111,0.323732 +"{0,2,3,4,6,7,8,9,10}",7.120,12.889,27.222,0.329519 +"{0,1,3,4,5,6,9,10,11}",7.120,13.111,27.556,0.332143 +"{0,1,3,4,5,6,8,10,11}",7.127,12.444,25.444,0.340928 +"{0,1,2,4,6,7,8,9,11}",7.130,12.333,25.333,0.347410 +"{0,4,5,6,7,8,9,10,11}",7.130,13.222,26.556,0.316391 +"{0,2,3,4,5,7,8,9,10}",7.131,12.556,25.556,0.337456 +"{0,1,2,4,5,6,9,10,11}",7.132,12.778,26.000,0.338228 +"{0,1,3,4,5,6,8,9,11}",7.137,12.556,25.778,0.331978 +"{0,1,2,4,5,6,8,10,11}",7.139,12.778,27.111,0.332066 +"{0,1,2,4,6,7,8,9,10}",7.139,12.889,27.222,0.330445 +"{0,2,3,4,5,6,8,9,10}",7.139,12.889,27.222,0.324890 +"{0,1,2,3,4,7,9,10,11}",7.141,12.778,26.111,0.335141 +"{0,1,3,4,5,6,7,9,11}",7.147,12.778,27.111,0.326510 +"{0,2,3,4,5,6,7,8,11}",7.147,12.778,26.000,0.322487 +"{0,1,2,3,4,7,8,10,11}",7.148,12.667,25.889,0.336376 +"{0,1,2,3,5,7,8,9,11}",7.148,12.778,27.111,0.332066 +"{0,1,2,4,5,6,8,9,11}",7.149,12.333,25.556,0.343089 +"{0,1,2,3,4,6,8,10,11}",7.156,12.556,25.889,0.331360 +"{0,1,5,6,7,8,9,10,11}",7.156,13.222,26.556,0.319169 +"{0,1,2,3,5,7,8,9,10}",7.157,12.444,25.444,0.343011 +"{0,1,2,4,5,6,7,9,11}",7.160,12.333,25.333,0.345558 +"{0,1,3,4,5,6,7,8,10}",7.162,12.667,26.000,0.335604 +"{0,1,2,3,4,6,8,9,11}",7.166,12.444,25.778,0.335373 +"{0,1,2,3,4,6,7,10,11}",7.166,12.667,25.889,0.334524 +"{0,1,2,3,5,6,7,9,11}",7.166,12.778,27.111,0.330214 +"{0,1,2,4,5,6,7,9,10}",7.168,12.556,25.778,0.336839 +"{0,1,3,4,5,6,7,8,9}",7.173,12.889,26.111,0.320866 +"{0,1,2,3,5,6,7,9,10}",7.175,12.444,25.667,0.341468 +"{0,1,2,3,4,6,8,9,10}",7.175,12.778,27.111,0.330445 +"{0,1,2,3,4,5,7,10,11}",7.177,12.778,26.111,0.329586 +"{0,3,4,5,6,7,8,10,11}",7.183,12.889,26.111,0.329894 +"{0,1,2,6,7,8,9,10,11}",7.185,13.222,26.556,0.324724 +"{0,1,2,3,4,5,7,9,11}",7.188,12.556,25.889,0.332286 +"{0,1,2,3,4,5,7,8,11}",7.194,12.667,25.889,0.328968 +"{0,1,2,3,4,5,7,9,10}",7.196,12.556,25.889,0.336530 +"{0,1,4,5,6,7,8,10,11}",7.202,13.111,27.556,0.335847 +"{0,1,2,3,4,5,7,8,10}",7.203,12.556,25.889,0.332826 +"{0,1,4,5,6,7,8,9,11}",7.212,12.889,26.111,0.333598 +"{0,1,2,3,7,8,9,10,11}",7.213,13.222,26.556,0.326576 +"{0,1,2,5,6,7,8,10,11}",7.221,13.111,27.556,0.332143 +"{0,1,2,5,6,7,8,9,11}",7.231,13.111,27.556,0.334921 +"{0,1,2,3,6,7,8,10,11}",7.238,12.778,26.000,0.336376 +"{0,1,2,5,6,7,8,9,10}",7.240,13.000,26.222,0.332903 +"{0,2,3,4,5,6,7,10,11}",7.242,12.778,26.000,0.333598 +"{0,1,2,3,4,8,9,10,11}",7.242,13.111,26.444,0.326576 +"{0,1,2,3,6,7,8,9,11}",7.248,13.111,27.556,0.332143 +"{0,1,3,4,5,6,7,10,11}",7.248,13.111,27.556,0.335847 +"{0,1,2,3,6,7,8,9,10}",7.257,13.222,27.667,0.335152 +"{0,1,2,4,5,6,7,10,11}",7.260,13.111,27.556,0.334921 +"{0,1,3,4,5,6,7,8,11}",7.265,12.778,26.000,0.329894 +"{0,1,2,3,5,6,7,10,11}",7.267,12.889,26.111,0.334524 +"{0,2,3,4,5,6,7,8,10}",7.267,12.889,26.222,0.318629 +"{0,1,2,3,4,7,8,9,11}",7.269,12.667,25.889,0.339153 +"{0,1,2,3,4,5,9,10,11}",7.271,13.111,26.444,0.324724 +"{0,1,2,4,5,6,7,8,11}",7.277,13.111,27.556,0.333995 +"{0,2,3,4,5,6,7,8,9}",7.277,13.111,26.444,0.318706 +"{0,1,2,3,4,5,8,10,11}",7.278,12.889,26.222,0.326808 +"{0,1,2,3,4,7,8,9,10}",7.278,13.111,27.556,0.335152 +"{0,1,2,3,5,6,7,8,11}",7.284,13.111,27.556,0.326587 +"{0,1,2,4,5,6,7,8,10}",7.286,13.000,27.333,0.324890 +"{0,1,2,3,4,5,8,9,11}",7.288,12.778,26.000,0.329894 +"{0,1,2,3,5,6,7,8,10}",7.292,12.556,25.556,0.337456 +"{0,1,2,3,4,6,7,8,11}",7.294,12.778,26.000,0.333598 +"{0,1,2,4,5,6,7,8,9}",7.296,12.889,26.111,0.330126 +"{0,1,2,3,4,5,8,9,10}",7.297,12.778,26.000,0.332903 +"{0,1,2,3,4,6,7,8,10}",7.303,12.889,27.222,0.329519 +"{0,1,2,3,5,6,7,8,9}",7.303,13.222,27.667,0.327745 +"{0,1,2,3,4,5,6,9,11}",7.306,12.889,26.222,0.323104 +"{0,1,2,3,4,5,6,8,11}",7.313,12.889,26.222,0.319400 +"{0,1,2,3,4,6,7,8,9}",7.313,13.111,27.556,0.327745 +"{0,1,2,3,4,5,6,9,10}",7.315,12.889,26.111,0.326422 +"{0,1,2,3,4,5,6,8,10}",7.322,12.778,26.111,0.322332 +"{0,1,2,3,4,5,7,8,9}",7.324,12.889,26.111,0.330126 +"{0,1,2,3,4,5,6,8,9}",7.332,12.889,26.111,0.320866 +"{0,1,2,3,4,5,6,7,9}",7.342,13.000,26.333,0.318706 +"{0,1,2,3,4,5,6,10,11}",7.407,13.222,26.556,0.319169 +"{0,1,2,3,4,5,6,7,11}",7.434,13.222,26.556,0.316391 +"{0,1,2,3,4,5,6,7,10}",7.443,13.111,26.444,0.320558 +"{0,1,2,3,4,5,6,7,8}",7.460,13.333,26.667,0.309215 +"{0,2,3,5,6,7,8,9,10,11}",6.999,13.100,27.100,0.329480 +"{0,1,3,4,6,7,8,9,10,11}",7.014,13.000,27.000,0.334665 +"{0,1,2,4,5,7,8,9,10,11}",7.035,13.000,27.000,0.337628 +"{0,1,2,3,5,6,8,9,10,11}",7.048,13.000,27.000,0.331702 +"{0,1,2,3,4,6,7,9,10,11}",7.067,12.900,26.900,0.337628 +"{0,2,4,5,6,7,8,9,10,11}",7.093,13.200,27.200,0.329233 +"{0,1,3,5,6,7,8,9,10,11}",7.105,13.200,27.200,0.328492 +"{0,2,3,4,6,7,8,9,10,11}",7.108,13.000,26.900,0.334665 +"{0,2,3,4,5,7,8,9,10,11}",7.118,13.100,27.100,0.336887 +"{0,1,3,4,5,7,8,9,10,11}",7.124,13.000,26.900,0.336146 +"{0,1,2,4,6,7,8,9,10,11}",7.125,13.100,27.100,0.335159 +"{0,2,3,4,5,6,8,9,10,11}",7.125,13.300,28.300,0.329056 +"{0,1,3,4,5,6,8,9,10,11}",7.131,13.100,27.100,0.334665 +"{0,2,3,4,5,6,7,9,10,11}",7.134,13.000,27.000,0.339109 +"{0,1,3,4,5,6,7,9,10,11}",7.140,13.300,28.300,0.332760 +"{0,1,2,3,5,7,8,9,10,11}",7.141,13.100,27.100,0.332937 +"{0,1,2,4,5,6,8,9,10,11}",7.142,13.000,26.900,0.335406 +"{0,2,3,4,5,6,7,8,9,11}",7.150,13.100,27.100,0.330220 +"{0,1,2,4,5,6,7,9,10,11}",7.151,13.000,27.000,0.339850 +"{0,1,2,3,4,6,8,9,10,11}",7.157,13.000,27.000,0.332937 +"{0,1,2,3,5,6,7,9,10,11}",7.157,13.000,26.900,0.335406 +"{0,1,3,4,5,6,7,8,9,10}",7.163,13.200,27.200,0.330406 +"{0,1,2,3,4,5,7,9,10,11}",7.176,13.000,27.000,0.335159 +"{0,1,2,3,4,5,7,8,10,11}",7.182,13.000,27.000,0.334665 +"{0,3,4,5,6,7,8,9,10,11}",7.182,13.400,27.400,0.324109 +"{0,1,4,5,6,7,8,9,10,11}",7.199,13.400,27.400,0.328554 +"{0,1,2,5,6,7,8,9,10,11}",7.216,13.400,27.400,0.329295 +"{0,1,2,3,6,7,8,9,10,11}",7.231,13.400,27.400,0.330776 +"{0,2,3,4,5,6,7,8,10,11}",7.240,13.100,27.000,0.327998 +"{0,1,3,4,5,6,7,8,10,11}",7.246,13.100,27.100,0.337628 +"{0,1,2,3,4,7,8,9,10,11}",7.250,13.300,27.300,0.334480 +"{0,1,3,4,5,6,7,8,9,11}",7.255,13.100,27.000,0.327998 +"{0,2,3,4,5,6,7,8,9,10}",7.257,13.300,27.300,0.324974 +"{0,1,2,4,5,6,7,8,10,11}",7.257,13.400,28.400,0.332760 +"{0,1,2,3,5,6,7,8,10,11}",7.263,13.100,27.100,0.334665 +"{0,1,2,4,5,6,7,8,9,11}",7.267,13.100,27.100,0.339109 +"{0,1,2,3,4,5,8,9,10,11}",7.267,13.300,27.300,0.330776 +"{0,1,2,3,4,6,7,8,10,11}",7.272,13.000,26.900,0.336146 +"{0,1,2,3,5,6,7,8,9,11}",7.272,13.400,28.400,0.329056 +"{0,1,2,4,5,6,7,8,9,10}",7.274,13.200,27.100,0.329665 +"{0,1,2,3,5,6,7,8,9,10}",7.280,13.200,27.200,0.335591 +"{0,1,2,3,4,6,7,8,9,11}",7.282,13.000,27.000,0.336887 +"{0,1,2,3,4,5,6,9,10,11}",7.284,13.300,27.300,0.329295 +"{0,1,2,3,4,5,6,8,10,11}",7.289,13.100,27.100,0.328492 +"{0,1,2,3,4,6,7,8,9,10}",7.289,13.400,28.400,0.331464 +"{0,1,2,3,4,5,7,8,9,11}",7.292,13.000,26.900,0.334665 +"{0,1,2,3,4,5,6,8,9,11}",7.299,13.100,27.100,0.329480 +"{0,1,2,3,4,5,7,8,9,10}",7.299,13.100,27.100,0.335591 +"{0,1,2,3,4,5,6,8,9,10}",7.306,13.100,27.000,0.329665 +"{0,1,2,3,4,5,6,7,9,11}",7.308,13.100,27.100,0.329233 +"{0,1,2,3,4,5,6,7,9,10}",7.316,13.200,27.200,0.330406 +"{0,1,2,3,4,5,6,7,10,11}",7.399,13.400,27.400,0.328554 +"{0,1,2,3,4,5,6,7,8,11}",7.414,13.400,27.400,0.324109 +"{0,1,2,3,4,5,6,7,8,10}",7.422,13.300,27.300,0.324974 +"{0,1,2,3,4,5,6,7,8,9}",7.431,13.500,27.500,0.321332 +"{0,2,3,4,5,6,7,8,9,10,11}",7.234,13.545,28.091,0.329733 +"{0,1,3,4,5,6,7,8,9,10,11}",7.239,13.545,28.091,0.330945 +"{0,1,2,4,5,6,7,8,9,10,11}",7.249,13.545,28.091,0.333369 +"{0,1,2,3,5,6,7,8,9,10,11}",7.255,13.545,28.091,0.331551 +"{0,1,2,3,4,6,7,8,9,10,11}",7.263,13.455,28.000,0.334582 +"{0,1,2,3,4,5,7,8,9,10,11}",7.272,13.455,28.000,0.334582 +"{0,1,2,3,4,5,6,8,9,10,11}",7.279,13.455,28.000,0.331551 +"{0,1,2,3,4,5,6,7,9,10,11}",7.287,13.455,28.000,0.333369 +"{0,1,2,3,4,5,6,7,8,10,11}",7.383,13.545,28.091,0.330945 +"{0,1,2,3,4,5,6,7,8,9,11}",7.392,13.545,28.091,0.329733 +"{0,1,2,3,4,5,6,7,8,9,10}",7.399,13.636,28.182,0.328672 +"{0,1,2,3,4,5,6,7,8,9,10,11}",7.366,13.833,28.833,0.331475 \ No newline at end of file diff --git a/inst/stolz15/readme.txt b/inst/stolz15/readme.txt new file mode 100644 index 0000000..8560637 --- /dev/null +++ b/inst/stolz15/readme.txt @@ -0,0 +1,5 @@ +Tables of computed harmoniousness values, in particular periodicity, for all 2048 possible harmonies consisting of up to 12 semitones + +http://ai-linux.hs-harz.de/fstolzenburg/harmony/ (sourced 10 April 2018) + +Manually converted to csv format. diff --git a/man/complex_sonor.Rd b/man/complex_sonor.Rd index adc8a03..4c4906f 100644 --- a/man/complex_sonor.Rd +++ b/man/complex_sonor.Rd @@ -24,7 +24,7 @@ Complex sonorousness, a numeric scalar. } \description{ Computes the complex sonorousness of a sound, after -\insertCite{Parncutt1994;textual}{parn94}. +\insertCite{Parncutt1994;textual}{incon}. } \references{ \insertAllCited{} diff --git a/man/get_free_field_threshold.Rd b/man/get_free_field_threshold.Rd index c0dd5d5..f038891 100644 --- a/man/get_free_field_threshold.Rd +++ b/man/get_free_field_threshold.Rd @@ -17,7 +17,7 @@ Returns the free-field threshold (dB SPL) of hearing in quiet for pure tones of given frequencies. This is the minimum sound level at which a pure tone at that frequency will be heard. -Corresponds to Equation 2 in \insertCite{Parncutt1994;textual}{parn94}. +Corresponds to Equation 2 in \insertCite{Parncutt1994;textual}{incon}. } \references{ \insertAllCited{} diff --git a/man/get_partial_masking_level.Rd b/man/get_partial_masking_level.Rd index 8f24976..e929df2 100644 --- a/man/get_partial_masking_level.Rd +++ b/man/get_partial_masking_level.Rd @@ -21,7 +21,7 @@ get_partial_masking_level( \item{maskee_pure_tone_height}{Numeric vector of maskee pure tone heights.} -\item{k_m}{Parameter \code{k_m} in \insertCite{Parncutt1994;textual}{parn94}. +\item{k_m}{Parameter \code{k_m} in \insertCite{Parncutt1994;textual}{incon}. represents the masking pattern gradient for a pure tone, with units of dB per critical band. Parncutt & Strasburger use a value of 12 in their examples, @@ -34,7 +34,7 @@ for masker j on maskee i. \description{ Returns the effective reduction in dB of the audible level of a masked pure tone (maskee) on account of a masking pure tone (masker). -Equation 4 in \insertCite{Parncutt1994;textual}{parn94}. +Equation 4 in \insertCite{Parncutt1994;textual}{incon}. } \references{ \insertAllCited{} diff --git a/man/get_pure_tone_audibility.Rd b/man/get_pure_tone_audibility.Rd index dcc44b8..a8b0a19 100644 --- a/man/get_pure_tone_audibility.Rd +++ b/man/get_pure_tone_audibility.Rd @@ -17,7 +17,7 @@ Numeric vector of pure tone audibilities. \description{ Returns the audibility of a set of pure tone components as a function of their audible levels. -Corresponds to Equation 7 of \insertCite{Parncutt1994;textual}{parn94}. +Corresponds to Equation 7 of \insertCite{Parncutt1994;textual}{incon}. } \references{ \insertAllCited{} diff --git a/man/get_pure_tone_audible_level.Rd b/man/get_pure_tone_audible_level.Rd index 98cb072..c06e5d9 100644 --- a/man/get_pure_tone_audible_level.Rd +++ b/man/get_pure_tone_audible_level.Rd @@ -19,7 +19,7 @@ Numeric vector of audible levels (dB). \description{ Returns the audible level for set of pure tones subject to a given masking pattern. -Corresponds to Equation 6 of \insertCite{Parncutt1994;textual}{parn94}. +Corresponds to Equation 6 of \insertCite{Parncutt1994;textual}{incon}. } \references{ \insertAllCited{} diff --git a/man/get_pure_tone_height.Rd b/man/get_pure_tone_height.Rd index a66b495..977cd59 100644 --- a/man/get_pure_tone_height.Rd +++ b/man/get_pure_tone_height.Rd @@ -16,7 +16,7 @@ with units of equivalent rectangular bandwidths (ERBs). \description{ Returns the pure-tone heights (a.k.a. critical-band rates) of pure tones of given frequencies. -Equation 3 in \insertCite{Parncutt1994;textual}{parn94}. +Equation 3 in \insertCite{Parncutt1994;textual}{incon}. } \references{ \insertAllCited{} diff --git a/man/get_tone_salience.Rd b/man/get_tone_salience.Rd index d44ae6b..42de079 100644 --- a/man/get_tone_salience.Rd +++ b/man/get_tone_salience.Rd @@ -10,7 +10,7 @@ get_tone_salience(combined_audibility, k_s) \item{combined_audibility}{Numeric vector corresponding to the audibilities of each tone in the combined (pure and complex) spectrum.} -\item{k_s}{Numeric scalar; \insertCite{Parncutt1994;textual}{parn94} +\item{k_s}{Numeric scalar; \insertCite{Parncutt1994;textual}{incon} set this to 0.5.} } \value{ diff --git a/man/har_19_composite.Rd b/man/har_19_composite.Rd index 2338b23..8cda088 100644 --- a/man/har_19_composite.Rd +++ b/man/har_19_composite.Rd @@ -31,8 +31,8 @@ composite consonance model. The model combines several sub-models: \itemize{ \item \code{hutch_78_roughness}, -the roughness model of \insertCite{Hutchinson1978;textual}{dycon} -(see \code{dycon::\link[dycon]{roughness_hutch}}); +the roughness model of \insertCite{Hutchinson1978;textual}{incon} +(see \code{incon::\link[incon]{roughness_hutch}}); \item \code{har_18_harmonicity}, the harmonicity model of \insertCite{Harrison2018;textual}{incon} (see \code{incon::\link[incon]{pc_harmonicity}}); diff --git a/man/incon.Rd b/man/incon.Rd index 8e8d73b..2213ff4 100644 --- a/man/incon.Rd +++ b/man/incon.Rd @@ -76,15 +76,15 @@ the harmonicity model of \insertCite{Harrison2018;textual}{incon} the harmonicity model of \insertCite{Milne2013;textual}{incon} (see \code{incon::\link[incon]{pc_harmonicity}}). \item \code{parn_88_root_ambig}: -the root ambiguity model of \insertCite{Parncutt1988;textual}{parn88} +the root ambiguity model of \insertCite{Parncutt1988;textual}{incon} (see \code{incon::\link[incon]{root_ambiguity}}). \item \code{parn_94_complex}: the complex sonorousness feature of \insertCite{Parncutt1994;textual}{incon} (see \code{incon::\link[incon]{complex_sonor}}). \item \code{stolz_15_periodicity}: smoothed logarithmic periodicity, -after \insertCite{Stolzenburg2015;textual}{stolz15} -(see \code{stolz15::\link[stolz15]{smooth_log_periodicity}}). +after \insertCite{Stolzenburg2015;textual}{incon} +(see \code{incon::\link[incon]{smooth_log_periodicity}}). \item \code{bowl_18_min_freq_dist}: the minimum frequency distance feature of \insertCite{Bowling2018;textual}{incon} @@ -92,23 +92,23 @@ the minimum frequency distance feature of \item \code{huron_94_dyadic}: aggregate dyadic consonance, after \insertCite{Huron1994;textual}{incon}. \item \code{hutch_78_roughness}: -the roughness model of \insertCite{Hutchinson1978;textual}{dycon} -(see \code{dycon::\link[dycon]{roughness_hutch}}). +the roughness model of \insertCite{Hutchinson1978;textual}{incon} +(see \code{incon::\link[incon]{roughness_hutch}}). \item \code{parn_94_pure}: the complex sonorousness feature of \insertCite{Parncutt1994;textual}{incon} (see \code{incon::\link[incon]{pure_sonor}}). \item \code{seth_93_roughness}: -the roughness model of \insertCite{Sethares1993;textual}{dycon} -(see \code{dycon::\link[dycon]{roughness_seth}}). +the roughness model of \insertCite{Sethares1993;textual}{incon} +(see \code{incon::\link[incon]{roughness_seth}}). \item \code{vass_01_roughness}: -the roughness model of \insertCite{Vassilakis2001;textual}{dycon} -(see \code{dycon::\link[dycon]{roughness_vass}}). +the roughness model of \insertCite{Vassilakis2001;textual}{incon} +(see \code{incon::\link[incon]{roughness_vass}}). \item \code{wang_13_roughness}: -the roughness model of \insertCite{Wang2013;textual}{wang13} +the roughness model of \insertCite{Wang2013;textual}{incon} (see \code{incon::\link[incon]{roughness_wang}}). \item \code{jl_12_tonal}: -the tonal dissonance model of \insertCite{Johnson-Laird2012;textual}{jl12} -(see \code{jl12::\link[jl12]{jl_tonal_dissonance}}). +the tonal dissonance model of \insertCite{Johnson-Laird2012;textual}{incon} +(see \code{incon::\link[incon]{jl_tonal_dissonance}}). \item \code{har_19_corpus}: a corpus-based model of cultural familiarity \insertCite{Harrison2019}{incon} @@ -117,7 +117,7 @@ a corpus-based model of cultural familiarity the multiplicity feature of \insertCite{Parncutt1994;textual}{incon} (see \code{incon::\link[incon]{multiplicity}}). \item \code{har_19_composite}: -a model combining interference \insertCite{Hutchinson1978}{dycon}, +a model combining interference \insertCite{Hutchinson1978}{incon}, periodicity/harmonicity \insertCite{Harrison2018}{incon}, and cultural familiarity, as introduced by \insertCite{Harrison2019;textual}{incon}. diff --git a/man/jl_rule_1.Rd b/man/jl_rule_1.Rd index 72e8e1e..7974062 100644 --- a/man/jl_rule_1.Rd +++ b/man/jl_rule_1.Rd @@ -18,7 +18,7 @@ No input checking is performed.} than chords occurring only in a minor scale, which in turn should be less dissonant than chords occurring in neither sort of scale." -\insertCite{Johnson-Laird2012}{jl12}. +\insertCite{Johnson-Laird2012}{incon}. } \references{ \insertAllCited{} diff --git a/man/jl_rule_2.Rd b/man/jl_rule_2.Rd index ffdf210..2f33c8b 100644 --- a/man/jl_rule_2.Rd +++ b/man/jl_rule_2.Rd @@ -16,7 +16,7 @@ No input checking is performed.} \description{ "Chords that are consistent with a major triad are more consonant than chords that are not consistent with a major triad" -\insertCite{Johnson-Laird2012}{jl12}. +\insertCite{Johnson-Laird2012}{incon}. Consistency with the major triad means that a major triad must be contained within the chord. Additionally, all notes must be contained within a major scale. diff --git a/man/jl_rule_3.Rd b/man/jl_rule_3.Rd index 9856c64..52aa309 100644 --- a/man/jl_rule_3.Rd +++ b/man/jl_rule_3.Rd @@ -21,7 +21,7 @@ accessible with the command \code{attr(x, "solution")}. than chords that are not built from thirds." "The principle allows for just one missing third intervening between two pitch classes a fifth apart" -\insertCite{Johnson-Laird2012}{jl12}. +\insertCite{Johnson-Laird2012}{incon}. } \references{ \insertAllCited{} diff --git a/man/jl_tonal_dissonance.Rd b/man/jl_tonal_dissonance.Rd index 7a6e105..9c4dbe8 100644 --- a/man/jl_tonal_dissonance.Rd +++ b/man/jl_tonal_dissonance.Rd @@ -22,7 +22,7 @@ with higher values corresponding to increasing degrees of dissonance. } \description{ Computes tonal dissonance using the algorithm -of \insertCite{Johnson-Laird2012;textual}{jl12}. +of \insertCite{Johnson-Laird2012;textual}{incon}. } \examples{ jl_tonal_dissonance(c(0, 4, 7)) diff --git a/man/multiplicity.Rd b/man/multiplicity.Rd index ba67b20..e71f5c3 100644 --- a/man/multiplicity.Rd +++ b/man/multiplicity.Rd @@ -17,14 +17,14 @@ multiplicity(x, k_s = parn94_params()$k_s, ...) \item{k_s}{Numeric scalar, parameter from Parncutt & Strasburger (1994).} -\item{...}{Further parameters to pass to \code{\link{parn94}()}.} +\item{...}{Further parameters to pass to \code{\link{incon}()}.} } \value{ Multiplicity, a numeric scalar. } \description{ Computes the multiplicity of a sound, after -\insertCite{Parncutt1994;textual}{parn94}. +\insertCite{Parncutt1994;textual}{incon}. } \references{ \insertAllCited{} diff --git a/man/parn88.Rd b/man/parn88.Rd index e885512..fd2bb43 100644 --- a/man/parn88.Rd +++ b/man/parn88.Rd @@ -20,8 +20,8 @@ This will be coerced to an object of class \code{\link[hrep]{pc_set}}.} Identifies the root support weights to use. \itemize{ \item \code{"v2"} (default) uses the updated -weights from \insertCite{Parncutt2006;textual}{parn88}. -\item \code{"v1"} uses the original weights from \insertCite{Parncutt2006;textual}{parn88}. +weights from \insertCite{Parncutt2006;textual}{incon}. +\item \code{"v1"} uses the original weights from \insertCite{Parncutt2006;textual}{incon}. } See \code{\link{root_support_weights}} for the values of these weights. @@ -30,7 +30,7 @@ with one column (interval) identifying the ascending interval in semitones, and another column (weight) identifying the corresponding root support weight.} \item{exponent}{(Numeric scalar) Exponent to be used when computing -root ambiguities. Defaults to 0.5, after \insertCite{Parncutt1988;textual}{parn88}.} +root ambiguities. Defaults to 0.5, after \insertCite{Parncutt1988;textual}{incon}.} } \value{ A list with three values: @@ -42,7 +42,7 @@ A list with three values: } \description{ Analyses a pitch-class set using the root-finding model of -\insertCite{Parncutt1988;textual}{parn88}. +\insertCite{Parncutt1988;textual}{incon}. } \references{ \insertAllCited{} diff --git a/man/parn94.Rd b/man/parn94.Rd index 041872d..ebb20ea 100644 --- a/man/parn94.Rd +++ b/man/parn94.Rd @@ -54,7 +54,7 @@ as created by \code{\link{parn94_params}()}.} \description{ This function analyses a sonority using Richard Parncutt's psychoacoustic model of harmony, as described in -\insertCite{Parncutt1994;textual}{parn94}. +\insertCite{Parncutt1994;textual}{incon}. } \references{ \insertAllCited{} diff --git a/man/parn94_params.Rd b/man/parn94_params.Rd index 77b55fb..4fda06b 100644 --- a/man/parn94_params.Rd +++ b/man/parn94_params.Rd @@ -55,7 +55,7 @@ such as \code{\link{parn94}}. \description{ This function compiles parameters for Parncutt's psychoacoustic model. The parameters are defined with reference to -\insertCite{Parncutt1994;textual}{parn94}. +\insertCite{Parncutt1994;textual}{incon}. } \references{ \insertAllCited{} diff --git a/man/pitch_commonality.Rd b/man/pitch_commonality.Rd index 38be538..9556bd6 100644 --- a/man/pitch_commonality.Rd +++ b/man/pitch_commonality.Rd @@ -20,7 +20,7 @@ Pitch commonality, as a numeric scalar. } \description{ Gets the pitch commonality between two sonorities, after -\insertCite{Parncutt1994;textual}{parn94}. +\insertCite{Parncutt1994;textual}{incon}. } \references{ \insertAllCited{} diff --git a/man/pitch_distance.Rd b/man/pitch_distance.Rd index 3fe39ae..52cec7b 100644 --- a/man/pitch_distance.Rd +++ b/man/pitch_distance.Rd @@ -20,7 +20,7 @@ Pitch distance, as a numeric scalar. } \description{ Gets the pitch distance between two sonorities, after -\insertCite{Parncutt1994;textual}{parn94}. +\insertCite{Parncutt1994;textual}{incon}. } \references{ \insertAllCited{} diff --git a/man/pitch_salience.Rd b/man/pitch_salience.Rd index 32e216d..345dd1d 100644 --- a/man/pitch_salience.Rd +++ b/man/pitch_salience.Rd @@ -29,7 +29,7 @@ and the last element corresponds to the \code{max_midi} argument. } \description{ Analyses the pitch salience of a sonority, after -\insertCite{Parncutt1994;textual}{parn94}. +\insertCite{Parncutt1994;textual}{incon}. } \references{ \insertAllCited{} diff --git a/man/pure_sonor.Rd b/man/pure_sonor.Rd index 0974b5a..0a90429 100644 --- a/man/pure_sonor.Rd +++ b/man/pure_sonor.Rd @@ -24,7 +24,7 @@ Pure sonorousness, a numeric scalar. } \description{ Computes the pure sonorousness of a sound, after -\insertCite{Parncutt1994;textual}{parn94}. +\insertCite{Parncutt1994;textual}{incon}. } \references{ \insertAllCited{} diff --git a/man/root.Rd b/man/root.Rd index 27c4946..20cd454 100644 --- a/man/root.Rd +++ b/man/root.Rd @@ -7,15 +7,15 @@ root(...) } \arguments{ -\item{...}{Arguments to pass to \code{\link{parn88}}.} +\item{...}{Arguments to pass to \code{\link{incon}}.} } \value{ The estimated chord root (integer scalar). } \description{ Estimates the chord root of a pitch-class set using the root-finding model of -\insertCite{Parncutt1988;textual}{parn88}. -This function is a wrapper for \code{\link{parn88}}. +\insertCite{Parncutt1988;textual}{incon}. +This function is a wrapper for \code{\link{incon}}. } \references{ \insertAllCited{} diff --git a/man/root_ambiguity.Rd b/man/root_ambiguity.Rd index ea22fc6..2478514 100644 --- a/man/root_ambiguity.Rd +++ b/man/root_ambiguity.Rd @@ -14,7 +14,7 @@ The root ambiguity (numeric scalar). } \description{ Estimates the root ambiguity of a pitch-class set using the root-finding model of -\insertCite{Parncutt1988;textual}{parn88}. +\insertCite{Parncutt1988;textual}{incon}. This function is a wrapper for \code{\link{parn88}}. } \references{ diff --git a/man/root_support_weights.Rd b/man/root_support_weights.Rd index 4be7e9e..ed227a4 100644 --- a/man/root_support_weights.Rd +++ b/man/root_support_weights.Rd @@ -12,7 +12,7 @@ root_support_weights } \description{ A list of different root support weights that may be used -by the root-finding algorithm of \insertCite{Parncutt1988;textual}{parn88}. +by the root-finding algorithm of \insertCite{Parncutt1988;textual}{incon}. See \code{\link{parn88}} for more information. } \references{ diff --git a/man/smooth_log_periodicity.Rd b/man/smooth_log_periodicity.Rd new file mode 100644 index 0000000..4125abd --- /dev/null +++ b/man/smooth_log_periodicity.Rd @@ -0,0 +1,38 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/model-stolz15.R +\name{smooth_log_periodicity} +\alias{smooth_log_periodicity} +\alias{smooth_log_periodicity.default} +\alias{smooth_log_periodicity.pi_chord} +\title{Smoothed log periodicity} +\usage{ +smooth_log_periodicity(x, d = 0.011) + +\method{smooth_log_periodicity}{default}(x, d = 0.011) + +\method{smooth_log_periodicity}{pi_chord}(x, d = 0.011) +} +\arguments{ +\item{x}{Sonority to analyse. +This will be coerced to an object of class \code{\link[hrep]{pi_chord}}. +Numeric inputs will be interpreted as MIDI note numbers.} + +\item{d}{(numeric scalar): +Maximal allowed error in the algorithm's +interval approximation step, expressed as +a fraction of the original interval. +The default value, 0.011, corresponds to 'Rational Tuning II' +in Stolzenburg's paper.} +} +\value{ +A numeric scalar identifying the chord's periodicity. +High values mean a higher period length, lower periodicity, +and lower consonance. +} +\description{ +This function computes a chord's smoothed logarithmic periodicity, +after \insertCite{Stolzenburg2015;textual}{incon}. +} +\references{ +\insertAllCited{} +} diff --git a/tests/testthat/test-stolz15.R b/tests/testthat/test-stolz15.R new file mode 100644 index 0000000..4025ef7 --- /dev/null +++ b/tests/testthat/test-stolz15.R @@ -0,0 +1,69 @@ +context("test-misc") + +context("stolzenburg") + +library(magrittr) + +test_that("approximating fractions", { + # These examples are taken from Table 1 of + # 10.1080/17459737.2015.1033024 + expect_equal(fraction(1.059, 0.01), c(16, 15)) + expect_equal(fraction(1.122, 0.01), c(9, 8)) + expect_equal(fraction(1.189, 0.01), c(6, 5)) + expect_equal(fraction(1.260, 0.01), c(5, 4)) + expect_equal(fraction(1.335, 0.011), c(4, 3)) + expect_equal(fraction(1.414, 0.011), c(7, 5)) + expect_equal(fraction(1.498, 0.011), c(3, 2)) + expect_equal(fraction(1.587, 0.011), c(8, 5)) +}) + +test_that("double fraction", { + expect_equal(double_fraction(c(3, 4)), c(3, 2)) + expect_equal(double_fraction(c(3, 7)), c(6, 7)) +}) + +test_that("double fraction", { + expect_equal(half_fraction(c(6, 5)), c(3, 5)) + expect_equal(half_fraction(c(3, 7)), c(3, 14)) +}) + +test_that("get_rational_interval_2", { + expect_equal(get_rational_interval(0), c(1, 1)) + expect_equal(get_rational_interval(12), c(2, 1)) + expect_equal(get_rational_interval(-3), c(5, 6)) + expect_equal(get_rational_interval(-9), c(3, 5)) + expect_equal(get_rational_interval(-6), c(7, 10)) +}) + +test_that("least common multiple", { + expect_equal(lcm(c(4, 6)), 12) + expect_equal(lcm(c(21, 6)), 42) + expect_equal(lcm(c(8, 9, 21)), 504) +}) + +test_that("smooth_log_periodicity", { + expect_equal(smooth_log_periodicity(c(0, 3, 9)) %>% round(digits = 1), + 3.7) + expect_equal(smooth_log_periodicity(c(48, 64, 67)), 1) + expect_equal(smooth_log_periodicity(c(0, 3, 7)), log2(10)) + expect_equal(smooth_log_periodicity(c(0:11)) %>% round(digits = 1), 7.4) +}) + +context("test-regression") + +test_that("regression tests", { + df <- read.csv(system.file("stolz15/data-formatted.csv", + package = "incon"), + stringsAsFactors = FALSE) + chords <- lapply(strsplit(gsub("\\{|\\}", "", df$chord), + split = ","), + as.integer) + + for (i in seq_along(chords)) { + expect_equal( + smooth_log_periodicity(chords[[i]]), + df$periodicity.stolz_smooth_t2_log[[i]], + tolerance = 1e-3 + ) + } +}) From 53f15f98ba1f936851ac5a7b09b3cc778b4ca534 Mon Sep 17 00:00:00 2001 From: Peter Harrison Date: Tue, 23 Jan 2024 23:47:09 +0000 Subject: [PATCH 16/24] Fix tests --- NAMESPACE | 5 ++- R/model-bowl18.R | 1 + R/model-stolz15.R | 14 +++--- tests/testthat/test-jl12.R | 84 +++++++++++++++++------------------ tests/testthat/test-stolz15.R | 22 ++++----- 5 files changed, 64 insertions(+), 62 deletions(-) diff --git a/NAMESPACE b/NAMESPACE index 52f30f0..fd98846 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -10,6 +10,7 @@ S3method(count_chords,corpus) S3method(double,fraction) S3method(expand_harmonics,fraction) S3method(expand_harmonics,rational_chord) +S3method(fraction,numeric) S3method(gcd,rational_chord) S3method(half,fraction) S3method(huron_1994,default) @@ -18,6 +19,7 @@ S3method(huron_1994,pc_set) S3method(jl_tonal_dissonance,default) S3method(jl_tonal_dissonance,pc_set) S3method(kl_div_from_uniform,smooth_spectrum) +S3method(lcm,rational_chord) S3method(multiplicity,default) S3method(multiplicity,parn94) S3method(parn88,default) @@ -34,6 +36,7 @@ S3method(pitch_salience,pitch_salience) S3method(print,corpus_dissonance_table) S3method(pure_sonor,default) S3method(pure_sonor,parn94) +S3method(rationalise_chord,pi_chord) S3method(roughness_hutch,default) S3method(roughness_hutch,sparse_fr_spectrum) S3method(roughness_seth,default) @@ -54,7 +57,6 @@ export(corpus_dissonance_table) export(cosine_similarity) export(count_chords) export(demo_wang) -export(fraction.numeric) export(get_free_field_threshold) export(get_overall_masking_level) export(get_partial_masking_level) @@ -79,7 +81,6 @@ export(jl_rule_2) export(jl_rule_3) export(jl_tonal_dissonance) export(kl_div_from_uniform) -export(lcm.rational_chord) export(list_models) export(multiplicity) export(parn88) diff --git a/R/model-bowl18.R b/R/model-bowl18.R index bf17a87..21ff469 100644 --- a/R/model-bowl18.R +++ b/R/model-bowl18.R @@ -185,6 +185,7 @@ rationalise_chord <- function(x, tonic) { UseMethod("rationalise_chord") } +#' @export rationalise_chord.pi_chord <- function(x, tonic) { x <- hrep::tp(x, - tonic) octave <- floor(hrep::get_bass_pi(x) / 12) diff --git a/R/model-stolz15.R b/R/model-stolz15.R index a88d9de..93381f8 100644 --- a/R/model-stolz15.R +++ b/R/model-stolz15.R @@ -36,14 +36,14 @@ smooth_log_periodicity.pi_chord <- function(x, d = 0.011) { chord <- as.numeric(x) mean(vapply(seq_along(x), function(i) { tmp_chord <- x - x[i] - log2(relative_periodicity(rationalise_chord(tmp_chord, d = d))) + log2(relative_periodicity(stolz15_rationalise_chord(tmp_chord, d = d))) }, numeric(1))) } # See DOI: 10.1080/17459737.2015.1033024 # @param x Number to approximate # @param d Tolerance ratio -fraction <- function(x, d, verbose = FALSE) { +stolz15_fraction <- function(x, d, verbose = FALSE) { x_min <- (1 - d) * x x_max <- (1 + d) * x a_l <- floor(x) @@ -81,7 +81,7 @@ get_rational_interval <- function(x, d) { stopifnot(length(x) == 1L) octave <- floor(x / 12) pitch_class <- x %% 12 - res <- fraction(2 ^ (pitch_class / 12), d = 0.011) + res <- stolz15_fraction(2 ^ (pitch_class / 12), d = 0.011) while (octave != 0) { if (octave < 0) { res <- half_fraction(res) @@ -106,17 +106,17 @@ double_fraction <- function(x) { x } -rationalise_chord <- function(x, d) { +stolz15_rationalise_chord <- function(x, d) { sapply(x, function(y) get_rational_interval(y, d)) } relative_periodicity <- function(x) { stopifnot(is.matrix(x), nrow(x) == 2, ncol(x) > 0L) - lcm(x[2, ]) * x[1, 1] / x[2, 1] + stolz15_lcm(x[2, ]) * x[1, 1] / x[2, 1] } -lcm <- function(x) { +stolz15_lcm <- function(x) { if (length(x) == 1L) x else if (length(x) == 2L) { gmp::lcm.default(x[1], x[2]) - } else lcm(c(x[1], lcm(x[-1]))) + } else stolz15_lcm(c(x[1], stolz15_lcm(x[-1]))) } diff --git a/tests/testthat/test-jl12.R b/tests/testthat/test-jl12.R index 896a9de..df385cd 100644 --- a/tests/testthat/test-jl12.R +++ b/tests/testthat/test-jl12.R @@ -2,48 +2,48 @@ context("main") library(magrittr) -test_that("combined model", { - # Test against the original paper - df <- c("johnson-laird-2012-data/figure-2.csv", - "johnson-laird-2012-data/figure-3.csv") %>% - system.file(package = "incon") %>% - lapply(function(x) read.csv(x, stringsAsFactors = FALSE)) %>% - do.call(rbind, .) - df$midi <- I(lapply(strsplit(df$midi, " "), as.numeric)) - df$pc_set <- I(lapply(df$midi, function(x) { - stopifnot(all.equal(x, sort(x))) - res <- sort(x %% 12) - stopifnot(!anyDuplicated(res)) - res - })) - df$rule_1 <- sapply(df$pc_set, jl_rule_1) - df$rule_2 <- sapply(df$pc_set, jl_rule_2) - df$rule_3 <- sapply(df$pc_set, jl_rule_3) - df$dual_process_2 <- sapply(df$pc_set, jl_tonal_dissonance) - # expect_equal(df$dual_process, df$dual_process_2) - # Seems there are mistakes in the original paper. - ## Fig 2: - # Here are the implied classifications in the paper: - # 1: consonant according to all rules - # 2-9: consonant by R1, dissonant by rule 2, consonant by R3 - # 10-15: consonant by R1, dissononant by R2, dissonant by R3 - # 16: medium by R1, dissonant by R2, consonant by R3 - # 17-18: medium by R1, dissonant by R2, consonant by R3 (?) same as previous category - # 19: dissonant by all rules - ## Here are some example problems with the paper's annotations: - # chord 6 cannot be built from stacked thirds, whereas chord 7 can. - # => chord 6 should be in a different category to chord 7 - # chord 11 can be bult by stacking thirds, whereas chord 10 can't - # => chord 11 should come in a different category to chord 10 - # chord 16 is not in the same category as chords 17-18, yet they all - # have the same properties: - # - from minor scale (R1) - # - neither contain a major triad (R2) - # - each can be built from thirds - ## Similar problems exist for the Figure 3. - # Conclusion: the original paper doesn't provide useful - # regression tests. -}) +# test_that("combined model", { +# # Test against the original paper +# df <- c("johnson-laird-2012-data/figure-2.csv", +# "johnson-laird-2012-data/figure-3.csv") %>% +# system.file(package = "incon") %>% +# lapply(function(x) read.csv(x, stringsAsFactors = FALSE)) %>% +# do.call(rbind, .) +# df$midi <- I(lapply(strsplit(df$midi, " "), as.numeric)) +# df$pc_set <- I(lapply(df$midi, function(x) { +# stopifnot(all.equal(x, sort(x))) +# res <- sort(x %% 12) +# stopifnot(!anyDuplicated(res)) +# res +# })) +# df$rule_1 <- sapply(df$pc_set, jl_rule_1) +# df$rule_2 <- sapply(df$pc_set, jl_rule_2) +# df$rule_3 <- sapply(df$pc_set, jl_rule_3) +# df$dual_process_2 <- sapply(df$pc_set, jl_tonal_dissonance) +# # expect_equal(df$dual_process, df$dual_process_2) +# # Seems there are mistakes in the original paper. +# ## Fig 2: +# # Here are the implied classifications in the paper: +# # 1: consonant according to all rules +# # 2-9: consonant by R1, dissonant by rule 2, consonant by R3 +# # 10-15: consonant by R1, dissononant by R2, dissonant by R3 +# # 16: medium by R1, dissonant by R2, consonant by R3 +# # 17-18: medium by R1, dissonant by R2, consonant by R3 (?) same as previous category +# # 19: dissonant by all rules +# ## Here are some example problems with the paper's annotations: +# # chord 6 cannot be built from stacked thirds, whereas chord 7 can. +# # => chord 6 should be in a different category to chord 7 +# # chord 11 can be bult by stacking thirds, whereas chord 10 can't +# # => chord 11 should come in a different category to chord 10 +# # chord 16 is not in the same category as chords 17-18, yet they all +# # have the same properties: +# # - from minor scale (R1) +# # - neither contain a major triad (R2) +# # - each can be built from thirds +# ## Similar problems exist for the Figure 3. +# # Conclusion: the original paper doesn't provide useful +# # regression tests. +# }) test_that("major scales", { expect_equal(major_scales$`2`, diff --git a/tests/testthat/test-stolz15.R b/tests/testthat/test-stolz15.R index 4025ef7..7aa9918 100644 --- a/tests/testthat/test-stolz15.R +++ b/tests/testthat/test-stolz15.R @@ -7,14 +7,14 @@ library(magrittr) test_that("approximating fractions", { # These examples are taken from Table 1 of # 10.1080/17459737.2015.1033024 - expect_equal(fraction(1.059, 0.01), c(16, 15)) - expect_equal(fraction(1.122, 0.01), c(9, 8)) - expect_equal(fraction(1.189, 0.01), c(6, 5)) - expect_equal(fraction(1.260, 0.01), c(5, 4)) - expect_equal(fraction(1.335, 0.011), c(4, 3)) - expect_equal(fraction(1.414, 0.011), c(7, 5)) - expect_equal(fraction(1.498, 0.011), c(3, 2)) - expect_equal(fraction(1.587, 0.011), c(8, 5)) + expect_equal(stolz15_fraction(1.059, 0.01), c(16, 15)) + expect_equal(stolz15_fraction(1.122, 0.01), c(9, 8)) + expect_equal(stolz15_fraction(1.189, 0.01), c(6, 5)) + expect_equal(stolz15_fraction(1.260, 0.01), c(5, 4)) + expect_equal(stolz15_fraction(1.335, 0.011), c(4, 3)) + expect_equal(stolz15_fraction(1.414, 0.011), c(7, 5)) + expect_equal(stolz15_fraction(1.498, 0.011), c(3, 2)) + expect_equal(stolz15_fraction(1.587, 0.011), c(8, 5)) }) test_that("double fraction", { @@ -36,9 +36,9 @@ test_that("get_rational_interval_2", { }) test_that("least common multiple", { - expect_equal(lcm(c(4, 6)), 12) - expect_equal(lcm(c(21, 6)), 42) - expect_equal(lcm(c(8, 9, 21)), 504) + expect_equal(stolz15_lcm(c(4, 6)), 12) + expect_equal(stolz15_lcm(c(21, 6)), 42) + expect_equal(stolz15_lcm(c(8, 9, 21)), 504) }) test_that("smooth_log_periodicity", { From aa6d24ade4e2768a4fa0a7be82b4b859d49c257f Mon Sep 17 00:00:00 2001 From: Peter Harrison Date: Wed, 24 Jan 2024 21:55:47 +0000 Subject: [PATCH 17/24] Fixing tests --- DESCRIPTION | 11 ++++++++++- R/model-corpdiss.R | 2 +- R/model-huron-1994.R | 10 ++++++++++ man/corpus_dissonance.Rd | 2 +- man/huron_1994.Rd | 16 ++++++++++++++++ man/huron_1994_weights.Rd | 16 ++++++++++++++++ 6 files changed, 54 insertions(+), 3 deletions(-) create mode 100644 man/huron_1994.Rd create mode 100644 man/huron_1994_weights.Rd diff --git a/DESCRIPTION b/DESCRIPTION index 669a779..ef470ff 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -10,6 +10,7 @@ License: MIT + file LICENSE Encoding: UTF-8 LazyData: true Imports: + assertthat, Rdpack (>= 0.11.0), gmp (>= 0.5.13.2), hrep (>= 0.10.0.9001), @@ -19,6 +20,9 @@ Imports: purrr (>= 0.3.2), methods, gtools, + Rcpp, + numbers, + phonTools, tibble (>= 2.1.3), dplyr (>= 0.8.3), rlang (>= 0.4.0), @@ -30,8 +34,13 @@ Suggests: knitr (>= 1.23), DT (>= 0.5), covr (>= 3.2.1), + cowplot, ggplot2 (>= 3.1.0.9000), - hcorp + hcorp, + shiny, + shinydashboard, + shinyjs, + tuneR RoxygenNote: 7.3.1 Remotes: pmcharrison/hrep diff --git a/R/model-corpdiss.R b/R/model-corpdiss.R index 78a746d..c7b3d89 100644 --- a/R/model-corpdiss.R +++ b/R/model-corpdiss.R @@ -118,7 +118,7 @@ type.corpus_dissonance_table <- function(x) { #' @references #' \insertAllCited{} #' @export -corpus_dissonance <- function(x, table = popular_1_pc_chord_type) { +corpus_dissonance <- function(x, table = incon::popular_1_pc_chord_type) { typ <- type(table) x <- hrep::represent(x, typ) i <- hrep::encode(x) diff --git a/R/model-huron-1994.R b/R/model-huron-1994.R index ba86c6d..b362a95 100644 --- a/R/model-huron-1994.R +++ b/R/model-huron-1994.R @@ -1,3 +1,10 @@ +#' Dyadic consonance (Huron 1994) +#' +#' Computes the aggregate dyadic consonance of a chord following +#' Huron (1994). +#' +#' @param x Chord to evaluate, will be coerced to a +#' pitch-class set (\code{\link[hrep]{pc_set}}). #' @export huron_1994 <- function(x) { UseMethod("huron_1994") @@ -20,6 +27,9 @@ huron_1994.int_vec <- function(x) { sum(x * huron_1994_weights) } +#' Dyadic consonance weights (Huron 1994) +#' +#' A vector of the weights used in Huron's (1994) dyadic consonance model. #' @export huron_1994_weights <- c(- 1.428, - 0.582, diff --git a/man/corpus_dissonance.Rd b/man/corpus_dissonance.Rd index c4527b7..c934438 100644 --- a/man/corpus_dissonance.Rd +++ b/man/corpus_dissonance.Rd @@ -4,7 +4,7 @@ \alias{corpus_dissonance} \title{Corpus dissonance} \usage{ -corpus_dissonance(x, table = popular_1_pc_chord_type) +corpus_dissonance(x, table = incon::popular_1_pc_chord_type) } \arguments{ \item{x}{Sonority to analyse. diff --git a/man/huron_1994.Rd b/man/huron_1994.Rd new file mode 100644 index 0000000..b6e1e2b --- /dev/null +++ b/man/huron_1994.Rd @@ -0,0 +1,16 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/model-huron-1994.R +\name{huron_1994} +\alias{huron_1994} +\title{Dyadic consonance (Huron 1994)} +\usage{ +huron_1994(x) +} +\arguments{ +\item{x}{Chord to evaluate, will be coerced to a +pitch-class set (\code{\link[hrep]{pc_set}}).} +} +\description{ +Computes the aggregate dyadic consonance of a chord following +Huron (1994). +} diff --git a/man/huron_1994_weights.Rd b/man/huron_1994_weights.Rd new file mode 100644 index 0000000..99134a9 --- /dev/null +++ b/man/huron_1994_weights.Rd @@ -0,0 +1,16 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/model-huron-1994.R +\docType{data} +\name{huron_1994_weights} +\alias{huron_1994_weights} +\title{Dyadic consonance weights (Huron 1994)} +\format{ +An object of class \code{numeric} of length 6. +} +\usage{ +huron_1994_weights +} +\description{ +A vector of the weights used in Huron's (1994) dyadic consonance model. +} +\keyword{datasets} From 36816580e61799fed0bc4fef9395729d4a89c2ff Mon Sep 17 00:00:00 2001 From: Peter Harrison Date: Wed, 24 Jan 2024 21:56:53 +0000 Subject: [PATCH 18/24] Fix DESCRIPTION --- DESCRIPTION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DESCRIPTION b/DESCRIPTION index ef470ff..5ea1c63 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -43,7 +43,7 @@ Suggests: tuneR RoxygenNote: 7.3.1 Remotes: - pmcharrison/hrep + pmcharrison/hrep, pmcharrison/hcorp VignetteBuilder: knitr Byte-Compile: yes From 503cb8a71fb0b678c61f14a53f122d8bf2a297f4 Mon Sep 17 00:00:00 2001 From: Peter Harrison Date: Wed, 24 Jan 2024 22:36:51 +0000 Subject: [PATCH 19/24] Remove hutch_visualise_theme --- NAMESPACE | 1 - R/model-dycon.R | 19 +------------------ man/hutch_visualise.Rd | 5 ++--- man/hutch_visualise_theme.Rd | 18 ------------------ 4 files changed, 3 insertions(+), 40 deletions(-) delete mode 100644 man/hutch_visualise_theme.Rd diff --git a/NAMESPACE b/NAMESPACE index fd98846..67212bb 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -72,7 +72,6 @@ export(hutch_cbw) export(hutch_dissonance_function) export(hutch_g) export(hutch_visualise) -export(hutch_visualise_theme) export(hutch_y) export(incon) export(incon_models) diff --git a/R/model-dycon.R b/R/model-dycon.R index a14d326..685c600 100644 --- a/R/model-dycon.R +++ b/R/model-dycon.R @@ -212,21 +212,6 @@ hutch_visualise_data <- function(x, cbw_cut_off, a, b, min_freq, max_freq, ...) df3 } -#' ggplot theme -#' -#' Defines a default theme for visualising computations -#' for Hutchinson & Knopoff's (1978) model -#' (see \code{\link{hutch_visualise}}). -#' @export -hutch_visualise_theme <- ggplot2::theme_classic() + - ggplot2::theme( - panel.spacing = ggplot2::unit(1.9, "lines"), - strip.background = ggplot2::element_blank(), - axis.text.x = ggplot2::element_text(colour = "black"), - axis.text.y = ggplot2::element_text(colour = "black"), - axis.ticks = ggplot2::element_line(colour = "black") - ) - #' Visualise #' #' Creates a plot visualising computations for Hutchinson & Knopoff's model. @@ -252,7 +237,6 @@ hutch_visualise <- function(x, colour_limits = c(0, 3), colour_low = "darkblue", colour_high = "red", - theme = hutch_visualise_theme, ...) { stopifnot(is.list(x), !is.null(names(x)), !anyDuplicated(names(x))) x <- purrr::map(x, hrep::sparse_fr_spectrum, ...) @@ -272,8 +256,7 @@ hutch_visualise <- function(x, low = colour_low, high = colour_high, limits = colour_limits) + - ggplot2::facet_wrap(~ label, ncol = 1) + - theme + ggplot2::facet_wrap(~ label, ncol = 1) } #' Spectral roughness (Sethares) diff --git a/man/hutch_visualise.Rd b/man/hutch_visualise.Rd index a285219..afac64e 100644 --- a/man/hutch_visualise.Rd +++ b/man/hutch_visualise.Rd @@ -14,7 +14,6 @@ hutch_visualise( colour_limits = c(0, 3), colour_low = "darkblue", colour_high = "red", - theme = hutch_visualise_theme, ... ) } @@ -37,9 +36,9 @@ hutch_visualise( \item{colour_high}{Colour to use for the highest roughness.} -\item{theme}{\code{\link[ggplot2]{ggplot}} theme to use.} - \item{...}{Passed to \code{\link[hrep]{sparse_fr_spectrum}}.} + +\item{theme}{\code{\link[ggplot2]{ggplot}} theme to use.} } \description{ Creates a plot visualising computations for Hutchinson & Knopoff's model. diff --git a/man/hutch_visualise_theme.Rd b/man/hutch_visualise_theme.Rd deleted file mode 100644 index 2527132..0000000 --- a/man/hutch_visualise_theme.Rd +++ /dev/null @@ -1,18 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/model-dycon.R -\docType{data} -\name{hutch_visualise_theme} -\alias{hutch_visualise_theme} -\title{ggplot theme} -\format{ -An object of class \code{theme} (inherits from \code{gg}) of length 97. -} -\usage{ -hutch_visualise_theme -} -\description{ -Defines a default theme for visualising computations -for Hutchinson & Knopoff's (1978) model -(see \code{\link{hutch_visualise}}). -} -\keyword{datasets} From 9ca202d888a1b1162bdb5298694d8be764ba947a Mon Sep 17 00:00:00 2001 From: Peter Harrison Date: Tue, 30 Jan 2024 17:30:57 +0000 Subject: [PATCH 20/24] Update gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 9f4e178..64a3e5c 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,5 @@ www inst/shiny/cache .cache .DS_Store +.idea +.ipynb_checkpoints From 1f08a50d4b766f2f115f45b5681459d4da0765eb Mon Sep 17 00:00:00 2001 From: Peter Harrison Date: Tue, 30 Jan 2024 17:31:06 +0000 Subject: [PATCH 21/24] Update README --- README.Rmd | 27 +++++++++++++++++++++++---- README.md | 17 +++++++++++++++++ 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/README.Rmd b/README.Rmd index ff5001c..bf8f2ad 100644 --- a/README.Rmd +++ b/README.Rmd @@ -31,10 +31,8 @@ models of simultaneous consonance perception. ## Citation -Harrison, P. M. C., & Pearce, M. T. (2019). -Instantaneous consonance in the perception and composition of Western music. -*PsyArXiv*. -https://doi.org/10.31234/osf.io/6jsug +Harrison, P. M. C., & Pearce, M. T. (2020). Simultaneous consonance in music perception and composition. +*Psychological Review*, *127*(2), 216–244. ## Installation @@ -46,6 +44,27 @@ if (!require(devtools)) install.packages("devtools") devtools::install_github("pmcharrison/incon") ``` +To run the Jupyter notebook, you need to download this code repository. +You then need to install Jupyter Notebook (see https://jupyter.org/install): + +``` +pip install notebook +``` + +Then open R, install `IRkernel`, then run `installspec`: + +```r +install.packages("IRkernel") +IRkernel::installspec() +``` + +Then from a new terminal, open this notebook: + +``` +jupyter notebook Demo.ipynb +``` + + ## Usage The primary function is `incon`, diff --git a/README.md b/README.md index 1a3b4c4..a93850f 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,23 @@ if (!require(devtools)) install.packages("devtools") devtools::install_github("pmcharrison/incon") ``` +To run the Jupyter notebook, you need to download this code repository. +You then need to install Jupyter Notebook (see +): + + pip install notebook + +Then open R, install `IRkernel`, then run `installspec`: + +``` r +install.packages("IRkernel") +IRkernel::installspec() +``` + +Then from a new terminal, open this notebook: + + jupyter notebook Demo.ipynb + ## Usage The primary function is `incon`, which applies consonance models to an From 4b4f4b5d208f25c85aa4d99109ef0fed367f9b43 Mon Sep 17 00:00:00 2001 From: Peter Harrison Date: Tue, 30 Jan 2024 17:31:11 +0000 Subject: [PATCH 22/24] Add notebook --- Demo.ipynb | 444 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 444 insertions(+) create mode 100644 Demo.ipynb diff --git a/Demo.ipynb b/Demo.ipynb new file mode 100644 index 0000000..3aba2ef --- /dev/null +++ b/Demo.ipynb @@ -0,0 +1,444 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "b66440e4-d192-4ac9-9553-718cd2618059", + "metadata": {}, + "source": [ + "# incon package demo\n", + "\n", + "This notebook demonstrates the basic functionality of the 'incon' package.\n", + "To run code in this notebook, select the top cell in the notebook (i.e. this one,\n", + "then click the 'Run' button to run one cell at a time and advance to the next one.\n", + "You can change the content of cells and rerun them to get new results if you wish." + ] + }, + { + "cell_type": "code", + "execution_count": 87, + "id": "516871a1-57c0-4289-beb9-4201f3689465", + "metadata": {}, + "outputs": [], + "source": [ + "# Run this cell to load core packages\n", + "\n", + "suppressPackageStartupMessages({\n", + " library(glue)\n", + " library(incon)\n", + " library(dplyr, quietly = TRUE)\n", + " library(hrep, quietly = TRUE)\n", + "})\n", + "\n", + "display_chord <- abcR:::html_from_pi_chord" + ] + }, + { + "cell_type": "markdown", + "id": "756e4ee8-6108-4f52-a9be-a7e7b984da46", + "metadata": {}, + "source": [ + "The incon package provides a collection of implementations of consonance models.\n", + "These models are enumerated in the following table:" + ] + }, + { + "cell_type": "code", + "execution_count": 88, + "id": "673f4370-d2fa-4412-b31e-763b79a2ebc4", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\t\n", + "\t\n", + "\n", + "\n", + "\t\n", + "\t\n", + "\t\n", + "\t\n", + "\t\n", + "\t\n", + "\t\n", + "\t\n", + "\t\n", + "\t\n", + "\t\n", + "\t\n", + "\t\n", + "\t\n", + "\t\n", + "\t\n", + "\t\n", + "\n", + "
A tibble: 17 × 3
Model typeCitationLabel
<chr><chr><chr>
Periodicity/harmonicityGill & Purves (2009) gill_09_harmonicity
Periodicity/harmonicityHarrison & Pearce (2018) har_18_harmonicity
Periodicity/harmonicityMilne (2013) milne_13_harmonicity
Periodicity/harmonicityParncutt (1988) parn_88_root_ambig
Periodicity/harmonicityParncutt & Strasburger (1994)parn_94_complex
Periodicity/harmonicityStolzenburg (2015) stolz_15_periodicity
Interference Bowling et al. (2018) bowl_18_min_freq_dist
Interference Huron (1994) huron_94_dyadic
Interference Hutchinson & Knopoff (1978) hutch_78_roughness
Interference Parncutt & Strasburger (1994)parn_94_pure
Interference Sethares (1993) seth_93_roughness
Interference Vassilakis (2001) vass_01_roughness
Interference Wang et al. (2013) wang_13_roughness
Culture Johnson-Laird et al. (2012) jl_12_tonal
Culture Harrison & Pearce (2019) har_19_corpus
Numerosity Parncutt & Strasburger (1994)parn_94_mult
Composite Harrison & Pearce (2019) har_19_composite
\n" + ], + "text/latex": [ + "A tibble: 17 × 3\n", + "\\begin{tabular}{lll}\n", + " Model type & Citation & Label\\\\\n", + " & & \\\\\n", + "\\hline\n", + "\t Periodicity/harmonicity & Gill \\& Purves (2009) & gill\\_09\\_harmonicity \\\\\n", + "\t Periodicity/harmonicity & Harrison \\& Pearce (2018) & har\\_18\\_harmonicity \\\\\n", + "\t Periodicity/harmonicity & Milne (2013) & milne\\_13\\_harmonicity \\\\\n", + "\t Periodicity/harmonicity & Parncutt (1988) & parn\\_88\\_root\\_ambig \\\\\n", + "\t Periodicity/harmonicity & Parncutt \\& Strasburger (1994) & parn\\_94\\_complex \\\\\n", + "\t Periodicity/harmonicity & Stolzenburg (2015) & stolz\\_15\\_periodicity \\\\\n", + "\t Interference & Bowling et al. (2018) & bowl\\_18\\_min\\_freq\\_dist\\\\\n", + "\t Interference & Huron (1994) & huron\\_94\\_dyadic \\\\\n", + "\t Interference & Hutchinson \\& Knopoff (1978) & hutch\\_78\\_roughness \\\\\n", + "\t Interference & Parncutt \\& Strasburger (1994) & parn\\_94\\_pure \\\\\n", + "\t Interference & Sethares (1993) & seth\\_93\\_roughness \\\\\n", + "\t Interference & Vassilakis (2001) & vass\\_01\\_roughness \\\\\n", + "\t Interference & Wang et al. (2013) & wang\\_13\\_roughness \\\\\n", + "\t Culture & Johnson-Laird et al. (2012) & jl\\_12\\_tonal \\\\\n", + "\t Culture & Harrison \\& Pearce (2019) & har\\_19\\_corpus \\\\\n", + "\t Numerosity & Parncutt \\& Strasburger (1994) & parn\\_94\\_mult \\\\\n", + "\t Composite & Harrison \\& Pearce (2019) & har\\_19\\_composite \\\\\n", + "\\end{tabular}\n" + ], + "text/markdown": [ + "\n", + "A tibble: 17 × 3\n", + "\n", + "| Model type <chr> | Citation <chr> | Label <chr> |\n", + "|---|---|---|\n", + "| Periodicity/harmonicity | Gill & Purves (2009) | gill_09_harmonicity |\n", + "| Periodicity/harmonicity | Harrison & Pearce (2018) | har_18_harmonicity |\n", + "| Periodicity/harmonicity | Milne (2013) | milne_13_harmonicity |\n", + "| Periodicity/harmonicity | Parncutt (1988) | parn_88_root_ambig |\n", + "| Periodicity/harmonicity | Parncutt & Strasburger (1994) | parn_94_complex |\n", + "| Periodicity/harmonicity | Stolzenburg (2015) | stolz_15_periodicity |\n", + "| Interference | Bowling et al. (2018) | bowl_18_min_freq_dist |\n", + "| Interference | Huron (1994) | huron_94_dyadic |\n", + "| Interference | Hutchinson & Knopoff (1978) | hutch_78_roughness |\n", + "| Interference | Parncutt & Strasburger (1994) | parn_94_pure |\n", + "| Interference | Sethares (1993) | seth_93_roughness |\n", + "| Interference | Vassilakis (2001) | vass_01_roughness |\n", + "| Interference | Wang et al. (2013) | wang_13_roughness |\n", + "| Culture | Johnson-Laird et al. (2012) | jl_12_tonal |\n", + "| Culture | Harrison & Pearce (2019) | har_19_corpus |\n", + "| Numerosity | Parncutt & Strasburger (1994) | parn_94_mult |\n", + "| Composite | Harrison & Pearce (2019) | har_19_composite |\n", + "\n" + ], + "text/plain": [ + " Model type Citation Label \n", + "1 Periodicity/harmonicity Gill & Purves (2009) gill_09_harmonicity \n", + "2 Periodicity/harmonicity Harrison & Pearce (2018) har_18_harmonicity \n", + "3 Periodicity/harmonicity Milne (2013) milne_13_harmonicity \n", + "4 Periodicity/harmonicity Parncutt (1988) parn_88_root_ambig \n", + "5 Periodicity/harmonicity Parncutt & Strasburger (1994) parn_94_complex \n", + "6 Periodicity/harmonicity Stolzenburg (2015) stolz_15_periodicity \n", + "7 Interference Bowling et al. (2018) bowl_18_min_freq_dist\n", + "8 Interference Huron (1994) huron_94_dyadic \n", + "9 Interference Hutchinson & Knopoff (1978) hutch_78_roughness \n", + "10 Interference Parncutt & Strasburger (1994) parn_94_pure \n", + "11 Interference Sethares (1993) seth_93_roughness \n", + "12 Interference Vassilakis (2001) vass_01_roughness \n", + "13 Interference Wang et al. (2013) wang_13_roughness \n", + "14 Culture Johnson-Laird et al. (2012) jl_12_tonal \n", + "15 Culture Harrison & Pearce (2019) har_19_corpus \n", + "16 Numerosity Parncutt & Strasburger (1994) parn_94_mult \n", + "17 Composite Harrison & Pearce (2019) har_19_composite " + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "incon_models %>%\n", + " select(\"Model type\" = class, \"Citation\" = citation, \"Label\" = label)" + ] + }, + { + "cell_type": "markdown", + "id": "cc801eba-c34b-4440-a3ba-b55ae798b072", + "metadata": {}, + "source": [ + "The below code illustrates basic usage. First we choose a chord to analyse. This chord is specified in MIDI note numbers. If you are not familiar with MIDI note numbers, do have a look at this [information page](https://pmcharrison.github.io/intro-to-music-and-science/pitch.html#pitch-notation). In this example we are analysing a closed C major triad on middle C, comprising the notes C4 (60), E4 (64), and G4 (67).\n", + "\n", + "To run these models on your own chord, simply change the numbers below, then rerun the cell.\n", + "For example, replace `60, 64, 67` with `60, 63, 67` to get a C minor triad (as opposed to a C major triad).\n", + "\n", + "To select different models, refer to the table of implemented models above, then replace the \n", + "model selected in the `incon` function." + ] + }, + { + "cell_type": "code", + "execution_count": 89, + "id": "2a7c610b-ffec-483f-a819-1affb9aa4d09", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\t\n", + "\t\t\n", + "\t\t\n", + "\t\n", + "\t\n", + "\t\t
\n", + " \n", + " \n", + " \n", + " \n", + "
\n", + "

\n", + "
\n", + " \n", + " \n", + "
\n", + "\t\n", + "\n" + ], + "text/plain": [ + "Shiny tags cannot be represented in plain text (need html)" + ] + }, + "metadata": { + "text/html": { + "isolated": true + } + }, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Roughness = 0.120242607949697 (higher values mean less consonance)\n", + "Relative period length = 2 (higher values mean less consonance) \n", + "Corpus scarcity = 0.780243332100067 (higher values mean less consonance) \n" + ] + } + ], + "source": [ + "chord <- c(60, 64, 67)\n", + "\n", + "display_chord(chord)\n", + "\n", + "roughness <- incon(chord, \"hutch_78_roughness\")\n", + "print(glue(\"Roughness = { roughness} (higher values mean less consonance)\"))\n", + "\n", + "period_length <- incon(chord, \"stolz_15_periodicity\")\n", + "print(glue(\"Relative period length = { period_length } (higher values mean less consonance) \"))\n", + "\n", + "corpus_scarcity <- incon(chord, \"har_19_corpus\")\n", + "print(glue(\"Corpus scarcity = { corpus_scarcity } (higher values mean less consonance) \"))" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "R", + "language": "R", + "name": "ir" + }, + "language_info": { + "codemirror_mode": "r", + "file_extension": ".r", + "mimetype": "text/x-r-source", + "name": "R", + "pygments_lexer": "r", + "version": "4.3.2" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From f417e4963686b5abd13dbee8d25ba536e399f9b6 Mon Sep 17 00:00:00 2001 From: Peter Harrison Date: Tue, 30 Jan 2024 17:33:16 +0000 Subject: [PATCH 23/24] Update NEWS --- NEWS.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/NEWS.md b/NEWS.md index 9d5c38f..a2f4768 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,6 @@ +- Consolidated all modelling dependencies into this package, + to avoid having to download so many GitHub repositories on installation. + # incon 0.4.1 - Addressing some minor dependency issues. From 2bba32bd5a02508c4a957dfcb863a901cd226ebc Mon Sep 17 00:00:00 2001 From: Peter Harrison Date: Tue, 30 Jan 2024 17:33:27 +0000 Subject: [PATCH 24/24] Increment version number to 0.5.0 --- DESCRIPTION | 2 +- NEWS.md | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/DESCRIPTION b/DESCRIPTION index 5ea1c63..73d175d 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,7 +1,7 @@ Package: incon Type: Package Title: Computational Models of Simultaneous Consonance -Version: 0.4.1 +Version: 0.5.0 Author: person("Peter", "Harrison", email = "pmc.harrison@gmail.com", role = c("aut", "cre")) Depends: R (>= 3.4.0) Maintainer: Peter Harrison diff --git a/NEWS.md b/NEWS.md index a2f4768..d300aa2 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,5 @@ +# incon 0.5.0 + - Consolidated all modelling dependencies into this package, to avoid having to download so many GitHub repositories on installation.