R/Metric.R

Defines functions syncopation.default .metrics .metric metsubpos.default metcount.default metlev.default metric onbeat.default scaled_timeline subpos.default timecount.default nbeats.NULL nbeats.character nbeats.meter nbeats measure.NULL measure.character measure.meter measure tactus.NULL tactus.meter tactus tatum.NULL tatum.token tatum.rational tatum.meter tatum timesignature2meter duple meter.list meter.rational meter.meter meter

Documented in duple measure measure.character measure.meter measure.NULL metcount.default meter meter.list meter.meter meter.rational metlev.default metsubpos.default nbeats nbeats.character nbeats.meter nbeats.NULL onbeat.default subpos.default syncopation.default tactus tactus.meter tactus.NULL tatum tatum.meter tatum.NULL tatum.rational tatum.token timecount.default

#################################### ###
# meter S4 class #######################
#################################### ###


## meter documentation ----


## Definition, validity, initialization ####

#' Musical Meter in humdrumR
#' 
#' `HumdrumR` represents musical meter, internally, using the S4-class `meter`.
#' A `meter` is simply a collection of regular or irregular beat "levels," with each
#' level represented as [musical durations][rhythmFunctions].
#' For example, the meter 4/4 could be represented as the [recip()] beat-levels 
#' `c("1", "2", "4", "8", "16")`---that is, 
#' whole-note, half-note, quater-note, eighth-note, and sixteenth note.
#' In addition, each meter has a *tactus*---the "main" beat level.
#'
#' @details 
#' 
#' Generating meters in `humdrumR` can be done easily with the `meter()` function.
#' If you pass a `character` string of a humdrum time signature, you'll get a meter object:
#' for example, `meter("M4/4")` or `meter("M12/8")`.
#' Additive time signatures, like `meter(M2+3/8)` are also parseable.
#'
#' ## Adding or removing levels
#'
#' Time signatures leave a lot of room for interpretation.
#' Even in "standard" 4/4 time, there are a number of questions you need to consider
#' when analyzing meter:
#' 
#' + What is the fastest level you want to count? 16ths? 32nds? 64ths?
#' + Do you want to count the half-note level, which is "in between" the full measure (whole-note)
#' and the tactus quarter-note?
#' + How do you want to treat triplets or other tuplets? What is a piece uses a lot of 
#'   triplet eighth-notes?
#' + Do you want to consider hypermeter, *above* the measure level?
#' 
#' Fortunately, `humdrumR` and the `meter()` function give you options to precisely specify 
#' metric levels.
#' The most transparent way is to simply pass `meter()` a `list` of duration values, like `meter(list("1", "4", "8"))`.
#' However, `meter()` includes a number of helpful arguments that can be used to quickly streamline the process of defining a meter.
#' To understand these arguments, lets first clarify some metric definitions used in `humdrumR`:
#' 
#' + *Measure*: The duration of the "measure" of the meter. I.e., the highest metric level, or
#' the least common multiple of all levels.
#' + *Hypermeter*: Metric levels above the measure indicated by the time signature.
#' + *Tactus*: The "main," usually central, level.
#' + *Subdivision*: The level directly below the tactus.
#' + *Tick*: The smallest/fastest metric level. (The "ticks" in the grid.)
#' + *Tatum*: The smallest common denominator between a set of beats/duration. 
#'   Note that a fully specified metric grid *should* have the tick and the tatum be identical.
#'   However, we don't require that the tick be the tatum.
#' 
#' ### Meter construction arguments
#'
#' The *measure* (largest) and the *tick* (smallest) levels capture the full range of a meter.
#' The tactus is typically somewhere in the middle between these two, but this is not required.
#' The `fill.levels` argument can be used to 'fill in' levels between the measure, tactus, and tick.
#' This means that if there is room to "fit" (geometrically) duple or triple subdivisions between the higher and lower level,
#' those subdivisions are added to the meter.
#' The `fill.levels` argument must be a single `character` string, [partially matching][partialMatching] either
#' `'above'`, `'below'`, `'both'`, or `'neither'`. 
#' "Above" means fill in any gap between the tactus and the measure;
#' "Below" means fill in any gap between the tactus and the tick.
#' As a shortcut, `logical` `TRUE` or `FALSE` can be used as an alternative way of specifiying
#' `'both'` or `'neither'`, respectively.
#' For example, if you start with levels measure-tactus-tick combination of `c("1", "4", "16")`, `fill.levels = TRUE`,
#' will fill in levels at `"2"` and `"8"`.
#' 
#' The `tick` argument can be used to directly specify a tick for the meter.
#' This is especially useful if you are parsing datasets with multiple meters, but want to force them all to use 
#' the same tick value. For example, `meter(TimeSignature, tick = '32')`.
#' The `tick` argument must be a single value which can be [parsed][rhythmInteval()] as a rhythmic duration.
#' 
#' The `subdiv` argument can be used to explicitly control how the tactus is subdivided.
#' `subdiv` should be a natural number (greater than 1), to divide the tactus by.
#' Similarly, the `hyper` argument is a natural number to explicitly multiply the measure by.
#' Thus, `hyper = 2` adds a two-measure hyper meter.
#' 
#' The `tatum` argument, if `TRUE`, causes the tatum of all other metric levels to be added to the meter (if not already present).
#' This is useful, for example, if you specify a meter with a mix of duple and triple levels and want to make sure the *tick*
#' of the meter is the tatum of the levels.
#' For example, if you have levels `c("4", "6", "8")`, `tatum = TRUE` will add the level `"24"` to the meter.
#' 
#' ### Constructing meters from time signatures
#' 
#' The "meter construction arguments" above can *also* be used when reading time signatures.
#' This allows you to use time signatures from your datasets, while maintaining precise control of the metric levels used.
#' For example, the command `meter("M4/4", fill.levels = FALSE)` generates a meter with levels `c("1", "4", "16")`.
#' We could add an eighth-note level by adding `subdiv = 2`, or triple-eighth-notes with `subdiv = 3`.
#' If we *did* add triplet-eighth-notes (`subdiv = 3`), we might want to add `tatum = TRUE`, which would automatically
#' calculate the common tatum of our levels---in this case, adding forty-eighth notes to the meter.
#' On the opposite end, `hyper = 4` could add a four-measure hyper meter on top of our 4/4 bar.
#' Finally, the `tactus` argument could be used to choose another tactus, like `tactus = "2"`.
#' 
#' Any additional, unnamed arguments to `meter()` will be parsed as durations, and added as levels to the meter.
#' 
#' @param x ***A time signature or list of beats used to construct a meter.***
#' 
#' Must be a `character` string or a `list` of either `numeric` or `character` values.
#' 
#' A `character` input is parsed as a time signature, which is used to extract tactus and measure levels.
#' The contents of `list` are parsed as durations using [rhythmInterval()]: these durations become the 
#' levels of the meter.
#' Failures to parse the input will result in an error.
#' 
#' @param measure,tick,tactus ***Durations to use longest (measure), shortest (tick), or tactus levels.***
#' 
#' Must be a singleton `character` or `numeric` value, or a `list` containing a single vector of such values.
#' 
#' These are parsed as durations using [rhythmInterval()]; parse failures lead to errors.
#' 
#' @param subdiv ***Subdivisions (fractions) of the `tactus` to add to the meter.***
#' 
#' Must be positive natural numbers.
#' 
#' @param hyper ***Multiples of the `measure` duration to add to the meter.***
#' 
#' Must be positive natural numbers.
#' 
#' @param tatum ***Should the least common denominator of all levels be added to the meter?***
#' 
#' Must be a singleton `logical` value: an on/off switch.
#'
#' @param fill.levels ***Should "gaps" between specified levels be added to the meter?***
#' 
#' Must be a singleton `logical` or `character` value; `character` values must be `'above'`, `'below'`, `'both'`, or `'neither'`.
#' 
#' `TRUE` is shorthand for `'both'`; `FALSE` is shorthand for `'neither'`.
#' 
#' 
#'
#' @name meter
#' @family {Metric functions}
#' @export
setClass('meter', contains = 'struct', slots = c(Levels = 'list', Tactus = 'integer'))


