R/brightness_comparisons.R

Defines functions ratio delta eps brightness_comparisons modecompare

Documented in brightness_comparisons delta eps ratio

#' Relative motions in a voice leading
#'
#' Does the main computation to determine voice-leading brightness comparisons.
#'
#' @inheritParams tnprime
#' @param ref A vector the same length as `set` to serve as the origin of a voice leading.
#' 
#' @returns A value of `-1`, `0`, or `1` whose meaning is the same as the entries of the 
#'   matrix returned by brightness_comparisons().
#'
#' @noRd
modecompare <- function(set, ref, rounder=10) sum(unique(sign(round(set - ref, rounder))))

#' Voice-leading brightness relationships for a scale's modes
#'
#' The essential step in creating the brightness graph of a scale's modes
#' is to compute the pairwise comparisons between all the modes. Which ones are strictly
#' brighter than others according to "voice-leading brightness" (see "Modal Color Theory," 6-7)?
#' This function makes those pairwise comparisons in a manner that's useful for more computation.
#'
#' Note that the returned value shows all voice-leading brightness comparisons, not just 
#' the transitive reduction of those comparisons. (That is, dorian is shown as darker than ionian
#' even though mixolydian intervenes in the brightness graph.)
#'
#' @inheritParams tnprime
#' @inheritParams fpunique
#' @inheritParams sim
#' @returns If `goal=NULL`, an n-by-n matrix where n is the size of the scale. 
#'   Row i represents mode i of the scale
#'   in comparison to all n modes. If the entry in row i, column j is `-1`, then mode i is
#'   "voice-leading darker" than mode j. If `1`, mode i is "voice-leading brighter". If 0, mode i
#'   is neither brighter nor darker, either because contrary motion is involved or because mode i
#'   is identical to mode j. (Entries on the principal diagonal are always 0.)
#'
#'   If `goal` is a set, the result is a 2n-by-2n matrix whose first n rows and columns represent
#'   the modes of `set` and whose last n rows and columns represent the modes of `goal`. (Thus the
#'   upper left n-by-n square is the same as if `goal` were `NULL` and the lower right n-by-n square
#'   is the result of entering `goal` as `set` with an empty goal parameter. The upper-right and 
#'   lower-left quadrants of the matrix make comparisons between the modes of `set` and `goal`.) The
#'   meaning of entries `-1`, `0`, and `1` are as above.
#'
#' @examples
#' # Because the diatonic scale, sc7-35, is non-degenerate well-formed, the only
#' # 0 entries should be on its diagonal.
#' brightness_comparisons(sc(7, 35))
#' 
#' mystic_chord <- sc(6,34)
#' colSums(sim(mystic_chord)) # The sum brightnesses of the mystic chord's 6 modes
#' brightness_comparisons(mystic_chord) 
#' # Almost all 0s because very few mode pairs are comparable.
#' # That's because nearly all modes have the same sum, which means they have sum-brightness
#' # ties, and voice-leading brightness can't break a sum-brightness tie.
#' # (See "Modal Color Theory," 7.)
#' 
#' major <- c(0, 4, 7)
#' minor <- c(0, 3, 7)
#' brightness_comparisons(major, minor)
#'
#' @seealso [brightnessgraph()] for a human-readable presentation of the same information.
#'
#' @export
brightness_comparisons <- function(set, goal=NULL, edo=12, rounder=10) {
  modes <- sim(set, goal=NULL, edo=edo, rounder=rounder)

  if (!is.null(goal)) {
    modes <- cbind(modes, sim(goal, edo=edo, rounder=rounder))
  }

  modes <- split(modes, col(modes))
  outer(modes, modes, Vectorize(modecompare), rounder=rounder)
}