setValidity('meter', 
            \(object) {
              levels <- object@Levels
              tactus <- object@Tactus
              errors <- Map(\(ls, ts) {
                if (length(ls) == 1L && is.na(ls)) {
                   return (if (!is.na(ts)) "NA levels must have an NA tactus.")
                  
                }
                spans <- do.call('c', lapply(ls, sum))
                errors <- c(
                  if (any(diff(spans) > rational(0))) "Metric levels must get consecutively shorter.",
                  if (!all(sapply(ls, is.rational))) "All levels of meter object must be rational vectors.",
                  if (any(lengths(ls) < 1L)) "All levels of meter object must have at least length 1.",
                  if (length(ts) > 1L || ts <= 0 || ts > length(ls)) "Invalid tactus."
                )
                
              }, levels, tactus) 
              
              errors <- unlist(errors)
              
              if (length(errors)) errors else TRUE
              
              
              
            })


setMethod('initialize', 
          'meter',
          function(.Object, Levels, Tactus) {
            Tactus <- rep(Tactus, length.out = length(Levels))
            na <- lengths(Levels) == 0L | is.na(Tactus)
            
            Levels[na] <- NA
            Tactus[na] <- NA
            
            .Object <- callNextMethod()
            .Object@Levels <- Levels
            .Object@Tactus <- Tactus
            
            .Object
            
          })

## Constructors ####

#' @rdname meter
#' @export
meter <- function(x, ..., measure, tactus, tick, fill.levels, subdiv, hyper, tatum) UseMethod('meter')
#' @rdname meter
#' @export
meter.meter <- function(x, ...) x
#' @rdname meter
#' @export
meter.rational <- function(x, ..., measure = NULL, tactus = NULL, tick = '16', fill.levels = 'neither', subdiv = NULL, hyper = NULL, tatum = FALSE) {
  levels <- list(x, ...)
  
  meter.list(levels, measure = measure, tactus = tactus, tick = tick, fill.levels = fill.levels, subdiv = subdiv, hyper = hyper, tatum = tatum)
  
}

#' @rdname meter
#' @export
meter.list <- function(x, ..., measure = NULL, tactus = NULL, tick = '16', fill.levels = 'neither', hyper = NULL, subdiv = NULL, tatum = FALSE) {
  checks(fill.levels, xlen1 & (xlogical | xcharacter &  xplegal(c('both', 'above', 'below', 'neither'))))
  if (is.logical(fill.levels)) fill.levels <- c('neither', 'both')[fill.levels + 1L]
  
  levels <- lapply(x, rhythmInterval)
  spans <- lapply(levels, sum)
  
  #
  measure <- if (is.null(measure)) do.call('lcm', spans) else rhythmInterval(measure)
  tactus  <- if (is.null(tactus))  levels[[1]] else rhythmInterval(tactus)
  tick   <-  rhythmInterval(tick)

  #
  if (fill.levels != 'neither') levels <- c(levels,
                                            if (fill.levels %in% c('above', 'both')) as.list(harmonicInterpolate(sum(tactus), measure, ...)),
                                            if (!is.null(tick) & fill.levels %in% c('below', 'both')) as.list(harmonicInterpolate(tick, sum(tactus), ...)))
  
  if (!is.null(hyper)) levels <- c(levels, lapply(hyper, \(n) n * measure))
  if (!is.null(subdiv)) levels <- c(levels, lapply(subdiv, \(n) tactus / n))
  
  
  levels <- c(list(tactus, measure, tick), levels)
  levels <- levels[lengths(levels) > 0L]
  if (tatum) levels <- c(levels, tatum(.unlist(levels)))
  levels <- unique(lapply(levels, `humdrumRattr<-`, value = NULL))
  
  
  ord <- order(.unlist(lapply(levels, sum)),
               -lengths(levels), 
               decreasing = TRUE)
  new('meter', Levels = list(levels[ord]), Tactus = which(ord == 1L))
  
  
}

#' Generate duple meters
#' 
#' This function generates a [meter()] objects representing duple meters.
#' The desired number of duple levels is controlled by the `nlevels` argument.
#' The span of the meter (i.e., the highest level) is indicated by the `measure` argument.
#' Finally, the `tactus` argument indicates which level (indexe from highest to lowest)
#' is the tactus.
#' The default arguments build a 4/4 meter with levels ranging from whole-notes down
#' to sixteenth-notes, and a quarter-note tactus.
#' 
#' @param nlevels ***The number of duple levels.***
#' 
#' Must be a singleton, positive natural number
#' 
#' 
#' @param measure ***The duration of the top level of the meter.***
#' 
#' Must be a singleton `numeric` or `character` value.
#' 
#' Is parsed as a duration by [rhythmInterval()]; a failure to parse leads to an error.

#' @param tactus ***Which level is the tactus?***
#' 
#' Must be a singleton, positive natural number; must be less than or equal to `nlevels`.
#'
#' 
#' @seealso {Use the [meter()] function to create abitrary meters.}
#' @export
duple <- function(nlevels = 4, measure = 1, tactus = 3L) {
  checks(nlevels, xpnatural & xminlength(1))
  checks(tactus, xpnatural)
  
  measure <- rhythmInterval(measure)
  match_size(nlevels = nlevels, measure = measure, tactus = tactus, toEnv = TRUE)
  
  tactus <- pmin(nlevels, tactus)
  
  levels <- Map(\(n, m) lapply(2^((1:n) - 1), \(d) rational(1, d) * m), nlevels, as.list(measure))
  new('meter', Levels = levels,
      Tactus = as.integer(tactus))
  
  
}


## Logic methods ####

setMethod('==', c('meter', 'meter'),
          function(e1, e2) {
            
            unlist(Map(e1@Levels, e2@Levels,
                       f = \(levs1, levs2) {
                         
                         length(levs1) == length(levs2) &&
                           all(.unlist(levs1) == .unlist(levs2))
                       }))
          })

###################################################################### ###
# Deparsing Meter Representations (meter2x) ##############################
###################################################################### ###

meter2timeSignature <- dofunc('x', function(x) {
  unlist(Map(\(ls, ts) {
    if (length(ls) == 0L || is.na(ls[1]) || is.na(ts) ) return(NA_character_)
    
    tactus <- ls[[ts]]
    
    if (length(tactus) == 1L) {
      if (tactus@Numerator == 3L) tactus <- tactus / 3L
      measure <- max(.unlist(lapply(ls, sum)))
      
      numerator <- measure %/% tactus
      denominator <- tactus
    } else {
      denominator <- Reduce('gcd', as.list(tactus))
      numerator <- paste(tactus %/% denominator, collapse = '+')
    }
   
  
    paste0('*M', numerator , '/', rint2recip(denominator))
    
  }, x@Levels, x@Tactus))
})
  
  

###################################################################### ### 
# Parsing Meter Representations (x2meter) ################################
###################################################################### ### 

timesignature2meter <- function(x, ..., sep = '/', compound = TRUE) {
  REparse(x, makeRE.timeSignature(sep = sep, collapse = FALSE),
          toEnv = TRUE)
  numerator <- lapply(strsplit(numerator, split = '\\+'), as.integer)
  denominator <- as.integer(denominator)
  
  baddenom <- !denominator %in% as.integer(2^(0:12))
  if (any(baddenom)) {
    .stop("Time signatures must use denominators which are (small) powers of 2.",
          "Your time signature includes {.values(x[baddenom])}.")
  }
  #
  beats <- Map(rational, numerator, denominator)
  denominator <- as.list(rational(1L, denominator))
  
  unnamed <- list(...)
  unnamed <- list(.names(unnamed) == '')
  levels <- lapply(unnamed, rhythmInterval)
  
  results <- Map(\(den, bts) {
    
    measure <- sum(bts)
    tactus <- if (length(bts) > 1)  bts  else den
    if (compound && 
        all((measure %/% tactus) > 3L) &&
        all(as.integer64(3L) %divides% bts@Numerator) && 
        all(den@Numerator == as.integer64(1L)) && 
        all(as.integer64(8L) %divides% den@Denominator)) {
      # compound meters
      do.call('meter.rational', list(tactus * 3L, tactus, ..., measure = measure))
    } else {
      do.call('meter.rational', list(tactus, ..., measure = measure))
    }
   
   
    # levels <- c(levels, subdiv)
  }, denominator, beats) 

  .unlist(results)
}


## Meter Parsing Dispatch ######################################

#' @rdname meter
#' @export
meter.character <- makeHumdrumDispatcher(list('any', makeRE.timeSignature, timesignature2meter),
                                         funcName = 'meter.character',
                                         outputClass = 'meter')

setMethod('as.character', 'meter', meter2timeSignature)

#### setAs meter ####
setAs('integer', 'meter', \(from) new('meter', 
                                      Levels = as.list(rep(NA, length(from))), 
                                      Tactus = rep(NA_integer_, length(from))))


# Meter extraction tools ----

## Find tatum

#' Find common denominator of beats
#' 
#' In `humdrumR`, we define a *tatum* as the greatest common denominator of 
#' a set of durations.
#' In other words, given a set of durations, the largest duration that divides all the given beats is the tatum---a common unit
#' which can measure any of the other durations
#' 
#' @details 
#' 
#' `tatum()` is a generic function; it can read any input which can be parsed by the [rhythm parser][rhythmInterval].
#' If can also take a [meter()] object or `character` string of the form `"MX/Y"`.
#' 
#' The tatum of a [meter()] is the tatum of all that meters metric levels.
#' If meters *and* durations are provided---like `tatum(c('M4/4', '6')`---, the tatum of all the meters'
#' levels *and* all the durations is computed.
#' 
#' The `deparser` argument is a [rhythm function][rhythmFunction] which controls the output format.
#' If `deparser` is `NULL`, the tatum is returned as a [rational()] value.
#'
#' @param x ***The input to compute the tatum of.***
#' 
#' Must be a [meter()] object, a singleton `character` value, or vector of either `character`, `numeric`, or [rational()] values.
#' 
#' For `character` input, valuest that match the regular expression `"^\*?M"` are parsed as a time signature using [meter()], while other strings are 
#' parsed as durations using [rhythmInterval()]. 
#' `numeric` input is also parsed using [rhythmInterval()]; parse failures result in errors.
#' 
#' @param deparser ***What output format is desired?***
#' 
#' For `character` or `meter` input, the default is [recip()]; for `numeric` input,
#' the default is [duration()].
#'
#' Must be a [rhythm function][rhythmFunction] or `NULL`.
#' 
#' 
#' 
#' @examples 
#' 
#' tatum(c("4", "8", "12"))
#' 
#' tatum(c("M4/4"))
#' 
#' tatum("M4/4", '6')
#' 
#' @family {Metric functions}
#' @export
tatum <- function(x, ...) UseMethod('tatum')
#' @rdname tatum
#' @export
tatum.meter <- function(x, deparser = recip) {
  result <- do.call('c', lapply(x@Levels, \(ls) tatum.rational(do.call('c', ls))))
  
  if (is.null(deparser)) result else deparser(result)
}
#' @rdname tatum
#' @export
tatum.character <- dofunc('x', function(x, deparser = recip) {
  checks(deparser, xinherits('rhythmFunction'))
  
  timesignatures <- grepl('^\\*?M', x)
  rint <- .unlist(c(if (any(timesignatures)) tatum.meter(meter.character(x[timesignatures]), deparser = NULL),
                  if (any(!timesignatures)) rhythmInterval.character(unique(x[!timesignatures]))))
  
  result <- tatum.rational(rint)
  
  if (is.null(deparser)) result else deparser(result)
  
})
    
#' @rdname tatum
#' @export
tatum.numeric <- dofunc('x', function(x, deparser = duration) {  
  checks(deparser, xinherits('rhythmFunction'))
    
  rint <- rhythmInterval(unique(x))
  result <- tatum.rational(rint)
  
  if (is.null(deparser)) result else deparser(result)
})
#' @rdname tatum
#' @export
tatum.rational <- function(x)  do.call('gcd', as.list(unique(x[!is.na(x) & x > rational(0L)])))
#' @rdname tatum
#' @export
tatum.token <- function(x) {
  tatum(x@.Data)
}
#' @rdname tatum
#' @export
tatum.NULL <- function(x) NULL

#' Extract levels from meters
#'
#' These functions take [meter()] objects---or values parseable as meters---and return
#' specific levels from the meter.
#' `tactus()` extracts the tactus of a meter; `measure()` extracts the length of the full measure of a meter.
#' `nbeats()` counts the number of tactus beats in the meter.
#' These functions are particularly useful as arguments to the [timecount and subpos][timecount()] functions.
#' 
#' 
#' @details 
#' 
#' By default, `tactus()` and `measure()` deparse their output as [recip()];
#' an alternative deparser (output format) can be chosen using the `deparser` argument.
#' 
#' @param x ***The input to compute the desired duration from.***
#' 
#' Must be a [meter()] object or a `character` vector.
#' 
#' A `character` input is parsed using [meter()]; failures to parse result in errors.
#' 
#' @param deparser ***What output format is desired?***
#' 
#' The default is [recip()].
#'
#' Must be a [rhythm function][rhythmFunction] or `NULL`.
#'
#' @param sep ***Seperator between irregular beat patterns.***
#' 
#' Defaults to `"+"`.
#' 
#' A singleton `character` value.
#'
#' If the tactus is a pattern of irregular beats, they are pasted together using this separator.
#'
#' 
#' @examples 
#' 
#' tactus("M4/4")
#' 
#' tactus("M6/8")
#' 
#' measure("M4/4")
#' 
#' measure("M6/8")
#' 
#' measure("M6/8", deparser = duration)
#'  
#' @family {Metric functions}
#' @export
tactus <- function(x, deparser, ...) UseMethod('tactus') 
#' @rdname tactus
#' @export
tactus.meter <- function(x, deparser = recip, sep = '+', ...) {
  uniqx <- unique(x)
  
  uniqTacti <- Map('[[', uniqx@Levels, uniqx@Tactus)
  
  if (!is.null(deparser)) uniqTacti <- lapply(uniqTacti, \(x) paste(deparser(x, ...), collapse = sep))
  
  uniqTacti <- .unlist(uniqTacti)
  
  uniqTacti[matches(list(x@Levels, x@Tactus), list(uniqx@Levels, uniqx@Tactus))]

} 
#' @rdname tactus
#' @export
tactus.character <- dofunc('x', function(x, deparser = recip) {
 tactus.meter(meter.character(x), deparser = deparser)
})

#' @rdname tactus
#' @export
tactus.NULL <- function(x) NULL

#' @rdname tactus
#' @export
measure <- function(x, deparser, ...) UseMethod('measure') 
#' @rdname tactus
#' @export
measure.meter <- function(x, deparser = recip) {
  x <- valind(x)

  x$values <- lapply(x$values@Levels, \(ls) max(.unlist(lapply(ls, sum))))

  
  x$values <- .unlist(x$values)
  result <- inverse.valind(x)
  
  if (!is.null(deparser)) deparser(result) else result
}
#' @rdname tactus
#' @export
measure.character <- function(x, deparser = recip) measure.meter(meter(x), deparser = deparser)
#' @rdname tactus
#' @export
measure.NULL <- function(x) NULL

#' Counting beats
#
#' @family {Metric functions}' 
#' @export
nbeats <- function(x) UseMethod('nbeats') 
#' @rdname nbeats
#' @export
nbeats.meter <- function(x) {
  as.integer(measure.meter(x, deparser = NULL) %/% tactus.meter(x, deparser = NULL))
  
}
#' @rdname nbeats
#' @export
nbeats.character <- function(x) unlist(lapply(as.list(meter(x)), nbeats.meter))
#' @rdname nbeats
#' @export
nbeats.NULL <- function(x) NULL

 


###################################################################### ###
# Metric analysis tools ##################################################
###################################################################### ###




## Count ----