#' The brightness ratio
#'
#' @description
#' Section 3.3 of "Modal Color Theory" describes a "brightness ratio" which characterizes
#' the modes of a scale in terms of how well "sum brightness" acts as a proxy for "voice-leading
#' brightness." Scales with a brightness ratio less than 1 are pretty well behaved from this
#' perspective, while ones with a brightness ratio greater than 1 are poorly behaved. When the
#' brightness ratio is 0, sum brightness and voice-leading brightness give exactly the same
#' results. (This can happen for sets on two extremes: those like the diatonic scale which are
#' well formed and those like the Weitzmann scales, which differ from "white" in only one
#' scale degree.) 
#'
#' I wish I had come up with a more descriptive name than "brightness ratio" for this 
#' property, because it's not really a ratio of brightness in the sense you might expect (i.e.
#' "this scale is 20% bright"). Rather, it's a ratio of two brightness-related properties,
#' `delta` and `eps`. "Modal Color Theory" (p. 20) offers definitions of these. Delta is
#' "the largest sum difference between (voice-leading) incomparable modes," with value 0 by 
#' definition if all of the modes are comparable. ("This, in a sense, is a measure of how badly
#' voice-leading brightness breaks down from the perspective of sum brightness.") **Eps**ilon
#' "represents the smallest sum difference between non-identical but comparable modes."
#' This is harder to give an intuitive gloss on, but my attempt in "MCT" was "Essentially,
#' epsilon measures the finest distinction that voice-leading brightness is capable of
#' parsing."
#' 
#' The brightness ratio (`ratio`) itself is simply delta divided by epsilon.
#'
#' @inheritParams tnprime
#' @inheritParams fpunique
#' @returns Single non-negative numeric value
#'
#' @examples
#' harmonic_minor <- c(0, 2, 3, 5, 7, 8, 11)
#' hypersaturated_harmonic_minor <- saturate(2, harmonic_minor)
#' c(delta(harmonic_minor), eps(harmonic_minor))
#' c(delta(hypersaturated_harmonic_minor), eps(hypersaturated_harmonic_minor))
#' 
#' # Delta and epsilon depend on the precise scale, but ratio() is constant on a hue
#' ratio(harmonic_minor)
#' ratio(hypersaturated_harmonic_minor)
#'
#' #### Sort all 12tet heptachords by brightness ratio
#' heptas12 <- unique(apply(combn(12, 7), 2, primeform),MARGIN=2)
#' hepta_ratios <- apply(heptas12, 2, ratio)
#' sorted_heptas <- heptas12[, order(hepta_ratios)]
#' colnames(sorted_heptas) <- apply(sorted_heptas, 2, fortenum)
#' sorted_heptas
#' 
#' #### Compare evenness to ratio for 12tet hetpachords
#' plot(apply(heptas12, 2, evenness), hepta_ratios, xlab="Evenness", ylab="Brightness Ratio")
#'
#' @export
eps <- function(set, edo=12, rounder=10) {
  if (length(set) < 2) {
    return(NA)
  }

  modes <- t(sim(set, edo=edo))
  comps <- brightness_comparisons(set, edo=edo, rounder=rounder)
  chart <- comps * comps
  diffs <- outer(rowSums(modes), rowSums(modes), '-')
  result <- chart * diffs

  min(result[result > 0])
}

#' @rdname eps
#' @export
delta <- function(set, edo=12, rounder=10) {
  if (length(set) < 2) {
    return(NA)
  }

  modes <- t(sim(set, edo=edo))
  comps <- brightness_comparisons(set, edo=edo, rounder=rounder)
  chart <- comps * comps
  diag(chart) <- -1
  chart <- (chart + 1)%%2

  diffs <- outer(rowSums(modes), rowSums(modes), '-')
  result <- chart * diffs

  max(result)
}

#' @rdname eps
#' @export
ratio <- function(set, edo=12, rounder=10) {
  if (length(set) < 2) {
    return(NA)
  }

  delta(set, edo, rounder)/eps(set, edo, rounder)
}

Try the musicMCT package in your browser

Any scripts or data that you put into this service are public.

musicMCT documentation built on June 21, 2026, 9:06 a.m.