#' Count beats or measures
#' 
#' 
#' The `timecount()` function takes a vector of rhythmic duration values and
#' counts (in the musical sense) the number of *beats* (or *measures*) which have occurred since the starting point, 
#' associating each rhythmic onsets with a beat.
#' The `subpos()` function is paired with `timecount()`, computing how far (in rhythmic time) each onset is from its
#' associated beat; if `subpos()` returns `0`, this means that an onset is *on* the beat.
#' Finally, `onbeat()` is simply a convenient shorthand for `subpos() == 0`, returning
#' a `logical` vector for indicating where onsets fall on or off beat.
#' 
#' @details
#' 
#' In many basic use cases, using `timecount()` is essentially the same as using `floor(timeline())`.
#' However, `timecount()` gives us a few additional options which add musicological power compared to [timeline()].
#' (`timecount()` also starts from `1` not `0`, as [timeline()] does.)
#' 
#' The first beat in an input vector is assigned the value of the `start` argument, which defaults to `start = 1L`.
#' There is no 'zeroth' count, as the first beat occurs at the instant of the starting time---i.e., the first onset in the input vector.
#' Every rhythmic onset is associated with one beat, but multiple onsets may occur within the same beat---thus
#' the output of `timecount()` assigns (rounds) each onset to the previous beat onset.
#' However, if `offBeats = FALSE`, only onsets that *land* on a beat are counted, with offbeat values returning `NA`.
#' 
#' The `phase` controls how offbeat onsets are associated with nearby beats.
#' `phase` is [parsed][rhythmParsing] as a rhythmic value and must be rhythmic values that are smaller than the smallest `beat` value.
#' The `phase` argument shifts the "boundary" between beats backwards, before the beat onset.
#' By default, `phase = 0` so the beat-association boundary lands on the beat: only onsets on or after each beat "belong" to that beat.
#' If `phase = '8'`, the beat boundary is pushed back to capture one eighth-note *before* the beat itself.
#' This can be used to, for example, associate the last 3/8s of a measure with the next measure (like pick ups);
#' This could be achieved with a command like `timecount(dur, beat = '1', phase = 3/8)`.
#' 
#'
#' 
#' @section "Beats":
#' 
#' The `unit` argument is used to indicate what size of beat you want to count.
#' The default `unit` is a whole note, equivalent to a measure of `M4/4` time.
#' The `unit` argument uses the [rhythm parser][rhythmInterval()], so it can understand unit values input in a variety of formats:
#' thus, you could specify quarter-note units as either `unit = '4'` or `unit = 0.25`.
#' The parser also understands how to parse the (full) duration of time signature: for example, `unit = 'M3/4'` would use a dotted-half-note unit (`'2.'`).
#' 
#' ### Changing meter
#' 
#' If your data has changing meters (either between pieces, or within pieces), you can specify
#' the `unit` argument as a vector which is the same length as `dur`, indicating the
#' beat size at each moment/index. 
#' This feature is very easy to use with any dataset that includes time signature interpretations, like `"*M4/4"`;
#' these interpetations, if present, are automatically [read into][readHumdrum()] a field called `TimeSignature`.
#' For such a dataset, you can simply pass the `TimeSignature` field to the `unit` argument of `timecount()`, and 
#' the measures of the piece will be correctly counted (even when changing!): `timecount(x, unit = TimeSignature)`.
#' Alternatively, you can use the [tactus()] command to extract the tactus beat from a time signature, like `timecount(x, unit = tactus(TimeSignature))`.
#' 
#' ### Irregular meter
#' 
#' Some musical meters consist of a pattern of irregular beats.
#' For example, the meter `M7/8` is often realized as two "short" beats (two eigth-notes each) and one "long" beat (three eigth-notes), forming a 2 + 2 + 3 pattern.
#' If we want to count each eighth-note, we can simply specify `unit = '8'` and get the `M7/8` beats counted as c(`1`, `3`, `5`).
#' However, if we want to count each short *or* long beat as a single unit, we must specify the desired pattern as a `list` of beat durations: for example, `unit = list(c("4", "4", "4."))`.
#' Let's see what these two cases look like, applied to two `M7/8` measures of straight eighth-notes:
#' 
#' ```
#' rhythm <- rep('8', 14)
#' 
#' timecount(rhythm, unit = '8'),
#' 
#' # output is: 1  2  3  4  5  6  7  8  9 10 11 12 13 14
#' 
#' timecount(rhythm, unit = list(c('4', '4', '4.')))
#' 
#' # output is: 1 1 2 2 3 3 3 4 4 5 5 6 6 6
#' ```
#'
#' To accommodate changing meters, the `unit` argument can still accept `list` of such patterns, so long as the list is the same length as `dur`.
#'
#' @section Pickups:
#' 
#' Another option is to pass the `pickup` argument a logical vector of the same length as the input `dur`.
#' Within each piece/group, any block of `TRUE` values at the *beginning* of the `pickup` vector 
#' indicate a pickup.
#' The *first* index where the `pickup` logical is `FALSE` is used as the location of beat `1`:
#' all the earlier (`pickup == TRUE`) points will be negative counts, counting backwards from the start.
#' In `humdrumR`, and datapoints before the first barline record (`=`) are labeled `Bar == 0` in the `Bar` [field][fields()].
#' Thus, a common use for the `pickup` argument is `within(humData, timecount(Token, pickup = Bar < 1)`, which makes the downbeat of
#' the first complete bar `1` the stating point---any notes in pickup bars are give negative counts.
#' 
#' **Note that there is never a 'beat zero'.**
#' Beats before the starting point progress directly from `-1` to `1` (the start).
#' As a result, doing arithmetic or other math with beat "counts" can be problematic when using a `pickup` argument.
#' It may be better to use `round(timeline())` in cases where you want to do much math with counts.
#'
#' @param dur ***An input vector of rhythmic durations.***
#'            
#' Must be a `character` or `numeric` vector.
#'      
#' Is parsed using [rhythmInterval()]; 
#' Wherever the input can't be parsed as a duration, 
#' that element is treated as a duration of zero.
#'             
#' @param unit ***The size of "beat" (or measure) to count.***
#' 
#' Defaults to a whole-note (one measure of 4/4 time).
#' 
#' Must be a `character` or `numeric` vector, or a list of such vectors; 
#' must be a singleton or the same length as `dur`.
#' 
#' Is parsed as a duration using [rhythmInterval()]; 
#' If the input can't be parsed as a duration, the output will be all `NA`.
#'
#' @param start ***The number to start counting from.***
#' 
#' Must be a single whole-number value (either `numeric` or `integer`).
#'
#' @param phase ***The phase offset between onsets and beats.***
#' 
#' Defaults to `0`.
#'
#' Must be a `character` or `numeric` vector; must be length `1` or the same length as `dur`;
#' The duration of `phase` must be smaller than the smallest duration value in `unit`.
#' 
#' Is parsed as a duration using [rhythmInterval()]; 
#' If the input can't be parsed as a duration, an error occurs.
#' 
#' @param pickup ***Indicates which leading values in the input are pickups, if any.***
#' 
#' Defaults to `NULL`.
#' 
#' Must be `NULL`, or a `logical` vector of the same length as `dur`.
#'
#' @param offBeats ***Should off-beat onsets be numbered in the output, or `NA`?***
#' 
#' Defaults to `TRUE`.
#' 
#' Must be a single `logical` value: an on/off switch.
#'
#' @param groupby ***Optional vectors to group by and count within.***
#' 
#' Defaults to empty `list()`.
#'
#' Must be a [list()], which is either empty or contains vectors which are all the same length as `dur`.
#' To function as a by-record timeline, the `groupby` list must include a *named* `Piece` and `Record` vectors.
#' Luckily, these are automatically passed by [with(in).humdrumR][withinHumdrum], so you won't need to worry about it!
#'   
#' @examples 
#' 
#' humData <- readHumdrum(humdrumRroot, "HumdrumData/BachChorales/chor00[1-4].krn")
#' 
#' show(within(humData, timecount(Token, unit = TimeSignature, pickup = Bar < 1)))
#' 
#' show(within(humData, timecount(Token, unit = tactus(TimeSignature))))
#'  
#'   
#' @seealso {`timecount()` and `subpos()` are closely related to the [timeline()] function. The [metcount()] function applies `timecount()` within a metric framework.}
#' @name timecount
#' @export
timecount.default <- function(dur, unit = rational(1L), start = 1L, phase = 0,  pickup = NULL, offBeats = TRUE,  groupby = list(), ...) {
  
  checks(dur, xcharacter | xnumber)
  checks(start, (xnumber & xlen1 & (xnotzero + "The 'first' beat to count occurs at the starting instant, so there is no 'zeroth' beat" )))
  checks(pickup, xnull | (xlogical & xmatch(dur)), seealso = c("?timecount", 'the rhythm vignette'))
  checks(phase, (xnumeric | xcharacter) & (xlen1 | xmatch(dur)))
  checks(offBeats, xTF)
  
  
  scaled <- scaled_timeline(dur, unit, rational(0L), pickup, groupby, callname = 'timecount', ...)
  
  # phase
  phase_rint <- rhythmInterval(phase) 
  checks(phase_rint, argname = 'phase',
         argCheck(\(arg) all(arg < min(.unlist(scaled$values))), "must be smaller than all units in the 'unit' argument'", 
                  \(arg) paste0(.show_values(rep(phase, length(scaled$Scale))[arg >= min(.unlist(scaled$values))]), " but 'unit' includes ", .show_vector(unique(unit)))))
  
  phase_rint <- phase_rint / scaled$Scale
  #
  mcount <- as.integer(numerator(floor((scaled$Timeline + phase_rint))))
  
  if (any(scaled$Irregular)) {
    subcounts <- Map(scaled$values, 
                     as.list(scaled$tatum),
                     f = \(bs, tat) cumsum(as.integer(numerator(bs / tat))))
    
    segments <- segments(scaled$indices)
    mcounts <- split(mcount, segments)
    phases <- split(as.numeric(phase_rint), segments)
    
    mcounts <- Map(mcounts, 
                   subcounts[attr(segments, 'values')],
                   phases,
                   f = \(m, sc, ph) {
                     m <- m - min(m)
                     m <- m + ph
                     
                     if (length(sc) == 1L) return(m)
                     
                     sub <- m %% max(sc)
                     
                     if (!offBeats) sub[!sub %in% c(0, sc)] <- NA
                     
                     sub <- findInterval(sub, sc, rightmost.closed = TRUE, left.open = FALSE)
                     
                     (length(sc) * (m %/% max(sc))) + sub
                   })
    
    mcount <- unlist(mcounts, use.names = FALSE)
    
    mcount <- delta.default(mcount)
    mcount[mcount < 0L] <- 1L
    mcount <- as.integer(sigma(mcount))
  }
  
  
  if (!offBeats) mcount[!scaled$Irregular & !(rational(1) %divides% (scaled$Timeline + 1))] <- NA
  
  # start at 1 (skip 0)
  mcount <- mcount + as.integer(start - (start > 0L))
  mcount[!is.na(mcount) & mcount >= 0L] <- mcount[!is.na(mcount) & mcount >= 0L] + 1L
  
  
  mcount
}

#' @export
timecount.humdrumR <- humdrumRmethod(timecount.default)
#' @rdname timecount
#' @export
timecount <- humdrumRgeneric(timecount.default)


#' @rdname timecount
#' @export
subpos.default <- function(dur, unit = rational(1L), phase = 0, pickup = NULL, deparser = duration, ..., groupby = list()) {
  
  checks(dur, xcharacter | xnumber)
  checks(pickup, xnull | (xlogical & xmatch(dur)), seealso = c("?subpos", 'the rhythm vignette'))
  checks(phase, (xnumeric | xcharacter) & (xlen1 | xmatch(dur)))
  checks(deparser, xinherits('rhythmFunction'))
  
  scaled <- scaled_timeline(dur, unit, rational(0L), pickup, groupby, callname = 'subpos', sumBeats = TRUE)
  
  # phase
  phase_rint <- rhythmInterval(phase) 
  checks(phase_rint, argname = 'phase',
         argCheck(\(arg) all(arg < min(.unlist(scaled$values))), "must be smaller than all units in the 'unit' argument'", 
                  \(arg) paste0(.show_values(rep(phase, length(scaled$Scale))[arg >= min(.unlist(scaled$values))]), " but 'unit' includes ", .show_vector(unique(unit)))))
  phase_rint <- phase_rint / scaled$Scale
  
  
  timeline <- (((scaled$Timeline + phase_rint) %% rational(1)) - phase_rint) * scaled$Scale
  
  if (any(scaled$Irregular)) {
    irregular <- scaled$Irregular
    indices <- scaled$indices
    irregTimeline <- Map(split(timeline[irregular], indices[irregular]),
                         scaled$values[unique(indices[irregular])], 
                         split(phase_rint[irregular] * scaled$Scale, indices[irregular]),
                         f =  \(tl, bts, ph) {
                           
                           subcount <- as.numeric(tl + ph)
                           btcounts <- cumsum(as.numeric(bts ))
                           bts <- cumsum(c(rational(0L), bts))[findInterval(subcount, btcounts, rightmost.closed = FALSE, left.open = FALSE) + 1L]
                           tl - bts
                         })
    
    timeline[unlist(split(seq_along(indices)[irregular], indices[irregular]))] <- .unlist(irregTimeline)
  }
  
  if (is.null(deparser)) timeline else deparser(timeline, ...)
}
#' @export
subpos.humdrumR <- humdrumRmethod(subpos.default)
#' @rdname timecount
#' @export
subpos <- humdrumRgeneric(subpos.default)

scaled_timeline <- function(dur, unit, start, pickup, groupby, callname, sumBeats = FALSE, ...) {
  dur <- rhythmInterval(dur, ...)
  
  checks(unit, (xatomic | xinherits(c('list', 'rational'))) & (xlen1 | xmatch(dur)))
  
  if (is.list(unit)) {
    unit <- rep(unit, length.out = length(dur))
    uniqueBeats <- valind(unit)
    
    uniqueBeats$values <- lapply(uniqueBeats$values, rhythmInterval)
    
    irregular <- (lengths(uniqueBeats$values) > 1L)[uniqueBeats$i]
    
    tatum <- .unlist(lapply(uniqueBeats$values, if(sumBeats) sum else tatum.rational))
    
    unit <- tatum[uniqueBeats$i]
    
  } else {
    irregular <- logical(length(dur))
    unit <- rhythmInterval(unit)
    uniqueBeats <- list(values = unit)
  }
  
  
  dur <- dur / unit
  
  timeline <- pathSigma(dur, groupby = groupby, start = start, pickup = pickup, callname = 'timecount')
  
  c(list(Timeline = timeline, Scale = unit, Irregular = irregular, tatum = tatum), uniqueBeats)
}


#' @rdname timecount
#' @export
onbeat.default <- function(dur, unit = rational(1L), groupby = list(), ...) {
  subpos(dur, unit = unit, groupby = groupby, deparser = NULL) == rational(0L)
}

#' @export
onbeat.humdrumR <- humdrumRmethod(onbeat.default)
#' @rdname timecount
#' @export
onbeat <- humdrumRgeneric(onbeat.default)


## Meter ####

#' Tools for analyzing rhythm and meter.
#' 
#' [humdrumR] includes a number of useful
#' functions for working with rhythms and meter.
#'
#' 
#' 
#' @name humMeter
NULL


metric <- function(dur, meter = duple(5), start = rational(0), value = TRUE, offBeats = TRUE, numeric = FALSE, deparser = recip, 
                   groupby = list(), ..., parseArgs = list(), remainderSubdivides = TRUE) {
  
  .metric(dur, meter = meter, start = start, groupby = groupby, 
          offBeats = offBeats, parseArgs = parseArgs, remainderSubdivides = remainderSubdivides, ...,
          callname = 'metric')
  
  
}

#' Count or measure metric position
#' 
#' These functions take vectors of rhythmic duration values and
#' compute the metric position of each rhythmic onset.
#' `metlev()` identifies the metric *level* of each onset;
#' `metcount()` counts beats within a measure;
#' `metsubpos()` measures the distance
#' between an onset and the nearest metric beat.
#' `metcount()` and `metsubpos()` parallel the more general `timecount()` and `subpos()` functions.
#' 
#' @details 
#' 
#' Watch out! These `met...()` functions require *meter* information and their output is
#' highly dependent on how you interpret meter from scores.
#' For a full discussion of how meter can represented, parsed, and created in `humdrumR`,
#' see the [meter()] manual.
#' Effective use of the [meter()] function is essential to use of `metlev()`, `metcount()`, and `metsubpos()`.
#' 
#' 
#' ## Metric levels
#' 
#' `metlev()` identifies the "highest" (longest) metric level that each onset lands in/on:
#' For example, in 4/4 time:
#' 
#' + An onset on the downbeat is at the highest level of all, the whole-note level;
#' + An onset on beat three of the 4/4 measure is on the half-note level;
#' + Onsets on the backbeats (beats two and two) fall on the quarter-note level; 
#' + The next level down is the eighth-note level, in between each quarter-note beat;
#' + etc.
#' 
#' The `metlev()` output expresses beat levels as the duration of
#' the the level, in [recip()] format by default.
#' So the whole-note level is `"1"` and the quarter-note level (backbeats) is `"4"`.
#' You can specify a different [deparsing][rhythmDeparsing] function 
#' (like `duration()`) using the `deparser` argument.
#' (If `deparser` is `NULL`, [rational()] numbers are returned.)
#' 
#' Another option is to express the metric levels simply as natural numbers, which you can achieve
#' with the argument `value = FALSE`.
#' In this case, the top level of the meter is `1`, which each next lower-level incrementing up:
#' i.e., the quarter-note level (of 4/4) would be `3`, while the sixteenth-note level would be `5`.
#' 
#' 
#' #### (Full) 4/4 meter levels
#' 
#' |                          | 1      | &      | 2      | &      | 3      | &      | 4      | &      | 
#' | ------------------------ | ------ | ------ | ------ | ------ | ------ | ------ | ------ | ------ | 
#' | Level ([recip()])        | `"1"`  | `"8"`  | `"4"`  | `"8"`  | `"2"`  | `"8"`  | `"4"`  | `"8"`  | 
#' | Level (`value = FALSE`)  | `1`    | `4`    | `3`    | `4`    | `2`    | `4`    | `3`    | `4`    | 
#' 
#' #### 3/4 meter levels
#' 
#' |                          | 1      | &      | 2      | &      | 3      | &      | 
#' | ------------------------ | ------ | ------ | ------ | ------ | ------ | ------ | 
#' | Level ([recip()])        | `"1"`  | `"8"`  | `"4"`  | `"8"`  | `"4"`  | `"8"`  | 
#' | Level (`value = FALSE`)  | `1`    | `3`    | `2`    | `3`    | `2`    | `3`    | 
#' 
#' #### 6/8 meter levels
#' 
#' |                          | 1      | &      | a      | 2      | &      | a      | 
#' | ------------------------ | ------ | ------ | ------ | ------ | ------ | ------ | 
#' | Level ([recip()])        | `"1"`  | `"8"`  | `"8"`  | `"4."` | `"8"`  | `"8"`  | 
#' | Level (`value = FALSE`)  | `1`    | `3`    | `3`    | `2`    | `3`    | `3`    |  
#' 
#' 
#' ## Metric counts
#' 
#' The `metcount()` function counts one beat level in a metric hierarchy within the span of highest ('measure') level (by default).
#' Which level you want to count is controlled by the `level` argument, which can be either a `character` string
#' in [recip()] format or a natural number (`1` is top level, `2` is next lowest level, etc.).
#' If you tell `metcount()` to count the highest (measure) level in the meter, it will count bars.
#'
#' An additional option is to count beats within the next *highest* beat (which is not necessarily the full bar).
#' For example, if we count eighth-notes in a 6/8 meter, the default behavior is to count 1, 2, 3, 4, 5, 6.
#' However, if we specify `withinNext = TRUE`, the eighth-notes will be counted within the next highest 
#' 6/8 beat level, which is the dotted-quarter note. So the eighth notes in a bar would be counted 1, 2, 3, 1, 2, 3.
#' The exact counting behavior can then be further controlled by changing exactly how the specifying [meter()] is specified.
#' 
#' #### (Full) 4/4 meter counts:
#'
#' If `withinNext = FALSE`:
#' 
#' |                  | 1   | &   | 2   | &   | 3   | &   | 4   | &   |
#' | ---------------- | --- | --- | --- | --- | --- | --- | --- | --- |
#' | `"1"` (whole)    | 1   | 1   | 1   | 1   | 1   | 1   | 1   | 1   |
#' | `"2"` (half)     | 1   | 1   | 1   | 1   | 2   | 2   | 2   | 2   | 
#' | `"4"` (quarter)  | 1   | 1   | 2   | 2   | 3   | 3   | 4   | 4   |
#' | `"8"` (eighth)   | 1   | 2   | 3   | 4   | 5   | 6   | 7   | 8   | 
#'
#' If `withinNext = TRUE`:
#'
#' |                  | 1   | &   | 2   | &   | 3   | &   | 4   | &   |
#' | ---------------- | --- | --- | --- | --- | --- | --- | --- | --- |
#' | `"1"` (whole)    | 1   | 1   | 1   | 1   | 1   | 1   | 1   | 1   |
#' | `"2"` (half)     | 1   | 1   | 1   | 1   | 2   | 2   | 2   | 2   | 
#' | `"4"` (quarter)  | 1   | 1   | 2   | 2   | 1   | 1   | 2   | 2   |
#' | `"8"` (eighth)   | 1   | 2   | 1   | 2   | 1   | 2   | 1   | 2   | 
#' 
#' #### 3/4 meter counts:
#' 
#' If `withinNext = FALSE`:
#'
#' |                       | 1   | &   | 2   | &   | 3   | &   |
#' | --------------------- | --- | --- | --- | --- | --- | --- |
#' | `"2."` (dotted-half)  | 1   | 1   | 1   | 1   | 1   | 1   | 
#' | `"4"` (quarter)       | 1   | 1   | 2   | 2   | 3   | 3   |
#' | `"8"` (eighth)        | 1   | 2   | 3   | 4   | 5   | 6   | 
#' 
#' If `withinNext = TRUE`:
#'
#' |                       | 1   | &   | 2   | &   | 3   | &   |
#' | --------------------- | --- | --- | --- | --- | --- | --- |
#' | `"2."` (dotted-half)  | 1   | 1   | 1   | 1   | 1   | 1   | 
#' | `"4"` (quarter)       | 1   | 1   | 2   | 2   | 3   | 3   |
#' | `"8"` (eighth)        | 1   | 2   | 1   | 2   | 1   | 2   | 
#' 
#'
#' #### 6/8 meter counts:
#' 
#' 
#' If `withinNext = FALSE`:
#'
#' |                         | 1   | &   | a   | 2   | &   | a   |
#' | ----------------------- | --- | --- | --- | --- | --- | --- |
#' | `"2."` (dotted-half)    | 1   | 1   | 1   | 1   | 1   | 1   | 
#' | `"4."` (dotted-quarter) | 1   | 1   | 1   | 2   | 2   | 2   |
#' | `"8"` (eighth)          | 1   | 2   | 3   | 4   | 5   | 6   | 
#' 
#' 
#' If `withinNext = TRUE`:
#'
#' |                         | 1   | &   | a   | 2   | &   | a   |
#' | ----------------------- | --- | --- | --- | --- | --- | --- |
#' | `"2."` (dotted-half)    | 1   | 1   | 1   | 1   | 1   | 1   | 
#' | `"4."` (dotted-quarter) | 1   | 1   | 1   | 2   | 2   | 2   |
#' | `"8"` (eighth)          | 1   | 2   | 3   | 1   | 2   | 3   | 
#' 
#' 
#' #### 4/4 meter with no half-note level:
#' 
#' |                  | 1   | &   | 2   | &   | 3   | &   | 4   | &   |
#' | ---------------- | --- | --- | --- | --- | --- | --- | --- | --- |
#' | `"1"` (whole)    | 1   | 1   | 1   | 1   | 1   | 1   | 1   | 1   |
#' | `"4"` (quarter)  | 1   | 1   | 2   | 2   | 3   | 3   | 4   | 4   |
#' | `"8"` (eighth)   | 1   | 2   | 1   | 2   | 1   | 2   | 1   | 2   | 
#' 
#' You can do this with `meter('M4/4', fill.levels = 'below')`.
#' 
#' ## Metric subpositions
#' 
#' In some cases, onsets may occur which do not land on any beat specified in the meter.
#' This could be very fast beat levels (e.g., 32nd notes), triplets, or other tuplets.
#' In these cases, you might consider adding these levels to the [meter()]; for example,
#' if you want to have a 32nd-note level in 4/4, you could use `meter('M4/4', tick = '32')`.
#' For `metlev()` and `metcount()`, the `offBeats` argument can be set to `FALSE` to cause 
#' offbeat onsets to return `NA`.
#' Another option is to use `metsubpos()`, which measures how far an onset is from the nearest
#' associated beat in the meter.
#' 
#' By default, off-beat onsets are always associated with the closets previous position in any level in the meter.
#' If the `remainderSubdivides` argument is `TRUE`, off-beat onsets are associated with the previous metric level
#' which the subposition makes an even subdivision of.
#' 
#' @param dur ***An input vector of rhythmic durations.***
#'
#' Must be a `character` or `numeric` vector.
#'      
#' Is parsed using [rhythmInterval()]; 
#' Wherever the input can't be parsed as a duration, 
#' that element is treated as a duration of zero.
#' 
#' @inheritParams timeline 
#' @inheritParams timecount
#' 
#' @param meter ***The meter(s) to compute levels from.***
#' 
#' Defaults to a standard, five-level duple (4/4) meter.
#' 
#' Must be a [meter()] object or a `character` vector.
#' 
#' For `character` input, the string is parsed using [meter()]; a
#' failure to parse will result in an error.
#' 
#' @param value ***Should the output levels be represented as rhythmic duration values?***
#' 
#' Defaults to `TRUE`.
#' 
#' Must be a singleton `logical` value: an on/off switch.
#' 
#' @param level ***Which metric level should be counted?***
#' 
#' Defaults to the tactus of the `meter`.
#' 
#' A single `character` value or positive natural number.
#' 
#' A `character` string input must be a [recip()] value, matching a beat level in the meter.
#' A numeric input directly indicates a level in the meter, starting from the highest level (`1`).
#' 
#' @param withinNext ***Should beats be counted within the next highest beat of the meter?***
#' 
#' Defaults to `FALSE`.
#' 
#' Must be a singleton `logical` value: an on/off switch.
#' 
#' @param remainderSubdivides ***Should off-beat onsets only be associated with beat levels that they evenly subdivide?***
#'
#' Defaults to `FALSE`.
#' 
#' A singleton `logical` value: an on/off switch.
#' 
#' @inheritSection timeline Pickups
#'
#' @examples
#' 
#' rhythm <- c('4', '8', '8', '4', '8', '16', '16','4.', '8','2')
#' 
#' metlev(rhythm)
#' metlev(rhythm, meter = 'M6/8')
#'
#' metcount(rhythm)
#' metcount(rhythm, offBeats = FALSE)
#' metcount(rhythm, meter = 'M6/8', offBeats = FALSE)
#' 
#' # chorales
#' chorales <- readHumdrum(humdrumRroot, 'HumdrumData/BachChorales/.*krn') 
#' 
#' within(chorales, metlev(Token, pickup = Bar < 1))
#' 
#' within(chorales, metcount(Token, pickup = Bar < 1, fill.levels = 'below'))
#'
#' @seealso {The [timecount()] and [subpos()] functions are more basic versions of `metcount()` and `metsubpos()`,
#' based only on counting a *single* beat level, rather then a hierarchy of beat levels.}
#' @name metlev
#' @export
metlev.default <- function(dur, meter = duple(5), pickup = NULL, value = TRUE, offBeats = TRUE, remainderSubdivides = FALSE, deparser = recip, 
                           groupby = list(), ..., parseArgs = list()) {
  
  checks(dur, xcharacter | xnumber)
  checks(pickup, xnull | (xlogical & xmatch(dur)))
  checks(offBeats, xTF)
  checks(value, xTF)
  checks(remainderSubdivides, xTF)
  checks(deparser, xnull | xinherits('rhythmFunction'))
  
  met <- .metric(dur = dur, meter = meter, pickup = pickup, groupby = groupby, parseArgs = parseArgs, 
                 remainderSubdivides = remainderSubdivides, callname = 'metlev', ...)
  
  
  metlev <- met$MetLev
  
  if (value) {
    metlev <- if (is.null(deparser)) {
      .unlist(lapply(met$Levels, sum))[metlev]
      
    } else {
      sapply(met$Levels, 
                       \(lev) {
                         output <- deparser(lev)
                         paste(output, collapse = '+')
                         
                       })[metlev]
    } 
  }
  
  
  if (!offBeats) metlev[met$Remainder != rational(0L)] <- as(NA, class(metlev))
  
  metlev
  
}

#' @export
metlev.humdrumR <- humdrumRmethod(metlev.default)
#' @rdname metlev
#' @export
metlev <- humdrumRgeneric(metlev.default)

#' @rdname metlev
#' @export
metcount.default <- function(dur, meter = duple(5), level = tactus(meter), pickup = NULL,  ...,
                             offBeats = TRUE, withinNext = FALSE, remainderSubdivides = FALSE, groupby = list(), parseArgs = list(), Exclusive = NULL) {
  
  checks(dur, xcharacter | xnumber)
  checks(offBeats, xTF)
  checks(remainderSubdivides, xTF)
  checks(pickup, xnull | (xlogical & xmatch(dur)), seealso = c("?metcount", 'the rhythm vignette'))
  
  met <- .metric(dur = dur, meter = meter, pickup = pickup, groupby = groupby, parseArgs = parseArgs, Exclusive = Exclusive, 
                 parentSelecter = if (withinNext) max else min, remainderSubdivides = remainderSubdivides, callname = 'metcount', ...)
  
  counts <- met$Counts
  
  if (is.character(level) && any(!is.na(level) & !level %in% colnames(counts))) {
    .stop("In your call to metcount(), {harvard(unique(level[!level %in% colnames(counts)]), 'and', quote = TRUE)}",
          "<are not names of metric levels|is not a name of a metric level>",
          "in the input meter.",
          "These levels as {harvard(colnames(counts), 'and', quote = TRUE)}.",
          ifelse = length(level[!level %in% colnames(counts)]) == 1L)
  }
  
  if (is.numeric(level) && any(level < 1 || level > ncol(counts))) {
    .stop("In your call to metcount(), {harvard(unique(level[level < 1 || level > ncol(counts)]), 'and')}",
          "<are not valid metric levels|is not a valid metric level>",
          "as, the in the input meter only has {num2word(ncol(counts))} levels.",
          ifelse = length(level[level < 1 || level > ncol(counts)]) == 1L)
  }
  

  if (is.character(level)) {
    level <- match(level, colnames(counts))
  }
  
  mcount <- if (length(level) == 1L) {
    counts[, level]
  }  else {
    
    counts[cbind(seq_len(nrow(counts)), rep(level, length.out = nrow(counts)))]
  }
  
  
  if (!offBeats) mcount[!met$OnBeat[cbind(seq_len(nrow(counts)), rep(level, length.out = nrow(counts)))]] <- NA_integer_
  
  mcount
    
}
#' @export
metcount.humdrumR <- humdrumRmethod(metcount.default)
#' @rdname metlev
#' @export
metcount <- humdrumRgeneric(metcount.default)

#' @rdname metlev
#' @export
metsubpos.default <- function(dur, meter = duple(5), pickup = NULL, deparser = duration, ...,
                              remainderSubdivides = TRUE, groupby = list(), parseArgs = list()) {
  
  checks(dur, xcharacter | xnumber)
  checks(pickup, xnull | (xlogical & xmatch(dur)))
  checks(remainderSubdivides, xTF)
  
  met <- .metric(dur = dur, meter = meter, pickup = pickup, groupby = groupby, parseArgs = parseArgs, 
                 remainderSubdivides = remainderSubdivides, callname = 'metsubpos', ...)
  
  if (is.null(deparser)) met$Remainder else deparser(met$Remainder, ...)
}
#' @export
metsubpos.humdrumR <- humdrumRmethod(metsubpos.default)
#' @rdname metlev
#' @export
metsubpos <- humdrumRgeneric(metsubpos.default)


.metric <- function(dur, meter = duple(5),  groupby = list(), pickup = NULL, ..., 
                    parentSelecter = max, parseArgs = list(), Exclusive = NULL, remainderSubdivides = TRUE, callname = '.metric') {
  
  if (length(unique(meter)) > 1L) {
    return(.metrics(dur, meter = meter, pickup = pickup, parentSelecter = parentSelecter,
                    groupby = groupby, parseArgs = parseArgs, Exclusive = Exclusive, remainderSubdivides = remainderSubdivides,
                    callname = callname, ...))
  }
  
  dur <- do.call('rhythmInterval', c(list(dur, Exclusive = Exclusive), parseArgs))
  meter <- meter(meter, ...)
  
  
  timeline <- pathSigma(dur, groupby = groupby, pickup = pickup, start = rational(0), callname = callname)
  
  levels <- meter@Levels[[1]]
  spans <- .unlist(lapply(levels, sum))
  nbeats <- lengths(levels)
  
  counts <- do.call('cbind', lapply(lapply(levels, \(l) if (length(l) > 1) list(l) else l), 
                                    timecount, 
                                    pickup = pickup, dur = dur, groupby = groupby))
  counts[!is.na(counts) & counts >= 1L] <- counts[!is.na(counts) & counts >= 1L] - 1L
  
  rounded_timelines <- lapply(seq_along(spans), \(i) spans[i] * counts[,i])
  remainders <- do.call('cbind', lapply(rounded_timelines, \(rt) timeline - rt))
  
  ## get counts
  parents <- unlist(Map(as.list(spans),  
                        seq_along(spans),
                        f = \(spn, i) {
                          hits <- seq_along(spans) < i & 
                            spn <= spans & 
                            spn %divides% spans &
                            !(nbeats[i] > 1 & nbeats > 1)
                          if (any(hits)) parentSelecter(which(hits)) else 0L
                        }))
  
  counts <- do.call('cbind', 
                    Map(parents, seq_along(spans),
                        f = \(parent, self) {
                          if (parent == 0L) return(counts[ , self])
                          
                          beatsPerParent <- (spans[parent] %/% spans[self]) * nbeats[self]
                          count <- counts[,self] %% as.integer(beatsPerParent) # the %% method for integer64 is screwed up!
                          
                          if (nbeats[parent] > nbeats[self]) {
                            beats <- cumsum(c(0L, as.integer(levels[[parent]] %/% spans[self])))
                            count <- count - beats[(counts[ , parent] %% nbeats[parent]) + 1L]
                          }
                          count
                          
                        })) 

  ## figure out remainders
  onbeats <- remainders == rational(0L)
  # leftmost <- leftmost(onbeats)
  # lowestLevel <- which(leftmost, arr.ind = TRUE)[ , 'col']
  
  lowestLevel <- leftmost(onbeats, which = TRUE)[ , 'col']
  onbeat <- lowestLevel > 0L
  
  
  if (any(!onbeat, na.rm = TRUE)) {
    
    offbeat <- !is.na(onbeat) & !onbeat 
    offbeats <- as.double(remainders[offbeat, ])
    
    if (remainderSubdivides) {
      subdivide <- do.call('cbind', lapply(as.list(spans), \(span) dur[offbeat] %divides% span))
      offbeats[!subdivide] <- max(offbeats)
    }
    
    lowestLevel[offbeat] <- max.col(-offbeats, ties.method = 'last')
    
  }
  
  remainder <- c(remainders[cbind(seq_len(nrow(remainders)), lowestLevel)])
    
  
  # remove redundant counts
  if (identical(parentSelecter,max)) {
    counts[sweep(col(counts), 1L, lowestLevel, '>')] <- 0L
    counts[sweep(col(counts), 1L, parents[lowestLevel], '>') & !sweep(col(counts), 1L, lowestLevel, '==')] <- 0L
  
    counts <- as.integer(counts) %<-dim% dim(counts)
  }
  
  colnames(counts) <-  colnames(onbeats) <- sapply(levels, \(ls) paste(recip(ls), collapse = '+'))

  list(Counts = counts + 1L, 
       Remainder = remainder, 
       OnBeat = onbeats, 
       Levels = levels,
       LeftMost = leftmost,
       MetLev = lowestLevel)
}

.metrics <- function(dur, meter = duple(5), pickup = NULL, groupby = list(), Exclusive = NULL, ..., 
                     parentSelecter = max, parseArgs = list(), remainderSubdivides = TRUE, callname = '.metric') {
  
  uniqmeters <- unique(meter)
  uniqmeters <- uniqmeters[!is.na(uniqmeters)]
  mets <- lapply(seq_along(uniqmeters), 
                \(i) {
                  targets <- !is.na(meter) & meter == uniqmeters[i]
                  
                  met <- .metric(dur[targets], uniqmeters[i], pickup = if (!is.null(pickup)) pickup[targets],
                                 groupby = lapply(groupby, '[', i = targets),
                                 parentSelecter = parentSelecter, parseArgs = parseArgs, Exclusive = Exclusive[targets],
                                 remainderSubdivides = remainderSubdivides,
                                 callname = callname, ...)
                  met$Indices <- which(targets)
                  met
                })
  
  ## get full counts table
  allCols <- unique(unlist(lapply(mets, \(met) colnames(met$Count))))
  
  
  counts  <- matrix(NA_integer_, 
                    nrow = length(dur), ncol = length(allCols),
                    dimnames = list(NULL, allCols))
  onbeats <- matrix(NA, 
                    nrow = length(dur), ncol = length(allCols),
                    dimnames = list(NULL, allCols))
  topLevels <- integer(length(dur))
  
  for (met in mets) {
    counts[cbind(rep(met$Indices, ncol(met$Counts)), 
                 rep(match(colnames(met$Counts), allCols), each = length(met$Indices)))] <- c(met$Counts)
    onbeats[cbind(rep(met$Indices, ncol(met$OnBeat)), 
                  rep(match(colnames(met$Counts), allCols), each = length(met$Indices)))] <- c(met$OnBeat)
    topLevels[met$Indices] <- match(colnames(met$Counts)[1], allCols)
  }
  
  # need to make counts in the same (top-level) beats accumulate across changes in meter
  # so, e.g., M6/4 and M12/8 will sum their "1."s
  for (toplev in unique(topLevels[topLevels > 0L])) {
    curTop <- topLevels == toplev
    counts[curTop, toplev] <- makeCumulative(counts[curTop, toplev],
                                              groupby = lapply(c(list(segments(meter[toplev])), groupby[colnames(groupby) %in% c('Piece', 'Spine', 'Path')]),
                                                               '[', i = curTop))
  }
  
  # levels
  levels <- unique(.unlist(lapply(mets, '[[', 'Levels')))
  neworder <- order(as.double(.unlist(lapply(levels, sum))), lengths(levels), decreasing = TRUE)
  
  counts <- counts[, neworder, drop = FALSE]
  onbeats <- onbeats[ , neworder, drop = FALSE]
  levels <- levels[neworder]
  
  
  # get other stuff
  indices <- unlist(lapply(mets, '[[', 'Indices'))
  remainder <- .unlist(lapply(mets, '[[', 'Remainder'))[order(indices)]
  lowestLevel <- match(unlist(lapply(mets, \(met) colnames(met$Counts)[met$MetLev])), allCols[neworder])[order(indices)]
  
    
  
  list(Counts = counts, 
       Remainder = remainder, 
       OnBeat = onbeats, 
       Levels = levels,
       MetLev = lowestLevel)
}

### Syncopation ----

#' Identify syncopated rhythms
#' 
#' The `syncopation()` function takes a vector of rhythmic duration values and a meter
#' and identifies which durations are syncopated, return `TRUE` for synocopations and `FALSE`
#' otherwise.
#' The output syncopation depends a lot on how meter is specified/interpreted
#' so check out the [meter()] documentation if you are looking for more control of the output.
#' 
#' @details 
#' 
#' A syncopation occurs whenever a rhythmic duration is longer than the highest
#' metric level that it lands on.
#' 
#' 
#' In some cases, we might want to restrict our attention to syncopations that occur
#' at a specific metric level: for example, "eighth-note syncpations."
#' We can proved a set of metric levels to the `levels` argument, to do this restriction.
#' The `levels` must be parsable as durations which match the levels of the [meter()].
#' 
#' @inheritParams metlev
#' @param levels ***On which metrics levels should we identify syncopations?***
#' 
#' Defaults to `"all"`.
#' 
#' Must be a non-empty `character` or `numeric` vector.
#' 
#' If `levels` is simply the singleton string `"all"`, syncopations at any
#' metric level are identified.
#' Otherwise, the `levels` are parsed with [rhythmInterval()]; fail to parse may lead to an error.
#' The parsed levels must be levels of the given [meter()].
#' 
#' @export
#' @name syncopation
syncopation.default <- function(dur, meter = duple(5), levels = 'all', groupby = list()) {
  checks(dur, xcharacter | xnumber)
  
  levs <- metlev(dur, meter = meter, groupby = groupby, deparser = NULL)
  
  dur <- rhythmInterval(dur)
 
  syncopation <- dur > levs
  
  if (!length(levels) == 1L || levels != 'all') {
    syncopation <- syncopation & Reduce('|', lapply(as.list(rhythmInterval(levels)), `==`, e2 = levs))
  }
  
  syncopation
  
}
#' @export
syncopation.humdrumR <- humdrumRmethod(syncopation.default)
#' @rdname syncopation
#' @export
syncopation <- humdrumRgeneric(syncopation.default)
Computational-Cognitive-Musicology-Lab/humdrumR documentation built on Oct. 22, 2024, 9:28 a.m.