diff --git a/DESCRIPTION b/DESCRIPTION index 95eac536..849e68f9 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: humdrumR Title: humdrumR -Version: 0.7.0.2 +Version: 0.7.0.3 Authors@R: c(person("Nathaniel", "Condit-Schultz", email = "natcs@gatech.edu", role = c("aut", "cre")), person("Claire", "Arthur", email = "claire.arthur@gatech.edu", role = "aut")) Description: This package is a toolkit for the visualization, manipulation, and analysis of data encoded in the Humdrum syntax ( parsed Key <- diatonicSet(Key) diff --git a/R/Dispatch.R b/R/Dispatch.R index 40690373..f04676b0 100644 --- a/R/Dispatch.R +++ b/R/Dispatch.R @@ -474,7 +474,6 @@ exclusiveDispatch <- function(x, dispatchDF, Exclusive, regexApply = TRUE, outpu dispatchDF <- dispatchDF[sapply(dispatchDF$Exclusives, \(exc) any(Exclusive %in% exc)), ] - dispatchDF$regex <- lapply(dispatchDF$regex, \(re) if (rlang::is_function(re)) re(...) else getRE(re)) diff --git a/R/Distributions.R b/R/Distributions.R new file mode 100644 index 00000000..3908f4f4 --- /dev/null +++ b/R/Distributions.R @@ -0,0 +1,1466 @@ + + + +# S4 Distributions ---- + +## Definitions ---- + + + + + +#' Distributions +#' +#' HUmdrumR has ways to... +#' +#' @name distributions +NULL + + +setClass('distribution', contains = 'data.frame', slots = c(Sort = 'integer')) +setClass('count', contains = 'distribution') +setClass('probability', contains = 'distribution', slots = c(N = 'integer', Condition = 'maybecharacter')) + + + +### Constructors ---- + +distribution <- function(x, type, Sort = 0L, N = 0L, Condition = NULL) { + df <- as.data.frame(x) + args <- list(new('distribution', df, Sort = Sort)) + + if (class(type) == 'character') { + + args$Class <- if (type == 'p') 'probability' else 'count' + if (args$Class == 'probability') { + args$N <- N + args$Condition <- Condition + } + + } else { # type must be a distribution + args$Class <- c(class(type)) + if (args$Class == 'probability') { + args$N <- type@N + args$Condition <- type@Condition + } + + } + do.call('new', args) +} + + + + +### Accessors ---- + + +dist_type <- function(dist) intersect(colnames(dist), c('n', 'p')) +getValues <- function(dist) as.data.frame(dist)[ , dist_type(dist)] +getLevels <- function(dist) as.data.frame(dist)[ , varnames(dist), drop = FALSE] + + +#' @export +varnames <- function(x) setdiff(colnames(x), c('n', 'p')) + +### print() ---- + +#' @export +#' @rdname distributions +setMethod('show', 'distribution', \(object) print.distribution(object)) + +#' @export +#' @rdname distributions +print.distribution <- function(dist, digits = if (inherits(dist, 'probability')) 3 else 1, + syntaxHighlight = humdrumRoption('syntaxHighlight'), + zeros = '.') { + + type <- dist_type(dist) + message <- paste0('humdrumR ', + if (type == 'p') paste0('probability distribution ', Pequation(dist)) else 'count distribution') + + varnames <- varnames(dist) + + if (nrow(dist) == 0) { + message <- paste0(message, ' (empty)') + if (syntaxHighlight) message <- syntaxHighlight(message, 'G') + cat(message, '\n') + return(invisible(dist)) + } + sort <- dist@Sort + + X <- getValues(dist) + + X <- if (type == 'p') prettyP(X, digits = digits, zeros = zeros) else prettyN(X, digits = digits, zeros = zeros) + + # do we scale or round? + scale <- attr(X, 'scale') + + scale <- if (length(scale) == 1L) { + if (scale > 0) paste0(' (', c('thousands', 'millions', 'billions', 'trillions')[scale], ')') + } + + if (attr(X, 'negative') %||% FALSE) scale <- paste0(scale, ' (∃N < 0)') + if (attr(X, 'approx')) scale <- paste0(scale, ', ~rounded') + message <- paste0(message, scale) + + dist <- getLevels(dist) + dist[[type]] <- X + + # check if we can widen + iswide <- FALSE + printmat <- (if(sort == -0L && length(varnames) >= 2L) { + + wide <- as.matrix(dcast(as.data.table(dist), rlang::new_formula(quote(...), rlang::sym(varnames[2])), fill = attr(X, 'zerofill'), value.var = type)) + + factorcols <- colnames(wide) %in% varnames + toprow <- ifelse( factorcols, colnames(wide), '') + toprow[length(varnames)] <- varnames[2] + secondrow <- ifelse(!factorcols, colnames(wide), '') + + wide <- rbind(toprow, secondrow, wide, secondrow, toprow) + wide <- apply(wide, 2, format, justify = 'right') + wide <- wide[ , order(match(toprow, colnames(dist), nomatch = 2))] + + width <- sum(nchar(wide[1, ])) + ncol(wide) * 2 + + iswide <- width < (options('width')$width - 10L) + if (iswide) wide + }) %||% apply(rbind(colnames(dist), + as.matrix(dist), + colnames(dist)), + 2, format, justify = 'right') + + if (sort != 0L) { + rank <- format(c('Rank', seq_len(nrow(printmat) - 2L), 'Rank'), + justify = 'left') + if (sort == 1) rank <- rev(rank) + printmat <- cbind(Rank = rank, printmat) + + } + + + + + if (syntaxHighlight) { + + syntax <- array('D', dim = dim(printmat)) + + syntax[c(1, nrow(printmat)), ] <- 'N' + + dataCols <- !colnames(printmat) %in% c(varnames, 'Rank') + rankCol <- colnames(printmat) == 'Rank' + + if (iswide) { + syntax[c(2, nrow(printmat) - 1L), ] <- 'I' + dataRows <- 3:(nrow(printmat) - 2L) + } else { + dataRows <- 2:(nrow(printmat) - 1L) + } + + syntax[dataRows, !dataCols & !rankCol] <- 'I' + syntax[dataRows, rankCol] <- 'E' + + printmat[] <- syntaxHighlight(printmat, syntax) + + + message <- syntaxHighlight(message, 'G') + } + + + cat(message, '\n') + cat(apply(printmat, 1, paste, collapse = ' '), sep = '\n') + cat(message, '\n') + invisible(dist) +} + + +Pequation <- function(dist, f = 'P', collapse = ',') { + varnames <- varnames(dist) + + condition <- dist@Condition + + eq <- paste(setdiff(varnames, condition), collapse = collapse) + if (!is.null(condition)) { + condition <- paste(condition, collapse = collapse) + eq <- paste0(eq, ' | ', condition) + } + + paste0(f, '(', eq, ')') +} + +prettyN <- function(N, digits = 1L, zeros = '.') { + tens <- ifelse(N == 0, 0, log10(abs(N)) |> floor()) + + thousands <- pmin(tens %/% 3, 4L) + thousands[thousands < 0L] <- thousands[thousands < 0L] + 1L + + # scaling (powers of 10^3) + scale <- c('', 'k', 'M', 'G', 'P')[1L + abs(thousands)] + scale[thousands < 0] <- paste0('/', scale[thousands < 0]) + N <- N / 1000^thousands + + globalscale <- if (length(unique(scale[N != 0])) == 1L) { + globalscale <- unique(thousands[N != 0]) + scale <- NULL + globalscale + } + + # round and format + Nround <- round(N, digits = digits) + approx <- N != Nround + + Nprint <- format(Nround, scientific = FALSE) + Nprint[N != 0] <- gsub('^( *)0\\.', '\\1 .', Nprint[N != 0]) + Nprint <- gsub('\\.0+$', '', Nprint) + Nprint <- gsub('^( *)0( *)$', paste0('\\1', zeros, '\\2'), Nprint) + + + output <- paste0(Nprint, scale, ifelse(approx, '~', '')) + output <- paste0(output, strrep(' ', max(nchar(output)) - nchar(output))) + + attr(output, 'zerofill') <- zeros + attr(output, 'scale') <- globalscale + attr(output, 'approx') <- any(approx, na.rm = TRUE) + attr(output, 'negative') <- any(N < 0, na.rm = TRUE) + + output + +} + + +prettyP <- function(P, digits = 3, zeros = '.') { + tens <- ifelse(P == 0, 0, log10(P) |> round()) + + thousands <- (tens - 1) %/% -3 + scale <- min(thousands) + + P <- P * 1000^-scale + + Pround <- round(P, digits = digits) + approx <- P != Pround + + Pprint <- format(Pround, scientific = FALSE) + Pprint <- gsub('^( *)0\\.', '\\1 .', Pprint) + Pprint <- gsub('\\0*$', '', Pprint) + Pprint[P == 0] <- paste0(' ', zeros) + + + output <- paste0(Pprint, ifelse(approx, '~', '')) + maxnchar <- max(nchar(output)) + output <- paste0(output, strrep(' ', maxnchar - nchar(output))) + # output <- gsub(paste0('\\.', strrep('0', digits), '~'), paste0('.~', strrep(' ', digits)), output) + + attr(output, 'zerofill') <- paste0(' ', zeros, strrep(' ', maxnchar - 2L)) + attr(output, 'scale') <- if (scale != 0L) paste0('10^(', -scale * 3, ')') + attr(output, 'approx') <- any(approx) + + output + +} + + +prettyBins <- function(x, maxN = 20, quantiles = 0, right = TRUE, ...) { + levels <- sort(unique(x)) + if (is.integer(x) || all(is.whole(levels))) { + range <- diff(range(levels)) + + if (range <= maxN) { + return(factor(x, levels = min(levels) : max(levels))) + } + if (length(levels) <= maxN) return(factor(x, levels = levels)) + } else { + if (length(levels) <= (maxN / 2)) return(factor(x, levels = levels)) + } + + if (quantiles) { + if (is.logical(quantiles)) quantiles <- 10L + + breaks <- quantile(x, probs = seq(0, 1, length.out = quantiles + 1L)) + } else { + breaks <- hist(x, plot = FALSE, ...)$breaks + } + + cut(x, breaks, include.lowest = TRUE, right = right, dig.lab = 3) + +} + +### indexing ---- +# +setMethod('[', c('distribution', 'ANY', 'missing'), + function(x, i, drop = FALSE) { + + df <- as.data.frame(x)[i , , drop = FALSE] + + if (drop) df else distribution(df, x) + }) +# +setMethod('[', c('distribution', 'missing', 'atomic'), + function(x, i, j, drop = FALSE) { + + + x <- unconditional(x) + varnames <- varnames(x) + + j <- if (is.numeric(j)) varnames[j] else intersect(j, varnames) + + if (any(is.na(j)) || length(j) == 0L) .stop("Can't index this distribution with non-existent columns.") + + type <- dist_type(x) + dt <- as.data.table(x)[ , setNames(list(sum(get(type))), type), by = j] + + if (drop) as.data.frame(dt) else distribution(dt, x) + + + }) + +setMethod('[', c('distribution', 'ANY', 'ANY'), + function(x, i, j, drop = FALSE) { + + x <- x[i , ] + x[ , j, drop = drop] + + + }) + +setMethod('[', c('distribution', 'matrix'), + function(x, i, j, cartesian = TRUE, drop = FALSE) { + + i <- as.data.frame(i) + names(i) <- NULL + + do.call('[[', c(list(x, cartesian = cartesian, drop = drop), i)) + + }) + +#' @export +setMethod('[[', 'distribution', + function(x, i, j, ..., cartesian = FALSE, drop = FALSE) { + + args <- as.list(rlang::enexprs(...)) + + args <- c(list(if (missing(i)) rlang::missing_arg() else i ), + list(if (missing(j)) rlang::missing_arg() else j ), + args) + + missing <- sapply(args, rlang::is_missing) + args[!missing] <- lapply(args[!missing], rlang::eval_tidy) + + if (missing[2] && length(args) == 2L) { + args <- args[1L] + missing <- missing[1L] + } + + + levels <- getLevels(x) + if (length(args) > length(levels)) .stop("This distribution only has {num2print(length(levels))} dimensions to index.", + "You have provided {num2print(length(args))}.") + levels <- levels[1:length(args)] + + args[!missing] <- Map(\(arg, lev) { + if (is.numeric(arg)) unique(lev)[arg] else arg + } , args[!missing], levels[!missing]) + + i <- if (cartesian) { + matches(args[!missing], levels[!missing]) + } else { + Reduce('|', Map(`%in%`, levels[!missing], args[!missing])) + } + + + x[i, , drop = drop] + + + + + }) + +setMethod('[', c('probability', 'missing', 'character'), + function(x, i, j, ...) { + + N <- x@N + x <- unconditional(x) + x <- as.data.table(x) + + x <- as.data.frame(x[ , list(p = sum(p)), by = j]) + + distribution(x, 'p', Condition = NULL, N = N) + + }) + + +#' @export +filter.distribution <- function(.data, ..., drop = FALSE) { + exprs <- rlang::enquos(...) + + .data[rlang::eval_tidy(exprs[[1]], data = as.data.frame(.data)), , drop = drop] +} + +### coercion ---- + +#' @export +as.matrix.distribution <- function(x, wide = TRUE, ...) { + varnames <- varnames(x) + type <- dist_type(x) + + if (length(varnames) >= 2L && wide && x@Sort == -0L) { + mat <- as.matrix(dcast(as.data.table(x), + rlang::new_formula(quote(...), rlang::sym(varnames[2])), + fill = 0, value.var = type)) + + talldim <- colnames(mat) %in% varnames + + rownames(mat) <- do.call('paste', c(as.data.frame(mat[ , talldim, drop = FALSE]), list(sep = '.'))) + mat <- mat[ , !talldim, drop = FALSE] + names(dimnames(mat)) <- c(paste(varnames[talldim], collapse = '.'), varnames[2]) + mat <- array(as.numeric(mat), dim = dim(mat), dimnames = dimnames(mat)) + + + } else { + mat <- matrix(getValues(x), ncol = 1) + rownames(mat) <- do.call('paste', c(getLevels(x), list(sep = '.'))) + names(dimnames(mat)) <- c(paste(varnames, collapse = '.'), '') + colnames(mat) <- type + } + + mat + + mat +} + +#' @export +setMethod('as.data.frame', 'distribution', + function(x) { + setNames(as.data.frame(x@.Data), colnames(x)) +}) + +#' @export +as.data.table.distribution <- function(x) { + as.data.table(as.data.frame(x)) +} + + +### sorting ---- + +#' @rdname distributions +#' @export +setMethod('sort', 'distribution', + function(x, decreasing = TRUE) { + X <- getValues(x) + + x <- x[order(X, decreasing = decreasing), ] + + x@Sort <- if (decreasing) -1L else 1L + x +}) + + +### aligning ---- + +alignDistributions <- function(..., funcname = '') { + + dists <- list(...) + varnames <- lapply(dists, varnames) + if (length(Reduce(\(x, y) if (setequal(x, y)) x, varnames)) == 0L) .stop("If using {funcname} on distributions, they must all have the same dimension names.") + varnames <- varnames[[1]] + + + levels <- setNames(lapply(varnames, + \(dn) do.call('mergeLevels', lapply(dists, \(dist) as.data.frame(dist)[[dn]]))), + varnames) + + levels <- do.call('expand.grid', levels) + + + aligned <- lapply(dists, + \(dist) { + df <- merge(levels, as.data.frame(dist), all = TRUE) + + df <- df[matches(levels, df[,varnames, drop = FALSE]), , drop = FALSE] # put in right order + + type <- dist_type(dist) + ifelse(is.na(df[[type]]), 0, df[[type]]) + + }) + + names(aligned) <- sapply(dists, dist_type) + + list(Levels = levels, X = aligned) +} + +mergeLevels <- function(...) { + args <- list(...) + newlev <- Reduce(union, args) + + mini <- do.call('pmin', c(lapply(args, + \(arg) { + match(newlev, arg) + }), + list(na.rm = TRUE))) + ord <- order(mini, newlev) + newlev[ord] +} + + +#' @export +cbind.distribution <- function(...) { + args <- list(...) + dists <- sapply(args, inherits, what = 'distribution') + + aligned <- do.call('alignDistributions', args[dists]) + args[dists] <- aligned$X + + mat <- do.call('cbind', args) + colnames(mat)[dists][colnames(mat)[dists] == ''] <- names(aligned$X)[colnames(mat)[dists] == ''] + + rownames(mat) <- do.call('paste', c(aligned$Levels, list(sep = '.'))) + names(dimnames(mat)) <- c(paste(colnames(aligned$Levels), collapse = '.'), "") + mat +} + + + + +### arithmetic ---- + +distmat <- function(factors, result, type = 'n') { + if (inherits(factors, 'distribution')) factors <- getLevels(factors) + mat <- matrix(result, ncol = 1) + rownames(mat) <- do.call('paste', c(factors, list(sep = '.'))) + colnames(mat) <- type + names(dimnames(mat)) <- c(paste(colnames(factors), collapse = '.'), '') + mat +} + +#' @export +setMethod('+', c('count', 'count'), + function(e1, e2) { + aligned <- alignDistributions(e1, e2, funcname = '+') + + df <- aligned$Levels + df$n <- callGeneric(aligned$X[[1]], aligned$X[[2]]) + + distribution(df, 'n') + }) + +#' @export +setMethod('+', c('count', 'integer'), + function(e1, e2) { + df <- getLevels(e1) + df$n <- callGeneric(getValues(e1), e2) + + distribution(df, 'n') + }) + +#' @export +setMethod('Ops', c('distribution', 'distribution'), + function(e1, e2) { + aligned <- alignDistributions(e1, e2, funcname = .Generic) + + + callGeneric(aligned$X[[1]], aligned$X[[2]]) + # distmat(aligned$Levels, result, dist_type(e1)) + + }) + + +#' @export +setMethod('Ops', c('distribution', 'numeric'), + function(e1, e2) { + + callGeneric(getValues(e1), e2) + # distmat(e1, result) + + + }) + +#' @export +setMethod('Math', 'distribution', + \(x) { + distmat(x, callGeneric(getValues(x))) + }) + +#' @export +setMethod('Summary', 'distribution', + \(x, ..., na.rm = FALSE) { + setNames(callGeneric(getValues(x), ..., na.rm), paste(varnames(x), collapse = '.')) + }) + + +#' @export +setMethod('mean', 'distribution', + \(x, ..., na.rm = FALSE) { + setNames(mean(getValues(x), ..., na.rm = na.rm), paste(varnames(x), collapse = '.')) + }) + +#' @export +setMethod('*', c('probability', 'probability'), + \(e1, e2) { + + varnames1 <- varnames(e1) + varnames2 <- varnames(e2) + + if (length(intersect(varnames1, varnames2))) return(callNextMethod(e1, e2)) + + if (length(varnames1) > 1L || length(varnames2) > 1L) .stop("Can't cross product probability distributions with more than one dimenion yet.") + + p1 <- setNames(e1$p, getLevels(e1)[[1]]) + p2 <- setNames(e2$p, getLevels(e2)[[1]]) + + jointp <- outer(p1, p2, '*') + + df <- as.data.frame(as.table(jointp)) + colnames(df) <- c(varnames1, varnames2, 'p') + + distribution(df, 'p', Condition = NULL, N = sum(e1@N, e2@N)) + + + }) + + + + + + + + + + + + +## User Functions ---- + + +### count() ---- + + +#' Tabulate and/or cross-tabulate data +#' +#' The `count()` function is exactly like R's fundamental [table()][base::table] function, +#' except that 1) will give special treatment to humdrumR [token()] data 2) +#' has more intuitive/simple argument names 3) makes it easier to combine/manipulate +#' disparate output tables. +#' +#' @details +#' +#' The `count()` function is essentially a wrapper +#' around [base::table()][base::table] function. +#' However, any [token()] class arguments are treated like [factors()], +#' calling generating their own levels. +#' This assures that, for example, pitch data is tabulated in order of pitch height, +#' and "missing" pitches are counted as zero. +#' +#' `count()` will, by default, count `NA` values if they are present---if you don't want +#' to count `NA`s, specify `na.rm = TRUE`. +#' You can also tell `count()` to exclude (not count) any other arbitrary values you +#' provide as a vector to the `exclude` argument. +#' +#' +#' `count()` will always give names to the dimensions of the table it creates. +#' You can specify these names directly as argument names, like `count(Kern = kern(Token))`; +#' if you don't specify a name, `count()` will make up a name(s) based on expression(s) it is tallying. +#' (Note that `count()` does not copy [base::table()]'s obtusely-named `dnn` or `deparse.level` arguments.) + +#' @section Manipulating humdrum tables: +#' +#' The output of `count()` is a special form of R `table`, a `humdrumR.table`. +#' Given two or more `humdrumR.table`s, if you apply basic R operators +#' (e.g., arithmetic, comparisons) or row/column binding (`cbind`/`rbind`) +#' `humdrumR` will align the tables by their dimension-names before +#' doing the operation. +#' This means, that if you have two tables of pitch data, but one table includes specific pitch and other doesn't, +#' you can still add them together or bind them into a matrix. +#' See the examples! +#' +#' @examples +#' +#' generic <- c('c', 'c', 'e', 'g', 'a', 'b', 'b', 'b') +#' complex <- c('c', 'c#', 'e', 'f', 'g','g#', 'g#', 'a') +#' +#' genericTable <- count(generic) +#' complexTable <- count(complex) +#' +#' genericTable +#' complexTable +#' +#' genericTable + complexTable +#' +#' cbind(genericTable, complexTable) +#' +#' @name distributions +#' @export +count.default <- function(..., sort = FALSE, na.rm = FALSE, + .drop = FALSE, binArgs = list()) { + checks(sort, xTF | (xwholenum & xlen1)) + checks(na.rm, xTF) + checks(.drop, xTF) + + args <- list(...) + + # get the appropriate (dim)names for each argument + exprs <- as.list(substitute(list(...)))[-1L] + + varnames <- .names(args) + if (any(varnames == '')) varnames[varnames == ''] <- vapply(exprs[varnames == ''], deparse, nlines = 1L, '') + + if (length(unique(lengths(args))) > 1L) .stop("Can't cross-tabulate these vectors ({harvard(varnames, 'and')}), because they are different lengths.") + + # factorize arguments as needed + args <- lapply(args, + \(arg) { + if (inherits(arg, 'token')) arg <- factorize(arg) + if (is.numeric(arg)) do.call('prettyBins', c(list(arg), binArgs)) else arg + }) + + + argdf <- as.data.frame(args) + colnames(argdf) <- varnames + + result <- rlang::eval_tidy(rlang::expr(count(argdf, !!!(rlang::syms(varnames)), name = 'n', .drop = !!.drop))) + + if (na.rm) result <- result[Reduce('&', lapply(result[ , varnames, drop = FALSE], \(col) !is.na(col))), , drop = FALSE] + + dist <- distribution(result, 'n') + + if (sort) sort(dist, decreasing = sort > 0L) else dist + + + +} + +#' @rdname distributions +#' @export +count.humdrumR <- function(x, ..., sort = FALSE, na.rm = FALSE, .drop = FALSE, binArgs = list()) { + quos <- rlang::enquos(...) + counts <- if (length(quos)) { + names(quos) <- sapply(quos, rlang::as_label) + quo <- rlang::quo(with(x, count.default(!!!quos, sort = !!sort, na.rm = !!na.rm, .drop = !!.drop, binArgs = binArgs))) + rlang::eval_tidy(quo) + + } else { + fields <- pullFields(x, union(selectedFields(x), getGroupingFields(x)), null = 'asis') + do.call('count.default', c(as.list(fields), list(sort = sort, na.rm = na.rm, .drop = .drop, binArgs = binArgs))) + } + + counts +} + + + +#' @rdname distributions +#' @export +count.table <- function(..., sort = FALSE, + na.rm = FALSE, + .drop = FALSE) { + tab <- list(...)[[1]] + type <- if (any(tab < 1 & tab > 0)) 'p' else 'n' + df <- as.data.frame(tab, responseName = type) + + dist <- if (type == 'p') { + distribution(df, type, N = sum(tab), Condition = NULL) + } else { + distribution(df, type) + } + + + if (sort) dist <- sort.distribution(dist, decreasing = sort > 0L) + + dist + +} + + + + +### pdist() ----- + + + +#' Tabulate and cross proportions +#' +#' +#' @export +pdist <- function(x, ..., condition = NULL, na.rm = FALSE, sort = FALSE, .drop = FALSE, binArgs = list()) { + checks(sort, xTF | (xwholenum & xlen1)) + checks(na.rm, xTF) + checks(.drop, xTF) + + UseMethod('pdist') +} + +#' @rdname distributions +#' @export +pdist.count <- function(x, ..., condition = NULL, na.rm = FALSE, sort = FALSE, .drop = FALSE, binArgs = list()) { + + if (na.rm) x <- x[!Reduce('|', lapply(getLevels(x), is.na)), ] + if (sort) x <- sort(x, sort > 0L) + + x <- as.data.frame(x) + + exprs <- rlang::enexprs(...) + if (length(exprs)) condition <- pexprs(exprs, colnames(x), condition)$Condition %||% condition + + n <- sum(x$n, na.rm = TRUE) + + x$p <- x$n / n + x$n <- NULL + + dist <- distribution(x, 'p', N = n) + + if (!is.null(condition)) dist <- conditional(dist, condition = condition) + + if (sort) dist <- sort(dist, decreasing = sort > 0L) + + dist + +} + +#' @rdname distributions +#' @export +pdist.probability <- function(x, ..., condition = NULL, na.rm = FALSE, sort = FALSE, .drop = FALSE, binArgs = list()) { + exprs <- rlang::enexprs(...) + if (length(exprs)) condition <- pexprs(exprs, colnames(x), condition)$Condition %||% condition + + if (!is.null(condition)) conditional(x, condition) else x + +} + + +#' @rdname distributions +#' @export +pdist.default <- function(..., condition = NULL, na.rm = FALSE, sort = FALSE, .drop = FALSE, binArgs = list()) { + exprs <- as.list(substitute(list(...)))[-1] + args <- list(...) + + # get the appropriate (dim)names for each argument + + varnames <- .names(args) + if (any(varnames == '')) varnames[varnames == ''] <- vapply(exprs[varnames == ''], deparse, nlines = 1L, '') + names(args) <- varnames + + # if condition is given as separate vector + conditionName <- deparse(substitute(condition), nlines = 1L) + if (!is.null(condition) && is.atomic(condition) && length(condition) == length(args[[1]])) { + args[[conditionName]] <- condition + condition <- conditionName + } + + + count <- do.call('count.default', c(args, list(na.rm = na.rm, sort = sort, .drop = .drop, binArgs = binArgs))) + + pdist(count, condition = condition) + +} + +#' @rdname distributions +#' @export +pdist.data.frame <- function(x, ..., condition = NULL, na.rm = FALSE, sort = FALSE, .drop = FALSE, binArgs = list()) { + exprs <- rlang::enexprs(...) + + if (length(exprs) == 0L) { + args <- as.list(x) + } else { + parsed <- pexprs(exprs, colnames(x), condition) + condition <- parsed$Condition + + args <- rlang::eval_tidy(rlang::quo(list(!!!(parsed$Exprs))), data = x) + names(args) <- names(parsed$Exprs) + } + + + do.call('pdist', c(args, list(condition = condition, na.rm = na.rm, sort = sort, .drop = .drop, binArgs = binArgs))) + + } + + + + + + +#' @rdname distributions +#' @export +pdist.humdrumR <- function(x, ..., condition = NULL, na.rm = FALSE, sort = FALSE, .drop = FALSE, binArgs = list()) { + + exprs <- rlang::enexprs(...) + + humtab <- getHumtab(x, 'D') + if (length(exprs)) { + + rlang::eval_tidy(rlang::expr(pdist(humtab, !!!exprs, condition = !!condition, na.rm = !!na.rm, sort = !!sort, .drop = !!.drop, binArgs = !!binArgs))) + } else { + selectedFields <- selectedFields(x) + names(selectedFields) <- selectedFields + + do.call('pdist', + c(list(humtab), + as.list(selectedFields), + list(condition = condition, na.rm = na.rm, sort = sort, .drop = .drop, binArgs = binArgs))) + + } +} + + + + +#' @rdname distributions +#' @export +pdist.table <- function(x, ..., condition = NULL, na.rm = FALSE, sort = FALSE, binArgs = list()) { + + if (length(dim(x)) == 1L) condition <- NULL + + if (na.rm) { + notna <- unname(lapply(varnames(x), \(dim) !is.na(dim))) + x <- do.call('[', c(list(x), notna)) + } + + if (is.character(condition)) { + condition <- pmatch(condition, varnames(x), duplicates.ok = FALSE) + condition <- condition[!is.na(condition)] + if (length(condition) == 0L) condition <- NULL + } + + ptab <- proportions(x, margin = condition) + + + N <- marginSums(x, margin = condition) + + distribution(N, 'p') + # new('probability.frame', ptab, N = as.integer(n), margin = as.integer(condition)) + } + + + +conditional <- function(pdist, condition) { + varnames <- varnames(pdist) + if (any(!condition %in% varnames)) .stop("We can only calculate a conditional probability across an existing dimension/factor.", + "The {harvard(setdiff(varnames, condition), 'and')} are not dimensions of the given", + "distribution ({harvard(varnames, 'and')}).") + + if (!is.null(pdist@Condition)) { + if (setequal(condition, pdist@Condition)) return(pdist) + pdist <- unconditional(pdist) + } + + + conditionvec <- do.call('paste', c(as.list(pdist)[condition], list(sep = '.'))) + + margin <- c(tapply(pdist$p, conditionvec, sum)) + margin <- margin[unique(conditionvec)] # to match original order + + pdist$p <- ifelse(margin[conditionvec] == 0, 0, pdist$p / margin[conditionvec]) + + pdist@Condition <- condition + + pdist@N <- setNames(as.integer(pdist@N * margin), names(margin)) + pdist + +} + +unconditional <- function(dist) { + if (!inherits(dist, 'probability') || is.null(dist@Condition)) return(dist) + + + N <- sum(dist@N) + margins <- dist@N / N + + dist$p <- dist$p * margins[paste(as.data.frame(dist)[[dist@Condition]])] # paste to deal with NA levels + + + dist@Condition <- NULL + dist@N <- N + dist + + +} + + +pexprs <- function(exprs, colnames, condition) { + + for (i in seq_along(exprs)) { + expr <- exprs[[i]] + + exprs[[i]] <- switch(class(expr), + integer = , + numeric = rlang::sym(colnames[expr]), + character = rlang::sym(expr), + formula = , + call = { + if (as.character(expr[[1]]) %in% c('|', '~')) { + + if (rlang::is_symbol(expr[[3]])) condition <- as.character(expr[[3]]) + + exprs[[length(exprs) + 1L]] <- expr[[3]] + expr[[2]] + + } else { + expr + } + }, + expr) + + } + + names(exprs) <- sapply(exprs, rlang::as_label) + exprs <- exprs[!duplicated(names(exprs))] + + list(Exprs = exprs, Condition = condition) +} + + + + + + +# Information Theory ---- + +## Likelihoods ---- + +### like() ---- + +#' @export +like <- function(..., distribution) UseMethod('like') + +#' @export +ic <- function(..., distribution, base = 2) -log(like(..., distribution = distribution), base = base) + +#' @export +like.default <- function(..., distribution) { + like.data.frame(data.frame(...), distribution = distribution) +} + +#' @export +like.data.frame <- function(df, ..., distribution) { + + if (missing(distribution)) distribution <- do.call('pdist', list(df, ...)) + + colnames <- colnames(df) + varnames <- varnames(distribution) + + if (!setequal(colnames, varnames)) .stop("To calculate likelihoods, the expected distribution must have the same variables as the observed variables.") + + distribution[as.matrix(df), , drop = TRUE]$p + +} + + +#' @export +pMI <- function(..., distribution, base = 2) { + df <- data.frame(...) + + if (missing(distribution)) distribution <- do.call('pdist', df) + + independent <- Reduce('*', lapply(varnames(distribution), \(j) distribution[ , j])) + + ic_observed <- ic(df, distribution = distribution, base = base) + ic_independent <- ic(df, distribution = independent, base = base) + + ic_independent - ic_observed + +} + + +## Distributional ---- + + +### entropy() ---- + +#' Calculate Entropy or Information Content of variables +#' +#' Information content and entropy are fundamental concepts in [information theory](https://en.wikipedia.org/wiki/Information_theory), +#' which quantify the amount of information (or "surprise") in a random variable. +#' Both concepts are closely related the probability density/mass of events: improbable events have higher information content. +#' The probability of *each* observation maps to the [information content](https://en.wikipedia.org/wiki/Information_content); +#' The average information content of a variable is the [entropy](https://en.wikipedia.org/wiki/Entropy_(information_theory)). +#' Information content/entropy can be calculated for discrete probabilities or continuous probabilities, +#' and humdrumR defines methods for calculating both. +#' +#' @details +#' +#' To calculate information content or entropy, we must assume (or estimate) a probability distribution. +#' HumdrumR uses R's standard [table()] and [density()] functions to estimate discrte and continuous probability +#' distributions respectively. +#' +#' Entropy is the average information content of a variable. +#' The `entropy()` function can accept either a [table()] object (for discrete variables), +#' or a [density()] object (for continuous variables). +#' If `entropy()` is passed an [atomic][base::vector()] vector, +#' the values of the vector are treated as observations or a random variable: +#' for `numeric` vectors, the [stats::density()] function is used to estimate the probability distribution +#' of the random (continuous) variable, then entropy is computed for the density. +#' For other atomic vectors, [table()] is called to tabulate the discrete probability mass for each +#' observed level, and entropy is then computed for the table. +#' +#' The `ic()` function only accepts atomic vectors as its main (`x`) argument, but must also +#' be provided a `distribution` argument. +#' By default, the `distribution` argument is estimated using [density()] (`numeric` input) or [table()] (other input). +#' +#' +#' @family {Information theory functions} +#' @export +entropy <- function(..., base = 2) { + checks(base, xnumber & xpositive) + + UseMethod('entropy') +} + + + +#' @rdname entropy +#' @export +H <- entropy + +#' @rdname entropy +#' @export +entropy.probability <- function(q, p, condition = NULL, base = 2) { + if (!is.null(condition)) q <- conditional(q, condition) + + if (missing(p) || !inherits(p, 'probability')) { + expected <- unconditional(q)$p + observed <- q$p + } else { + # cross entropy of q and p! + aligned <- alignDistributions(q, p, funcname = 'entropy') + observed <- aligned$X[[1]] + expected <- aligned$X[[2]] + + } + + observed <- ifelse(observed > 0L, log(observed, base = base), 0) + + + equation <- Pequation(q, 'H') + setNames(-sum(expected * observed), equation) + } + + +#' @rdname entropy +#' @export +entropy.numeric <- function(x, base = 2, na.rm = TRUE) { + entropy(density(x, ...), base = base, na.rm = na.rm) +} + + +#' @rdname entropy +#' @export +entropy.density <- function(x, base = 2, na.rm = TRUE) { + label <- rlang::expr_name(rlang::enexpr(x)) + if (any(is.na(x))) return(NA_real_) + + dx <- diff(x$x[1:2]) + + equation <- paste0('H(', label, ')') + + setNames(-sum(log(x$y, base = base) * x$y * dx), equation) + + + } + + +#' @rdname entropy +#' @export +entropy.default <- function(..., base = 2) { + entropy(pdist(...), base = base) + } + + +entropies <- function(..., base = 2, conditional = FALSE) { + args <- list(...) + + names <- .names(args) + names <- ifelse(names == '', sapply(substitute(list(...))[-1], deparse, nlines = 1L), names) + names(args) <- names + + mat <- matrix(NA_real_, nrow = length(args), ncol = length(args)) + + indices <- expand.grid(i = seq_along(args), j = seq_along(args)) + if (!conditional) indices <- subset(indices, j >= i) + + + for (k in 1:nrow(indices)) { + i <- indices$i[k] + j <- indices$j[k] + + + + mat[i, j] <- if (i == j) { + entropy(args[[i]], base = base) + } else { + do.call('entropy', c(args[c(i, j)], list(condition = if (conditional) names[j]))) + } + } + + colnames(mat) <- rownames(mat) <- names + mat +} + + +### mutualInfo() ---- + +#' Calculate Entropy or Information Content of variables +#' +#' +#' @family {Information theory functions} +#' @rdname entropy +#' @export +mutualInfo <- function(..., base = 2) { + checks(base, xnumber & xpositive) + + UseMethod('mutualInfo') +} + + + +#' @rdname entropy +#' @export +mutualInfo.probability <- function(x, base = 2) { + varnames <- varnames(x) + if (length(varnames) < 2L) .stop("Can't calculate mutual information of a single variable.") + + x <- unconditional(x) + + observed <- setNames(x$p, do.call('paste', c(getLevels(x), list(sep = '.')))) + + independent <- Reduce('*', lapply(varnames, \(j) x[ , j])) + # expected <- (x[ , 1] * x[ , 2]) + independent <- setNames(independent$p, do.call('paste', c(getLevels(independent), list(sep = '.')))) + + independent <- independent[names(observed)] + + ratio <- observed / independent + logratio <- ifelse(ratio == 0 | ratio == Inf, 0, log(ratio, base = base)) + + equation <- Pequation(x, 'I', ';') + + setNames(sum(observed * logratio, na.rm = TRUE), equation) + +} + + + + +#' @rdname entropy +#' @export +mutualInfo.default <- function(..., base = 2) { + mutualInfo(pdist(...), base = base) +} + + + + + + +##################################################- +# table() extensions for humdrumR ---- ########### +##################################################- + + + +humdrumR.table <- function(tab) { + class(tab) <- unique(c('humdrumR.table', 'table', class(tab))) + tab +} + + +#' @export +cbind.humdrumR.table <- function(...) { + args <- list(...) + classes <- unique(lapply(args, class)) + + table.i <- which(sapply(args, is.table)) + tables <- args[table.i] + tables <- lapply(tables, \(tab) if (length(dim(tab)) == 1) t(t(tab)) else tab) + colnames <- unlist(lapply(tables, \(tab) if (is.null(colnames(tab))) rep('', ncol(tab)) else colnames(tab))) + tables <- lapply(tables, `colnames<-`, value = NULL) + tables <- if (length(tables) > 1L) alignTables(tables, 'cbind', margin = 1) else lapply(tables, unclass) + + args[table.i] <- tables + newtab <- do.call('cbind', args) + + argnames <- rep(.names(args), sapply(args, ncol)) + colnames(newtab) <- ifelse(argnames == '', colnames, argnames) + + class(newtab) <- if (length(classes) == 1L) classes[[1]] else c('humdrumR.table', 'table') + newtab +} + +#' @export +rbind.humdrumR.table <- function(...) { + args <- list(...) + table.i <- which(sapply(args, is.table)) + tables <- args[table.i] + + tables <- lapply(tables, \(tab) if (length(dim(tab)) == 1) t(tab) else tab) + rownames <- unlist(lapply(tables, \(tab) if (is.null(rownames(tab))) rep('', nrow(tab)) else rownames(tab))) + tables <- lapply(tables, `rownames<-`, value = NULL) + tables <- alignTables(tables, 'rbind', margin = 2) + + args[table.i] <- tables + newtab <- do.call('rbind', args) + + argnames <- rep(.names(args), sapply(args, \(arg) if (length(dim(arg)) == 1L) 1 else nrow(arg))) + rownames(newtab) <- ifelse(argnames == '', rownames, argnames) + + class(newtab) <- 'table' + newtab + + +} + + + +#' @export +as.table.distribution <- function(x) { + levels <- lapply(getLevels(x), unique) + varnames <- varnames(x) + type <- dist_type(x) + + x <- as.data.frame(x) + x[varnames] <- lapply(x[varnames], paste) # forces NA to be strings + + tab <- array(dim = lengths(levels), dimnames = lapply(levels, paste)) + tab[do.call('cbind', x[varnames])] <- x[[type]] + dimnames(tab) <- levels + + humdrumR.table(tab) + +} + +alignTables <- function(tables, funcname = '', margin = NULL) { + tables <- lapply(tables, + \(tab) { + dn <- dimnames(tab) + null <- sapply(dn, is.null) + dn[null] <- lapply(dim(tab)[null], \(n) paste0(seq_len(n), '___')) + dimnames(tab) <- lapply(dn, \(names) ifelse(is.na(names), '__', names) ) + tab}) + olddimnames <- lapply(tables, dimnames) + dimensions <- lapply(olddimnames, .names) + dimensions <- Reduce(\(a, b) ifelse(a == b, a, paste(a, b, sep = if (nchar(funcname) == 1) funcname else '/')), dimensions) + if (length(unique(lengths(olddimnames))) > 1L) .stop("If using {funcname} on tables, they must all have the same number of dimensions.") + + dimnames <- as.list(as.data.frame(do.call('rbind', olddimnames))) + if (is.null(margin)) margin <- seq_along(dimnames) + + # dimnames[ margin] <- lapply(dimnames[ margin], \(dim) rep(list(stringr::str_sort(Reduce('union', dim), numeric = TRUE)), length(dim))) + dimnames[ margin] <- lapply(dimnames[margin], + \(dim) { + uniondim <- Reduce('union', dim) + + # get order + uniondim <- uniondim[order(do.call('pmin', c(list(na.rm = TRUE), lapply(dim, \(d) match(uniondim, d)))), + uniondim)] + + rep(list(uniondim), length(dim)) + + }) + + + dimnames <- as.data.frame(do.call('rbind', dimnames)) + rownames(dimnames) <- dimensions + + empties <- lapply(dimnames, \(dn) array(data = 0L, dim = lengths(dn), dimnames = dn)) + + Map(tables, empties, + f = \(tab, empty) { + + # indices <- as.matrix(do.call('expand.grid', c(dimnames(tab), list(stringsAsFactors = FALSE)))) + indices <- rep(list(rlang::missing_arg()), length(dim(tab))) + indices[margin] <- dimnames(tab)[margin] + empty[] <- do.call('[<-', c(list(empty), indices, list(value = tab))) + # empty[indices] <- tab[indices] + + dimnames(empty) <- lapply(dimnames(empty), + \(names) { + if (!any(grepl('[0-9]___', names))) ifelse(names == '__', NA_character_, names) + }) + empty + }) + +} + + +### table() ----- + +deparse.names <- function(exprs, deparse.level = 1L) { + # exprs <- as.list(substitute(list(...)))[-1L] + # if (length(exprs) == 1L && is.list(..1) && !is.null(nm <- names(..1))) + # return(nm) + vapply(exprs, \(x) switch(deparse.level + 1, + "", + if (is.symbol(x)) as.character(x) else "", + deparse(x, nlines = 1)[1L]), + "") +} + +#' @rdname distributions +#' @export +setGeneric('table', signature = 'x', + def = function(x, ..., exclude = if (useNA == 'no') c(NA, NaN), useNA = 'no', dnn = NULL, deparse.level = 1) { + # this is an approach to making base::table generic, but dispatched on a single x argument. + # this is necessary to make it so we can dispatch on x=humdrumR with non-standard evaluation of ... + # the hope is that this function behaves exactly like base::table (for non humdrumR classes), but its tricky to achieve. + # table(x = ..., x=...) will cause an error that doesn't appear in base::table. + args <- list(...) + + if (missing(x)) { + if (is.null(dnn)) dnn <- character(length(args)) + dnn <- ifelse(dnn == '', names(args), dnn) + names(args)[1] <- 'x' + return(do.call('table', + c(args, + list(exclude = exclude, useNA = useNA, dnn = dnn, deparse.level = deparse.level)))) + } + exprs <- sys.call()[-1] + exprs <- exprs[!.names(exprs) %in% c('exclude', 'useNA', 'dnn', 'deparse.level', '.drop', 'na.rm')] + names <- .names(exprs) + if (!any(names == 'x')) names[which(names == '')[1]] <- 'x' + + + args <- append(args, list(x = x), which(names == 'x')[1] - 1) # put x into right position (argument order) + + + if (is.null(dnn)) dnn <- character(length(args)) + dnn <- ifelse(dnn == '', names(args), dnn) + dnn <- ifelse(dnn == '' | (dnn == 'x' & .names(exprs) != 'x'), deparse.names(exprs, deparse.level), dnn) + + token <- any(sapply(args, inherits, what = 'token')) + args <- lapply(args, factorize) + + tab <- do.call(base::table, c(args, list(exclude = exclude, useNA = useNA, dnn = dnn, deparse.level = deparse.level))) + + tab + }) + + + + +#' @export +#' @rdname distributions +setMethod('table', 'humdrumR', + function(x, ..., exclude = if (useNA == 'no') c(NA, NaN), + useNA = 'no', dnn = NULL, + deparse.level = 1) { + + quos <- rlang::enquos(...) + tab <- if (length(quos)) { + dnn <- .names(quos) + dnn[dnn == ''] <- deparse.names(quos[dnn == ''], deparse.level = deparse.level) + quo <- rlang::quo(with(x, table(!!!quos, exclude = exclude, useNA = !!useNA, dnn = dnn))) + rlang::eval_tidy(quo) + + + } else { + fields <- pullFields(x, union(selectedFields(x), getGroupingFields(x))) + + do.call('table', c(as.list(fields), list(exclude = exclude, useNA = useNA, dnn = dnn, deparse.level = deparse.level))) + } + + humdrumR.table(tab) + + }) + +#' @export +#' @rdname distributions +setMethod('table', 'distribution', + function(x) { + as.table(x) + }) diff --git a/R/Keys.R b/R/Keys.R index cb947dc4..158f1fe2 100644 --- a/R/Keys.R +++ b/R/Keys.R @@ -402,10 +402,11 @@ setMethod('%%', signature = c('integer', 'diatonicSet'), hits <- !is.na(alter) & alter == 0L output[hits] <- (((e1[hits] + 1L) - signature[hits]) %% 7L) - 1 + signature[hits] - if (any(!is.na(alter) & alter != 0L)) { - output[!is.na(alter) & alter != 0L] <- { - lof <- t(LO5th(e2[!is.na(alter) & alter != 0L])) - lof[sweep(lof %% 7L, 2, e1[!is.na(alter) & alter != 0L] %% 7L, `==`)] + if (any(!is.na(e1) & !is.na(alter) & alter != 0L)) { + notna <- !is.na(e1) & !is.na(alter) & alter != 0L + output[notna] <- { + lof <- t(LO5th(e2[notna])) + lof[sweep(lof %% 7L, 2, e1[notna] %% 7L, `==`)] } } @@ -507,14 +508,9 @@ dset2alterations <- function(dset, augment = '#', diminish = 'b', ...) { alterations <- getAlterations(dset)[altered, , drop = FALSE] alterations[] <- c(augment, diminish, "")[match(alterations, c(7, -7, 0))] - order <- lapply(mode[altered] %% 7L, \(m) ((0L:6L + m) %% 7L) + 1 ) - - labs <- do.call('rbind', lapply(order, \(ord) c('4', '1', '5', '2', '6', '3', '7')[ord])) - labs[alterations == ''] <- '' - - alterations[] <- paste0(alterations, labs) - + alterations[alterations != ''] <- paste0(alterations[alterations != ''], c('4', '1', '5', '2', '6', '3', '7')[col(alterations)[alterations != '']]) alterations <- apply(alterations, 1, paste, collapse = '') + output <- character(length(mode)) output[altered] <- alterations output @@ -713,13 +709,14 @@ qualities2dset <- function(x, steporder = 2L, allow_partial = FALSE, if (any(abs(change) > 1L)) change <- sign(change) altermat <- matrix(0L, nrow = 1, ncol = 7) - # altermat[ , ((which(altered) - mode - 1L) %% 7L) + 1L] <- change + altered <- which(altered) altermat[ , (altered %% 7L) + 1L] <- change alterint <- baltern2int(altermat) c(mode = mode, altered = alterint) }) |> do.call(what = 'rbind') + mode[altered] <- mode_alterations[ , 1] alterations[altered] <- mode_alterations[ , 2] @@ -862,7 +859,7 @@ key2dset <- function(x, parts = c('step', 'species', 'mode', 'alterations'), signature <- root + mode + minor ## Alterations - alterations <- alteration2trit(alterations, mode + minor) %|% 0 + alterations <- alteration2trit(alterations, mode) %|% 0 dset <- dset(root, signature, alterations) @@ -909,6 +906,10 @@ romanNumeral2dset <- function(x, Key = NULL, flat = '-', sep = '/', ...) { } else { dset } + + na <- is.na(dset) & x == '' + dset[na] <- Key[na] + dset } diff --git a/R/Metric.R b/R/Metric.R index ee7cceaa..c49546fe 100644 --- a/R/Metric.R +++ b/R/Metric.R @@ -188,7 +188,7 @@ setMethod('initialize', #' @rdname meter #' @export -meter <- function(x, ...) UseMethod('meter') +meter <- function(x, ..., measure, tactus, tick, fill.levels, subdiv, hyper, tatum) UseMethod('meter') #' @rdname meter #' @export meter.meter <- function(x, ...) x @@ -332,7 +332,6 @@ meter2timeSignature <- dofunc('x', function(x) { 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) @@ -345,21 +344,26 @@ timesignature2meter <- function(x, ..., sep = '/', compound = TRUE) { beats <- Map(rational, numerator, denominator) denominator <- as.list(rational(1L, denominator)) - levels <- lapply(list(...), rhythmInterval) + 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 - tactus <- tactus * 3L - } - do.call('meter.rational', list(tactus, ..., measure = measure)) + 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) @@ -484,7 +488,7 @@ tatum.NULL <- function(x) NULL #' 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 [count and subpos][count()] functions. +#' These functions are particularly useful as arguments to the [timecount and subpos][timecount()] functions. #' #' #' @details @@ -594,21 +598,6 @@ nbeats.character <- function(x) unlist(lapply(as.list(meter(x)), nbeats.meter)) nbeats.NULL <- function(x) NULL -#' @export -beats <- function(x) UseMethod('beats') -#' @rdname nbeats -#' @export -beats.meter <- function(x) { - measure.meter(x, deparser = NULL) %/% tactus.meter(x, deparser = NULL) - -} -#' @rdname nbeats -#' @export -beats.character <- function(x) unlist(lapply(meter(x), beats.meter)) -#' @rdname nbeats -#' @export -beats.NULL <- function(x) NULL - ###################################################################### ### @@ -623,24 +612,24 @@ beats.NULL <- function(x) NULL #' Count beats or measures #' #' -#' The `count()` function takes a vector of rhythmic duration values and +#' 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 `count()`, computing how far (in rhythmic time) each onset is from its +#' 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 `count()` is essentially the same as using `floor(timeline())`. -#' However, `count()` gives us a few additional options which add musicological power compared to [timeline()]. -#' (`count()` also starts from `1` not `0`, as [timeline()] does.) +#' 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 `count()` assigns (rounds) each onset to the previous beat onset. +#' 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. @@ -649,50 +638,50 @@ beats.NULL <- function(x) NULL #' 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 `count(dur, beat = '1', phase = 3/8)`. +#' This could be achieved with a command like `timecount(dur, beat = '1', phase = 3/8)`. #' #' #' #' @section "Beats": #' -#' The `beat` argument is used to indicate what size of beat you want to count. -#' The default `beat` is a whole note, equivalent to a measure of `M4/4` time. -#' The `beat` argument uses the [rhythm parser][rhythmInterval()], so it can understand beat values input in a variety of formats: -#' thus, you could specify quarter-note beats as either `beat = '4'` or `beat = 0.25`. -#' The parser also understands how to parse the (full) duration of time signature: for example, `beat = 'M3/4'` would use a dotted-half-note beat (`'2.'`). +#' 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 `beat` argument as a vector which is the same length as `dur`, indicating the +#' 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 `beat` argument of `count()`, and -#' the measures of the piece will be correctly counted (even when changing!): `count(x, beat = TimeSignature)`. -#' Alternatively, you can use the [tactus()] command to extract the tactus beat from a time signature, like `count(x, beat = tactus(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 `beat = '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, `beat = list(c("4", "4", "4."))`. +#' 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) #' -#' count(rhythm, beat = '8'), +#' timecount(rhythm, unit = '8'), #' #' # output is: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 #' -#' count(rhythm, beat = list(c('4', '4', '4.'))) +#' 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 `beat` argument can still accept `list` of such patterns, so long as the list is the same length as `dur`. +#' 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: #' @@ -702,7 +691,7 @@ beats.NULL <- function(x) NULL #' 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, count(Token, pickup = Bar < 1)`, which makes the downbeat of +#' 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'.** @@ -718,7 +707,7 @@ beats.NULL <- function(x) NULL #' Wherever the input can't be parsed as a duration, #' that element is treated as a duration of zero. #' -#' @param beat ***The size of "beat" (or measure) to count.*** +#' @param unit ***The size of "beat" (or measure) to count.*** #' #' Defaults to a whole-note (one measure of 4/4 time). #' @@ -737,7 +726,7 @@ beats.NULL <- function(x) NULL #' 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 `beat`. +#' 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. @@ -766,29 +755,29 @@ beats.NULL <- function(x) NULL #' #' humData <- readHumdrum(humdrumRroot, "HumdrumData/BachChorales/chor00[1-4].krn") #' -#' show(within(humData, count(Token, beat = TimeSignature, pickup = Bar < 1))) +#' show(within(humData, timecount(Token, unit = TimeSignature, pickup = Bar < 1))) #' -#' show(within(humData, count(Token, beat = tactus(TimeSignature)))) +#' show(within(humData, timecount(Token, unit = tactus(TimeSignature)))) #' #' -#' @seealso {`count()` and `subpos()` are closely related to the [timeline()] function. The [metcount()] function applies `count()` within a metric framework.} +#' @seealso {`timecount()` and `subpos()` are closely related to the [timeline()] function. The [metcount()] function applies `timecount()` within a metric framework.} #' @export -count <- function(dur, beat = rational(1L), start = 1L, phase = 0, pickup = NULL, offBeats = TRUE, groupby = list()) { +timecount <- 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("?count", 'the rhythm vignette')) + 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, beat, rational(0L), pickup, groupby, callname = 'count') + 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 beats in the 'beat' argument'", - \(arg) paste0(.show_values(rep(phase, length(scaled$Scale))[arg >= min(.unlist(scaled$values))]), " but 'beat' includes ", .show_vector(unique(beat))))) + 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 # @@ -839,22 +828,22 @@ count <- function(dur, beat = rational(1L), start = 1L, phase = 0, pickup = NUL mcount } -#' @rdname count +#' @rdname timecount #' @export -subpos <- function(dur, beat = rational(1L), phase = 0, pickup = NULL, deparser = duration, ..., groupby = list()) { +subpos <- 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, beat, rational(0L), pickup, groupby, callname = 'subpos', sumBeats = TRUE) + 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 beats in the 'beat' argument'", - \(arg) paste0(.show_values(rep(phase, length(scaled$Scale))[arg >= min(.unlist(scaled$values))]), " but 'beat' includes ", .show_vector(unique(beat))))) + 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 @@ -880,14 +869,14 @@ subpos <- function(dur, beat = rational(1L), phase = 0, pickup = NULL, deparser if (is.null(deparser)) timeline else deparser(timeline, ...) } -scaled_timeline <- function(dur, beat, start, pickup, groupby, callname, sumBeats = FALSE) { - dur <- rhythmInterval(dur) +scaled_timeline <- function(dur, unit, start, pickup, groupby, callname, sumBeats = FALSE, ...) { + dur <- rhythmInterval(dur, ...) - checks(beat, (xatomic | xinherits(c('list', 'rational'))) & (xlen1 | xmatch(dur))) + checks(unit, (xatomic | xinherits(c('list', 'rational'))) & (xlen1 | xmatch(dur))) - if (is.list(beat)) { - beat <- rep(beat, length.out = length(dur)) - uniqueBeats <- valind(beat) + if (is.list(unit)) { + unit <- rep(unit, length.out = length(dur)) + uniqueBeats <- valind(unit) uniqueBeats$values <- lapply(uniqueBeats$values, rhythmInterval) @@ -895,27 +884,27 @@ scaled_timeline <- function(dur, beat, start, pickup, groupby, callname, sumBeat tatum <- .unlist(lapply(uniqueBeats$values, if(sumBeats) sum else tatum.rational)) - beat <- tatum[uniqueBeats$i] + unit <- tatum[uniqueBeats$i] } else { irregular <- logical(length(dur)) - beat <- rhythmInterval(beat) - uniqueBeats <- list(values = beat) + unit <- rhythmInterval(unit) + uniqueBeats <- list(values = unit) } - dur <- dur / beat + dur <- dur / unit - timeline <- pathSigma(dur, groupby = groupby, start = start, pickup = pickup, callname = 'count') + timeline <- pathSigma(dur, groupby = groupby, start = start, pickup = pickup, callname = 'timecount') - c(list(Timeline = timeline, Scale = beat, Irregular = irregular, tatum = tatum), uniqueBeats) + c(list(Timeline = timeline, Scale = unit, Irregular = irregular, tatum = tatum), uniqueBeats) } -#' @rdname count +#' @rdname timecount #' @export -onbeat <- function(dur, beat = rational(1L), groupby = list(), ...) { - subpos(dur, beat = beat, groupby = groupby, deparser = NULL) == rational(0L) +onbeat <- function(dur, unit = rational(1L), groupby = list(), ...) { + subpos(dur, unit = unit, groupby = groupby, deparser = NULL) == rational(0L) } @@ -950,7 +939,7 @@ metric <- function(dur, meter = duple(5), start = rational(0), value = TRUE, off #' `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 `count()` and `subpos()` functions. +#' `metcount()` and `metsubpos()` parallel the more general `timecount()` and `subpos()` functions. #' #' @details #' @@ -1083,7 +1072,7 @@ metric <- function(dur, meter = duple(5), start = rational(0), value = TRUE, off #' that element is treated as a duration of zero. #' #' @inheritParams timeline -#' @inheritParams count +#' @inheritParams timecount #' #' @param meter ***The meter(s) to compute levels from.*** #' @@ -1136,7 +1125,7 @@ metric <- function(dur, meter = duple(5), start = rational(0), value = TRUE, off #' #' within(chorales, metcount(Token, pickup = Bar < 1, fill.levels = 'below')) #' -#' @seealso {The [count()] and [subpos()] functions are more basic versions of `metcount()` and `metsubpos()`, +#' @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.} #' @export metlev <- function(dur, meter = duple(5), pickup = NULL, value = TRUE, offBeats = TRUE, remainderSubdivides = FALSE, deparser = recip, @@ -1179,21 +1168,20 @@ metlev <- function(dur, meter = duple(5), pickup = NULL, value = TRUE, offBeats #' @rdname metlev #' @export -metcount <- function(dur, meter = duple(5), level = tactus(meter), pickup = NULL, ..., - offBeats = TRUE, remainderSubdivides = FALSE, groupby = list(), parseArgs = list()) { +metcount.default <- function(dur, meter = duple(5), level = tactus(meter), pickup = NULL, ..., + offBeats = TRUE, 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, + met <- .metric(dur = dur, meter = meter, pickup = pickup, groupby = groupby, parseArgs = parseArgs, Exclusive = Exclusive, remainderSubdivides = remainderSubdivides, callname = 'metcount', ...) counts <- met$Counts - - if (is.character(level) && any(!level %in% colnames(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)}", "", "in the input meter.", @@ -1226,6 +1214,12 @@ metcount <- function(dur, meter = duple(5), level = tactus(meter), pickup = NULL mcount } +#' @rdname metlev +#' @export +metcount.humdrumR <- humdrumRmethod(metcount.default) +#' @rdname metlev +#' @export +metcount <- humdrumRgeneric(metcount.default) #' @rdname metlev #' @export @@ -1244,17 +1238,18 @@ metsubpos <- function(dur, meter = duple(5), pickup = NULL, deparser = duration, .metric <- function(dur, meter = duple(5), groupby = list(), pickup = NULL, ..., - parseArgs = list(), remainderSubdivides = TRUE, callname = '.metric') { + parseArgs = list(), Exclusive = NULL, remainderSubdivides = TRUE, callname = '.metric') { + if (length(unique(meter)) > 1L) { return(.metrics(dur, meter = meter, pickup = pickup, - groupby = groupby, parseArgs = parseArgs, remainderSubdivides = remainderSubdivides, + groupby = groupby, parseArgs = parseArgs, Exclusive = Exclusive, remainderSubdivides = remainderSubdivides, callname = callname, ...)) } - dur <- do.call('rhythmInterval', c(list(dur), parseArgs)) - + 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]] @@ -1262,9 +1257,9 @@ metsubpos <- function(dur, meter = duple(5), pickup = NULL, deparser = duration, nbeats <- lengths(levels) counts <- do.call('cbind', lapply(lapply(levels, \(l) if (length(l) > 1) list(l) else l), - count, + timecount, pickup = pickup, dur = dur, groupby = groupby)) - counts[counts >= 1L] <- counts[counts >= 1L] - 1L + 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)) @@ -1298,22 +1293,27 @@ metsubpos <- function(dur, meter = duple(5), pickup = NULL, deparser = duration, ## 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)) { + if (any(!onbeat, na.rm = TRUE)) { - offbeats <- as.double(remainders[!onbeat , ]) + offbeat <- !is.na(onbeat) & !onbeat + offbeats <- as.double(remainders[offbeat, ]) if (remainderSubdivides) { - subdivide <- do.call('cbind', lapply(as.list(spans), \(span) dur[!onbeat] %divides% span)) + subdivide <- do.call('cbind', lapply(as.list(spans), \(span) dur[offbeat] %divides% span)) offbeats[!subdivide] <- max(offbeats) } - lowestLevel[!onbeat] <- max.col(-offbeats, ties.method = 'last') + lowestLevel[offbeat] <- max.col(-offbeats, ties.method = 'last') } + remainder <- c(remainders[cbind(seq_len(nrow(remainders)), lowestLevel)]) # remove redundant counts @@ -1328,28 +1328,29 @@ metsubpos <- function(dur, meter = duple(5), pickup = NULL, deparser = duration, Remainder = remainder, OnBeat = onbeats, Levels = levels, + LeftMost = leftmost, MetLev = lowestLevel) } -.metrics <- function(dur, meter = duple(5), pickup = NULL, groupby = list(), ..., +.metrics <- function(dur, meter = duple(5), pickup = NULL, groupby = list(), Exclusive = NULL, ..., parseArgs = list(), remainderSubdivides = TRUE, callname = '.metric') { uniqmeters <- unique(meter) - + uniqmeters <- uniqmeters[!is.na(uniqmeters)] mets <- lapply(seq_along(uniqmeters), \(i) { - targets <- meter == 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), - parseArgs = parseArgs, remainderSubdivides = remainderSubdivides, + parseArgs = parseArgs, Exclusive = Exclusive[targets], + remainderSubdivides = remainderSubdivides, callname = callname, ...) met$Indices <- which(targets) met }) ## get full counts table - topLevels <- unique(unlist(lapply(mets, \(met) colnames(met$Count)[1]))) allCols <- unique(unlist(lapply(mets, \(met) colnames(met$Count)))) @@ -1359,19 +1360,24 @@ metsubpos <- function(dur, meter = duple(5), pickup = NULL, deparser = duration, 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 beat accumulate across changes in meter - counts[, colnames(counts) %in% topLevels] <- apply(counts[, colnames(counts) %in% topLevels], - 2, - makeCumulative, - groupby = c(list(segments(meter)), groupby)) + # 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'))) diff --git a/R/Pitch.R b/R/Pitch.R index 8f9f74e2..3148035c 100644 --- a/R/Pitch.R +++ b/R/Pitch.R @@ -1327,8 +1327,6 @@ tint2tonalChroma <- function(x, parts <- matched(parts, c( "species", "step", "octave")) - - # simple part step <- if (step) tint2step(x, ...) species <- if (specific) tint2specifier(x, qualities = qualities, Key = Key, ...) @@ -4247,6 +4245,7 @@ transpose.factor <- transpose.token #' @export transpose.character <- function(x, by = NULL, from = NULL, to = NULL, ...) { x <- tonalInterval.character(x, ...) + tints <- transpose.tonalInterval(x, by = by, from = from, to = to, ...) dispatch <- attr(x, 'dispatch') diff --git a/R/Analysis.R b/R/Plots.R similarity index 53% rename from R/Analysis.R rename to R/Plots.R index 1f69585e..63296db1 100644 --- a/R/Analysis.R +++ b/R/Plots.R @@ -1,601 +1,3 @@ -# Tallies ---- - -setClass('text', contains = 'character') - -setClass('humdrum.table', contains = 'table') - -#' Tabulate and/or cross-tabulate data -#' -#' The `tally()` function is exactly like R's fundamental [table()][base::table] function, -#' except that 1) will give special treatment to humdrumR [token()] data 2) -#' has more intuitive/simple argument names 3) makes it easier to combine/manipulate -#' disparate output tables. -#' -#' @details -#' -#' The `tally()` function is essentially a wrapper -#' around [base::table()][base::table] function. -#' However, any [token()] class arguments are treated like [factors()], -#' calling generating their own levels. -#' This assures that, for example, pitch data is tabulated in order of pitch height, -#' and "missing" pitches are counted as zero. -#' -#' `tally()` will, by default, count `NA` values if they are present---if you don't want -#' to count `NA`s, specify `na.rm = TRUE`. -#' You can also tell `tally()` to exclude (not count) any other arbitrary values you -#' provide as a vector to the `exclude` argument. -#' -#' -#' `tally()` will always give names to the dimensions of the table it creates. -#' You can specify these names directly as argument names, like `tally(Kern = kern(Token))`; -#' if you don't specify a name, `tally()` will make up a name(s) based on expression(s) it is tallying. -#' (Note that `tally()` does not copy [base::table()]'s obtusely-named `dnn` or `deparse.level` arguments.) - -#' @section Manipulating humdrum tables: -#' -#' The output of `tally()` is a special form of R `table`, a `humdrum.table`. -#' Given two or more `humdrum.table`s, if you apply basic R operators -#' (e.g., arithmetic, comparisons) or row/column binding (`cbind`/`rbind`) -#' `humdrumR` will align the tables by their dimension-names before -#' doing the operation. -#' This means, that if you have two tables of pitch data, but one table includes specific pitch and other doesn't, -#' you can still add them together or bind them into a matrix. -#' See the examples! -#' -#' @examples -#' -#' generic <- c('c', 'c', 'e', 'g', 'a', 'b', 'b', 'b') -#' complex <- c('c', 'c#', 'e', 'f', 'g','g#', 'g#', 'a') -#' -#' genericTable <- tally(generic) -#' complexTable <- tally(complex) -#' -#' genericTable -#' complexTable -#' -#' genericTable + complexTable -#' -#' cbind(genericTable, complexTable) -#' -#' @name tally -#' @export -tally.humdrumR <- function(x, ..., sort = FALSE, na.rm = FALSE, exclude = NULL) { - quos <- rlang::enquos(...) - - if (length(quos)) { - quo <- rlang::quo(with(x, tally(!!!quos, na.rm = !!na.rm, exclude = !!exclude))) - rlang::eval_tidy(quo) - - - } else { - fields <- pullFields(x, union(selectedFields(x), getGroupingFields(x))) - - do.call('tally', c(as.list(fields), list(na.rm = na.rm, exclude = exclude))) - } -} - - -#' @export -tally.default <- function(..., sort = FALSE, - na.rm = FALSE, - exclude = NULL) { - - - # exprs <- rlang::enexprs(...) - exprs <- as.list(substitute(list(...)))[-1L] - - args <- list(...) - dimnames <- .names(args) - if (any(dimnames == '')) dimnames[dimnames == ''] <- deparse.unique(exprs[dimnames == '']) - - - args <- lapply(args, - \(arg) { - if (inherits(arg, 'token')) factorize(arg) else arg - }) - tab <- do.call(base::table, - c(args, list(exclude = c(exclude, (if (na.rm) c(NA, NaN))), - useNA = if (na.rm) 'no' else 'ifany', - deparse.level = 0))) - - # dimnames(tab) <- lapply(dimnames(tab), \(dn) ifelse(is.na(dn), 'NA', dn)) - names(dimnames(tab)) <- dimnames - - if (sort) { - if (length(dimnames) > 1) { - for (i in seq_along(dimnames)) { - reorder <- order(apply(tab, i, sum), decreasing = TRUE) - tab <- apply(tab, seq_along(dimnames)[-i], '[', i = reorder) - } - } else { - tab <- sort(tab, decreasing = TRUE) - } - - class(tab) <- 'table' - } - - new('humdrum.table', tab) - - -} - - - -#' @rdname tally -#' @export -setMethod('Ops', c('humdrum.table', 'humdrum.table'), - \(e1, e2) { - - tables <- alignTables(list(e1, e2)) - e3 <- callNextMethod(tables[[1]], tables[[2]]) - - if (inherits(e3, 'table')) new('humdrum.table', e3) else e3 - - }) - -#' @rdname tally -#' @export -cbind.humdrum.table <- function(...) { - tables <- list(...) - tables <- Filter(Negate(is.null), tables) - - tables <- alignTables(tables, 'cbind') - - do.call('cbind', tables) - -} - -#' @rdname tally -#' @export -rbind.humdrum.table <- function(...) { - tables <- list(...) - tables <- Filter(Negate(is.null), tables) - - tables <- alignTables(tables, 'rbind') - - do.call('rbind', tables) - -} - -#' @rdname tally -#' @export -as.data.frame.humdrum.table <- function(x, ...) { - tab <- as.data.frame(S3Part(x), ...) - names(tab)[names(tab) == 'Freq'] <- 'Tally' - tab -} - -alignTables <- function(tables, funcname = '') { - tables <- lapply(tables, - \(tab) { - dimnames(tab) <- lapply(dimnames(tab), \(names)ifelse(is.na(names), '__', names) ) - tab}) - dimnames <- lapply(tables, dimnames) - dimensions <- Filter(\(dn) any(dn != ''), lapply(dimnames, names)) - - if (length(unique(lengths(dimnames))) > 1L) .stop("If using {funcname} on humdrum.tables, they must all have the same number of dimensions.") - - dimnames <- as.data.frame(do.call('rbind', dimnames)) - dimnames <- lapply(dimnames, \(dim) Reduce('union', dim)) - - - allindices <- as.data.frame(do.call('expand.grid', dimnames)) - - empty <- do.call('table', allindices) - 1L - - dimensions <- Reduce(\(a, b) paste(a, b, sep = '/'), dimensions) - names(dimnames(empty)) <- dimensions - - lapply(tables, - \(tab) { - - indices <- as.matrix(allindices[Reduce('&', Map(`%in%`, allindices, dimnames(tab))), ]) - empty[indices] <- tab[indices] - - dimnames(empty) <- lapply(dimnames(empty), \(names) ifelse(names == '__', NA_character_, names)) - empty - }) - - - - -} - - -# Probabilities ---- - -## probabilityDistribution ---- - - - -setClass('probabilityDistribution', contains = 'humdrum.table', slots = c(N = 'integer', margin = 'integer')) - -#' @rdname p -#' @export -setMethod('%*%', c('probabilityDistribution', 'probabilityDistribution'), - function(x, y) { - new('probabilityDistribution', as.table(outer(S3Part(x), S3Part(y), '*')), N = x@N, margin = integer(0L)) - }) - - -unmargin <- function(pd) { - if (length(pd@margin) == 0L) return(pd) - - marginal <- proportions(pd@N) - - pd <- sweep(pd, pd@margin, marginal, '*') - pd@margin <- integer(0L) - pd@N <- sum(pd@N) - pd - -} -#' @rdname p -#' @export -setMethod('show', 'probabilityDistribution', - function(object) { - digits <- 4L - x <- S3Part(object, strictS3 = TRUE) - zeros <- x == 0 - - x[] <- format(x, scientific = FALSE) - n <- nchar(x) - 2 - long <- n > digits & !is.na(x) - x[long] <- paste0(substr(x[long], start = 1, stop = digits + 2), '_') - - # dimension names - header <- pdist.name(x, object@margin) - - # names(dimnames(x)) <- NULL - x[zeros] <- '.' - x[] <- gsub('^0', '', x) - cat('\t\t', header, '\n', sep = '') - print(x, quote = FALSE, na.print = '.NA') - }) - - -pdist.name <- function(ptab, margin = NULL, func = 'P') { - dimnames <- names(dimnames(ptab)) - - args <- if (length(margin)) { - independent <- dimnames[margin] - dependent <- dimnames[-margin] - - paste0(paste(dependent, collapse = ', '), - ' | ', - paste(independent, collapse = ', ')) - - - } else { - paste(dimnames, collapse = ', ') - } - - paste0(func, '(', args, ')') - -} - -#' @rdname tally -#' @export -as.data.frame.probabilityDistribution <- function(x, ...) { - tab <- as.data.frame(S3Part(x), ...) - names(tab)[names(tab) == 'Freq'] <- 'p' - tab -} - -## p() ----- - -#' Tabulate and cross proportions -#' -#' -#' @export -setGeneric('p', function(x, ...) standardGeneric('p')) - - - -#' @rdname p -#' @export -setMethod('p', c('table'), - function(x, margin = NULL, na.rm = FALSE) { - - - if (length(dim(x)) == 1L) margin <- NULL - - if (na.rm) { - notna <- unname(lapply(dimnames(x), \(dim) !is.na(dim))) - x <- do.call('[', c(list(x), notna)) - } - - ptab <- proportions(x, margin = margin) - - - n <- marginSums(x, margin = margin) - - new('probabilityDistribution', ptab, N = as.integer(n), margin = as.integer(margin)) - }) - - - -setClassUnion("discrete", members = c('character', 'integer', 'logical', 'factor')) - -#' @rdname p -#' @export -setMethod('p', 'discrete', - function(x, ..., distribution = NULL, margin = NULL, na.rm = TRUE) { - checks(distribution, xnull | xclass('probabilityDistribution')) - - if (is.null(distribution)) distribution <- p(tally(x, ..., na.rm = FALSE), margin = margin, na.rm = na.rm) - - - - ind <- if (na.rm) { - cbind(x, ...) - } else { - dimnames(distribution) <- lapply(dimnames(distribution), \(dim) ifelse(is.na(dim), '', dim)) - ind <- cbind(x, ...) - ind[is.na(ind)] <- '' - ind - } - c(unclass(distribution)[ind]) - }) - - -#' @rdname p -#' @export -setMethod('p', 'numeric', - function(x, density = NULL, na.rm = FALSE, ..., bw = 'SJ', adjust = 1.5) { - checks(density, xnull | xclass('density')) - - if (!na.rm && any(is.na(x))) { - return(rep_len(NA_real_, length(x))) - } else { - .x <- x[!is.na(x)] - } - - nuniq <- length(unique(.x)) - if ((all(is.whole(.x)) && nuniq < 50L)) return(p(as.character(x), margin = margin, na.rm = na.rm)) - - if (is.null(density)) density <- density(.x, ..., bw = bw, adjust = adjust) - - - x[!is.na(x)] <- density$y[closest(.x, density$x, value = FALSE)] - - x - - }) - -#' @rdname p -#' @export -setMethod('p', 'missing', - function(x, ..., margin = NULL, na.rm = TRUE) { - args <- list(..., margin = margin, na.rm = na.rm) - origname <- names(args)[1] - names(args)[1] <- 'x' - - result <- do.call('p', args) - - if (is.table(result)) names(dimnames(result))[1] <- origname - result - }) - - - - - -# Information theory ---- - - - -#' Calculate Entropy or Information Content of variables -#' -#' Information content and entropy are fundamental concepts in [information theory](https://en.wikipedia.org/wiki/Information_theory), -#' which quantify the amount of information (or "surprise") in a random variable. -#' Both concepts are closely related the probability density/mass of events: improbable events have higher information content. -#' The probability of *each* observation maps to the [information content](https://en.wikipedia.org/wiki/Information_content); -#' The average information content of a variable is the [entropy](https://en.wikipedia.org/wiki/Entropy_(information_theory)). -#' Information content/entropy can be calculated for discrete probabilities or continuous probabilities, -#' and humdrumR defines methods for calculating both. -#' -#' @details -#' -#' To calculate information content or entropy, we must assume (or estimate) a probability distribution. -#' HumdrumR uses R's standard [table()] and [density()] functions to estimate discrte and continuous probability -#' distributions respectively. -#' -#' Entropy is the average information content of a variable. -#' The `entropy()` function can accept either a [table()] object (for discrete variables), -#' or a [density()] object (for continuous variables). -#' If `entropy()` is passed an [atomic][base::vector()] vector, -#' the values of the vector are treated as observations or a random variable: -#' for `numeric` vectors, the [stats::density()] function is used to estimate the probability distribution -#' of the random (continuous) variable, then entropy is computed for the density. -#' For other atomic vectors, [table()] is called to tabulate the discrete probability mass for each -#' observed level, and entropy is then computed for the table. -#' -#' The `ic()` function only accepts atomic vectors as its main (`x`) argument, but must also -#' be provided a `distribution` argument. -#' By default, the `distribution` argument is estimated using [density()] (`numeric` input) or [table()] (other input). -#' -#' -#' @family {Information theory functions} -#' @export -setGeneric('entropy', function(x, base = 2, ...) standardGeneric('entropy')) -#' @rdname entropy -#' @export -setMethod('entropy', 'table', - function(x, base = 2, margin = NULL, na.rm = FALSE) { - - joint <- p(x, margin = NULL, na.rm = na.rm) - - other <- p(x, margin = margin, na.rm = na.rm) - other <- ifelse(x == 0L, 0, log(other, base = base)) - - equation <- pdist.name(joint, margin, 'H') - setNames(-sum(joint * other), equation) - }) - - -#' @rdname entropy -#' @export -setMethod('entropy', 'probabilityDistribution', - function(x, base = 2) { - - joint <- unmargin(x) - - other <- x - other <- ifelse(x == 0L, 0, log(other, base = base)) - - equation <- pdist.name(joint, x@margin, 'H') - - setNames(-sum(joint * other), equation) - }) - - - - -#' @rdname entropy -#' @export -setMethod('entropy', 'density', - function(x, base = 2, na.rm = TRUE) { - label <- rlang::expr_name(rlang::enexpr(x)) - if (any(is.na(x))) return(NA_real_) - - dx <- diff(x$x[1:2]) - - equation <- paste0('H(', label, ')') - - setNames(-sum(log(x$y, base = base) * x$y * dx), equation) - - - }) - -#' @rdname entropy -#' @export -setGeneric('ic', function(x, ...) standardGeneric('ic')) -#' @rdname entropy -#' @export -setMethod('ic', 'discrete', - function(x, ..., base = 2, distribution = NULL, margin = NULL, na.rm = TRUE) { - - p <- p(x, distribution = distribution, ..., margin = margin, na.rm = na.rm) - - -ifelse(p == 0, NA, log(p, base = base)) - - - }) - -#' @rdname entropy -#' @export -setMethod('ic', 'numeric', - function(x, base = 2, density = NULL, ..., na.rm = TRUE) { - - p <- p(x, density = density, ..., na.rm = na.rm) - - -ifelse(p == 0, NA, log(p, base = base)) - - - }) - -#' @rdname entropy -#' @export -setMethod('ic', 'missing', - function(x, base = 2, ..., margin = NULL, na.rm = TRUE) { - args <- list(..., base = base, margin = margin, na.rm = na.rm) - names(args)[1] <- 'x' - do.call('ic', args) - - - }) - - - - - -#' Calculate Mutual Information of variables -#' -#' @family {Information theory functions} -#' @export -setGeneric('mutualInfo', function(x, ...) standardGeneric('mutualInfo')) - - -#' @rdname mutualInfo -#' @export -setMethod('mutualInfo', 'probabilityDistribution', - function(x, ..., base = 2) { - if (length(dim(x)) < 2L) return(entropy(x, base = base)) - - joint <- S3Part(unmargin(x)) - - independentJoint <- outer(rowSums(x), colSums(x), '*') - - logjoint <- ifelse(joint == 0, 0, log(joint, base)) - logindependent <- ifelse(independentJoint == 0, 0, log(independentJoint, base)) - - ratio <- logjoint - logindependent - - equation <- pdist.name(joint, x@margin, 'I') - setNames(sum(joint * ratio), equation) - - }) - - -#' @rdname mutualInfo -#' @export -setMethod('mutualInfo', 'table', - function(x, base = 2, margin = NULL, na.rm = FALSE) { - - ptab <- p(x, margin = margin, na.rm = na.rm) - mutualInfo(ptab, base = base) - - }) - - -#' @rdname mutualInfo -#' @export -setMethod('mutualInfo', 'discrete', - function(x, ..., base = 2, na.rm = FALSE) { - args <- list(x, ...) - marginals <- lapply(args, \(arg) p(tally(arg))) - - joint <- Reduce('%*%', marginals) - - p_observed <- p(x, ..., margin = NULL, na.rm = na.rm) - p_joint <- p(x, ..., distribution = joint, margin = NULL, na.rm = na.rm) - - p_observed <- ifelse(p_observed == 0, NA_real_, log(p_observed, base = base)) - p_joint <- ifelse(p_joint == 0, NA_real_, log(p_joint, base = base)) - - p_observed - p_joint - - - - - }) - - - -#' Calculate cross entropy between two distributions -#' -#' TBA -#' @family {Information theory functions} -#' @export -setGeneric('crossEntropy', function(distribution1, distribution2, ...) standardGeneric('crossEntropy')) - -#' @rdname crossEntropy -#' @export -setMethod('crossEntropy', c('probabilityDistribution', 'probabilityDistribution'), - function(distribution1, distribution2, base = 2) { - - distribution2 <- ifelse(distribution2 == 0L, 0, log(distribution2, base = base)) - - - equation <- paste0('H(', - pdist.name(distribution1, func = ''), ', ', - pdist.name(distribution2, func = '')) - setNames(-sum(distribution1 * distribution2), equation) - - }) - - - # Plotting ---- @@ -622,7 +24,7 @@ setMethod('crossEntropy', c('probabilityDistribution', 'probabilityDistribution' #' + `x` and `y` both numeric: scatter plot. #' + `x` numeric by itself: histogram. #' + `y` numeric by itself: quantile plot. -#' + `x` is a [table][tally()]: barplot. +#' + `x` is a [table][count()]: barplot. #' + `y` is numeric, `x` is `character` or `factor`: a violin plot. #' #' All the standard arguments to base-R plots can be used to customize plots. @@ -760,14 +162,14 @@ setMethod('draw', c('missing', 'numeric'), #' @export setMethod('draw', c('discrete', 'discrete'), function(x, y, ...){ - draw(tally(x, y), ...) + draw(count(x, y), ...) }) #' @rdname draw #' @export setMethod('draw', c('discrete', 'missing'), function(x, y, ...){ - draw(tally(x), ..., xlab = '') + draw(count(x), ..., xlab = '') }) #' @rdname draw @@ -799,16 +201,24 @@ setMethod('draw', c(x = 'token', y = 'token'), # #' @export #setMethod('draw', c('missing', 'discrete'), # function(x, y, ...){ -# output <- draw(tally(y), ...) +# output <- draw(count(y), ...) # }) ################ THis can work except the labels are reversed...need to figure that your + +#' @rdname draw +#' @export +setMethod('draw', 'count', + function(x, ...) { + draw(as.table.distribution(x), ...) + }) + #' @rdname draw #' @export setMethod('draw', 'table', function(x, y, col = 1:nrow(x), log = '', ..., ylim = NULL, yat = NULL, beside = TRUE) { yticks <- sort(unique(prep_ticks(ylim %||% c(0, x), log = grepl('y', log), at = yat))) if (grepl('y', log)) yticks <- yticks[yticks > 0] - if (inherits(x, 'humdrum.table')) x <- S3Part(x) + if (inherits(x, 'count.frame')) x <- S3Part(x) names(x)[is.na(names(x))] <- 'NA' barx <- barplot(x, col = col, log = gsub('x', '', log), beside = beside, axes = FALSE, @@ -828,11 +238,19 @@ setMethod('draw', 'table', #' @rdname draw #' @export -setMethod('draw', 'probabilityDistribution', +setMethod('draw', 'humdrumR.table', + function(x, ...) { + class(x) <- class(x)[-1] + draw(x, ...) + }) + +#' @rdname draw +#' @export +setMethod('draw', 'probability', function(x, y, col = 1:nrow(x), log = '', ..., yat = NULL, beside = TRUE) { yticks <- sort(unique(c(0, prep_ticks(c(x), log = grepl('y', log), at = yat)))) if (grepl('y', log)) yticks <- yticks[yticks > 0] - if (inherits(x, 'humdrum.table')) x <- S3Part(x) + if (inherits(x, 'count.frame')) x <- S3Part(x) names(x)[is.na(names(x))] <- 'NA' barx <- barplot(x, col = col, beside = beside, axes = FALSE, @@ -1269,3 +687,121 @@ viewKernTable <- function(table) { toHNP(lines, "Tabulating kern data and viewing using the PLUGIN.") } + +# pattern finding ---- + + +findrep <- function(x, func = `==`) { + x <- outer(x, x, func) + + + x + +} + +getDiagonals <- function(mat, upper = TRUE, min.n = 4, max.lag = 100) { + grid <- as.data.table(expand.grid(Row = seq_len(nrow(mat)), Col = seq_len(ncol(mat)))) + + grid[ , Lag := Col - Row] + setorder(grid, Lag) + if (upper) grid <- grid[Lag > 0] + + grid <- grid[(nrow(mat) - Lag) >= min.n & Lag <= max.lag] + + grid[, list(Sequence = list(rle(mat[cbind(Row, Col)]))), by = Lag] +} + +findstretches <- function(rle, lag , min.n = 4) { + + rle$values[rle$lengths < min.n] <- FALSE + hits <- cumsum(c(1, head(rle$lengths, n = -1L)))[rle$values] + cbind(Antecedent = hits, Consequent = hits + lag, Length = rle$lengths[rle$values]) +} + +findrepeats <- function(x, min.n = 4, max.lag = 400, func = `==`) { + findrep(x, func = func) |> getDiagonals(min.n = min.n, max.lag = max.lag) -> sequences + + sequences[ , Hits := Map(\(s, l) findstretches(s, l, min.n = min.n), Sequence, Lag)] + sequences[lengths(Hits) > 1L, Hits] |> do.call(what = 'rbind') |> as.data.table() -> sequences + if (nrow(sequences) == 0) return(data.table(Antecedent = integer(0), Consequent = integer(0))) + setorder(sequences, Antecedent) + sequences[ , Lag := Consequent - Antecedent] + sequences[] + +} + +# ggplot2 ---- + + + +#' @rdname withinHumdrum +#' @export +ggplot.humdrumR <- function(data = NULL, mapping = aes(), ..., dataTypes = 'D') { + humtab <- getHumtab(data, dataTypes = dataTypes) + + ggplot(as.data.frame(data), mapping = mapping, ...) + theme_humdrum() +} + + + + +### Treatment of token ---- + +#' @export +scale_type.token <- function(x) if (class(x@.Data) %in% c('integer', 'numeric', 'integer64')) 'continuous' else 'discrete' + + +#' @export +scale_x_token <- function(..., expand = waiver(), guide = waiver(), position = "bottom") { + sc <- ggplot2::discrete_scale(c("x", "xmin", "xmax", "xend"), "position_d", identity, ..., + # limits = c("c", "c#", "d-", "d", "d#", "e-", "e", "e#", "f", "f#", "f##", "g-", "g", "g#", "a-", "a", "a#", "b-", "b", "b#"), + expand = expand, guide = guide, position = position, super = ScaleDiscretePosition) + + sc$range_c <- scales::ContinuousRange$new() + sc +} + + + +### humdrumR plot style ---- + +#### Colors ---- + +scale_color_humdrum <- ggplot2::scale_fill_manual(values = flatly) +# scale_color_continuous(type = colorRamp(flatly[2:3])) + +options(ggplot2.continuous.fill = ggplot2::scale_color_gradientn(colors = flatly_continuous(100))) +options(ggplot2.continuous.color = ggplot2::scale_color_gradientn(colours = flatly_continuous(100))) +options(ggplot2.continuous.colour = ggplot2::scale_color_gradientn(colours = flatly_continuous(100))) + +# options(ggplot2.continuous.colour = 'humdrum') + +#### Theme ---- + + +theme_humdrum <- function() { + ggplot2::update_geom_defaults("point", list(size = .5, color = flatly[1], fill = flatly[2])) + ggplot2::update_geom_defaults("line", list(size = .5, color = flatly[4], fill = flatly[3])) + ggplot2::update_geom_defaults("rect", list(fill = flatly[1])) + + theme(panel.background = element_blank(), axis.ticks = element_blank(), + strip.background = element_blank(), + # panel.border = element_rect(linetype = 'dashed', fill = NA), + legend.key = element_rect(fill = NA), + title = element_text(family = 'Lato', color = flatly[5], size = 16), + plot.title.position = 'plot', plot.title = element_text(hjust = .5), + line = element_line(color = flatly[1]), + rect = element_rect(color = flatly[2]), + text = element_text(family = 'Lato', color = flatly[4]), + axis.text = element_text(color = flatly[5], size = 7), + axis.title = element_text(color = flatly[4], size = 11) + ) +} + + + + + + + + diff --git a/R/README.md b/R/README.md index 88c046e4..301c5c8d 100644 --- a/R/README.md +++ b/R/README.md @@ -784,7 +784,7 @@ If an argument has not default value, simply omit this paragraph. The defaults paragraph should more or less just say "Defaults to `x`", where `x` is some value. However, each function manual's "signature" will already list the *literal* value. The @param field is a place to put the default in words---like saying "zero" instead of `0`. -For example, in the `count()` man, I say the `beat` argument "Defaults to a whole-note", rather than `rational(1)`, which is the literal default. +For example, in the `timecount()` man, I say the `beat` argument "Defaults to a whole-note", rather than `rational(1)`, which is the literal default. #### Constraints diff --git a/R/Regex.R b/R/Regex.R index 6e979f2e..56b3a731 100644 --- a/R/Regex.R +++ b/R/Regex.R @@ -577,6 +577,11 @@ makeRE.key <- function(..., parts = c("step", "species", "mode", "alterations"), REs['mode'] <- captureRE(c('mix', 'lyd', 'ion'), n = '?') res['mode'] <- captureRE(c('phr', 'aeo', 'loc', 'dor'), n = '?') + if ("alterations" %in% parts){ + REs["alterations"] <- makeRE.alterations(...) + res["alterations"] <- makeRE.alterations(...) + } + majors <- cREs(REs[parts[parts %in% names(REs)]]) minors <- cREs(res[parts[parts %in% names(REs)]]) @@ -589,12 +594,11 @@ makeRE.key <- function(..., parts = c("step", "species", "mode", "alterations"), REs['colon'] <- ':?' REs['star'] <- '\\*?' REs['mode'] <- captureRE(c('mix', 'lyd', 'ion', 'phr', 'aeo', 'loc', 'dor'), n = '?') + + if ("alterations" %in% parts) REs["alterations"] <- makeRE.alterations(...) } - if ("alterations" %in% parts) { - REs["alterations"] <- makeRE.alterations(...) - } REs <- REs[parts[parts %in% names(REs)]] diff --git a/R/Rhythm.R b/R/Rhythm.R index 456be7cf..ccdd60ba 100644 --- a/R/Rhythm.R +++ b/R/Rhythm.R @@ -825,11 +825,12 @@ rhythmFunctions <- list(Metric = list(Symbolic = c('recip' = 'reciprocal note v #' #' Must be `numeric` or [rational]. #' -#' @param parseArgs ***An optional list of arguments passed to the [rhythm parser][rhythmParsing].*** +#' @param Exclusive,parseArgs ***An vector of exclusive interpretations and/or an optional list of arguments passed to the [rhythm parser][rhythmParsing].*** #' -#' Defaults to an empty `list()`. +#' Default to `NULL` and an empty `list()` respectively. #' -#' Must be a `list` of named arguments to the [rhythm parser][rhythmParsing]. +#' `Exclusive` must be a `character` vector of length 1, or the same length as `x`; +#' `parseArgs` must be a `list` of named arguments to the [rhythm parser][rhythmParsing]. #' #' @param inPlace ***Should non-rhythm information be retained in the output string?*** #' @@ -1662,7 +1663,7 @@ localDuration <- function(x, choose = min, deparser = duration, ..., Exclusive = #' Another option is to pass the `pickup` argument a logical vector of the same length as the input `x`. #' 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 starting point of the timeline/count; +#' The *first* index where the `pickup` logical is `FALSE` is used as the starting point of the timeline/timecount; #' All the earlier (`pickup == TRUE`) points will be negative numbers, measured backwards from the start index. #' 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, timeline(Token, pickup = Bar < 1)`, which makes the downbeat of @@ -1736,7 +1737,7 @@ localDuration <- function(x, choose = min, deparser = duration, ..., Exclusive = #' within(B075, timeline(Token)) #' } #' -#' @seealso {The [count()] and [metcount()] functions provide "higher level" musical interpretations of timeline information.} +#' @seealso {The [timecount()] and [metcount()] functions provide "higher level" musical interpretations of timeline information.} #' @family rhythm analysis tools #' @name timeline #' @export @@ -1826,7 +1827,7 @@ pathSigma <- function(rints, groupby, start, pickup, threadNA = TRUE, callname) if (!is.null(pickup)) { .SD$Pickup <- pickup .SD[ , Time := { - if (all(!Pickup)) Time else Time - Time[which(!Pickup)[1]] + if (all(!Pickup, na.rm = TRUE)) Time else Time - Time[which(!Pickup)[1]] }, by = list(Piece, Spine, Path)] } diff --git a/R/Struct.R b/R/Struct.R index 70f4f243..bc80bb09 100644 --- a/R/Struct.R +++ b/R/Struct.R @@ -592,7 +592,7 @@ setMethod('[', c(x = 'struct'), match_size(i = i, j = j, toEnv = TRUE) - if (any(i < 0 | j < 0)) stop(call. = FALSE, "Can't do cartesian indexing with negative indices.") + if (any(i < 0 | j < 0, na.rm = TRUE)) stop(call. = FALSE, "Can't do cartesian indexing with negative indices.") i.internal <- humvectorI(i, x) i.internal <- split(i.internal, rep(1:length(i), length.out = length(i.internal))) diff --git a/R/Subset.R b/R/Subset.R index 59f9d760..858c9864 100644 --- a/R/Subset.R +++ b/R/Subset.R @@ -888,13 +888,27 @@ setMethod('[[', signature = c(x = 'humdrumR', i = 'missing', j = 'missing'), #' @export index <- function(x, i, j, drop = TRUE) { - pat <- paste0(missing(i), missing(j)) - - switch(pat, - 'TRUETRUE' = x, - 'TRUEFALSE' = x[ , j, drop], - 'FALSETRUE' = if (length(dim(x)) > 1L) x[i , , drop] else x[i], - 'FALSEFALSE' = x[i, j, drop]) + i <- rlang::enexpr(i) + j <- rlang::enexpr(j) + + if (missing(i) && missing(j)) return(x) + + expr <- rlang::expr(x[]) + if (!missing(i)) expr[[3]] <- i + if (!missing(j)) expr[[4]] <- j + if (!is.null(dim)) expr$drop <- drop + if (inherits(x, 'data.table') && !missing(j)) x$with = FALSE + + + rlang::eval_tidy(expr) + # + # pat <- paste0(missing(i), missing(j)) + # + # switch(pat, + # 'TRUETRUE' = x, + # 'TRUEFALSE' = x[ , j, drop = drop], + # 'FALSETRUE' = if (length(dim(x)) > 1L) x[i , , drop = drop] else x[i], + # 'FALSEFALSE' = x[i, j, drop = drop]) } #' @rdname indexHumdrum diff --git a/R/Summary.R b/R/Summary.R index 78c57123..9baf07ed 100644 --- a/R/Summary.R +++ b/R/Summary.R @@ -1000,8 +1000,8 @@ print.humInterpretations <- function(interps, showEach = TRUE, screenWidth = opt interpmat <- data.table(Piece = paste0(trimTokens(interps$Filename, 70L), ' [', num2str(seq_along(interps$Filename), pad = TRUE), ']'), "{X}" = paste0('{', LETTERS[match(interps$ExclusivePat, names(tallies))], '}'), - interps$Exclusive, - tandems) + unclass(interps$Exclusive), + unclass(tandems)) nfiles <- nrow(interpmat) corpusMessage <- paste0('\n###### Interpretation content', if (!showEach) ':\n') diff --git a/R/Token.R b/R/Token.R index 5f761ad9..62c2c670 100644 --- a/R/Token.R +++ b/R/Token.R @@ -163,6 +163,7 @@ format.token <- function(x, ...) { } factorize <- function(token) { + if (!inherits(token, 'token')) return(if (is.factor(token)) token else factor(token)) factorizer <- token@Attributes$factorizer if (is.null(factorizer)) return(factor(token@.Data)) diff --git a/R/Validation.R b/R/Validation.R index 5ca87bed..29da7a16 100644 --- a/R/Validation.R +++ b/R/Validation.R @@ -102,6 +102,7 @@ isValidHumdrum <- function(fileFrame, errorReport.path = NULL) { if (nrow(fileFrame) == 0L) { message("No files to validate.") fileFrame[ , Valid := logical(0)] + fileFrame[ , Errors := vector('list', 0L)] return(fileFrame) } @@ -145,6 +146,8 @@ isValidHumdrum <- function(fileFrame, errorReport.path = NULL) { if (nrow(reports) == 0L) { message("all valid.") fileFrame[ , Valid := TRUE] + fileFrame[ , Errors := replicate(nrow(fileFrame), simplify = FALSE, + data.table(Filepath = character(0), Record = integer(0), Message = character(0)))] return(fileFrame) } diff --git a/R/Within.R b/R/Within.R index ec81edff..d449e62c 100644 --- a/R/Within.R +++ b/R/Within.R @@ -118,11 +118,11 @@ #' ``` #' humData |> #' select(Token) |> -#' with(tally(.)) +#' with(count(.)) #' #' ``` #' -#' will run [tally()] on the `Token` field. +#' will run [count()] on the `Token` field. #' #' Because new fields created by `within()`/`mutate()`/`reframe()` become the [selected fields][selectedFields] #' (details below), the `.` makes it easy to refer to the *last* new field in pipes. @@ -132,11 +132,11 @@ #' ``` #' humData |> #' mutate(kern(Token, simple = TRUE)) |> -#' with(tally(.)) +#' with(count(.)) #' #' ``` #' -#' the `tally()` function is run on the output of the `mutate(kern(Token, simpe = TRUE))` expression. +#' the `count()` function is run on the output of the `mutate(kern(Token, simpe = TRUE))` expression. # #' #### Automatic argument insertion #' @@ -203,9 +203,9 @@ #' Thus, *these* two calls are also the same: #' #' ``` -#' humData |> with(tally(Token[lag = 1:2])) +#' humData |> with(count(Token[lag = 1:2])) -#' humData |> with(tally(lag(Token, 1), lag(Token, 2))) +#' humData |> with(count(lag(Token, 1), lag(Token, 2))) #' ``` #' #' Note that the lagging will also be automatically grouped within the fields `list(Piece, Spine, Path)`, @@ -261,7 +261,7 @@ #' #' ``` #' humData |> within(dataTypes = 'Dd', -#' tally(Token[splat = Spine %in% 1:2])) +#' count(Token[splat = Spine %in% 1:2])) #' ``` #' #' ### Saving expressions for later @@ -275,15 +275,15 @@ #' `with()`, `within()`, `mutate()`, `summarize()`, and `reframe()`. #' #' Image that you have three different datasets (`humData1`, `humData2`, and `humData3`), -#' and you'd like to evaluate the expression `tally(kern(Token, simple = TRUE))` in all three. +#' and you'd like to evaluate the expression `count(kern(Token, simple = TRUE))` in all three. #' Use the `~` operator to quote and save that expression to variable, then use it with `with()`: #' #' ``` -#' tallyKern <- ~tally(kern(Token, simple = TRUE)) +#' countKern <- ~count(kern(Token, simple = TRUE)) #' -#' humData1 |> with(tallyKern) -#' humData2 |> with(tallyKern) -#' humData3 |> with(tallyKern) +#' humData1 |> with(countKern) +#' humData2 |> with(countKern) +#' humData3 |> with(countKern) #' #' ``` #' @@ -550,7 +550,7 @@ #' #' humData <- readHumdrum(humdrumRroot, "HumdrumData/BachChorales/chor00[1-4].krn") #' -#' humData |> with(tally(kern(Token, simple = TRUE), Spine)) +#' humData |> with(count(kern(Token, simple = TRUE), Spine)) #' #' humData |> within(Kern <- kern(Token), #' Recip <- recip(Token), @@ -1380,78 +1380,3 @@ reframe.humdrumR <- function(.data, ..., dataTypes = 'D', alignLeft = TRUE, expa - -## ggplot2 ----- - - -#' @rdname withinHumdrum -#' @export -ggplot.humdrumR <- function(data = NULL, mapping = aes(), ..., dataTypes = 'D') { - humtab <- getHumtab(data, dataTypes = dataTypes) - - ggplot(as.data.frame(data), mapping = mapping, ...) + theme_humdrum() -} - - - - -### Treatment of token ---- - -#' @export -scale_type.token <- function(x) if (class(x@.Data) %in% c('integer', 'numeric', 'integer64')) 'continuous' else 'discrete' - - -#' @export -scale_x_token <- function(..., expand = waiver(), guide = waiver(), position = "bottom") { - sc <- ggplot2::discrete_scale(c("x", "xmin", "xmax", "xend"), "position_d", identity, ..., - # limits = c("c", "c#", "d-", "d", "d#", "e-", "e", "e#", "f", "f#", "f##", "g-", "g", "g#", "a-", "a", "a#", "b-", "b", "b#"), - expand = expand, guide = guide, position = position, super = ScaleDiscretePosition) - - sc$range_c <- scales::ContinuousRange$new() - sc -} - - - -### humdrumR plot style ---- - -#### Colors ---- - -scale_color_humdrum <- ggplot2::scale_fill_manual(values = flatly) -# scale_color_continuous(type = colorRamp(flatly[2:3])) - -options(ggplot2.continuous.fill = ggplot2::scale_color_gradientn(colors = flatly_continuous(100))) -options(ggplot2.continuous.color = ggplot2::scale_color_gradientn(colours = flatly_continuous(100))) -options(ggplot2.continuous.colour = ggplot2::scale_color_gradientn(colours = flatly_continuous(100))) - -# options(ggplot2.continuous.colour = 'humdrum') - -#### Theme ---- - - -theme_humdrum <- function() { - ggplot2::update_geom_defaults("point", list(size = .5, color = flatly[1], fill = flatly[2])) - ggplot2::update_geom_defaults("line", list(size = .5, color = flatly[4], fill = flatly[3])) - ggplot2::update_geom_defaults("rect", list(fill = flatly[1])) - - theme(panel.background = element_blank(), axis.ticks = element_blank(), - strip.background = element_blank(), - # panel.border = element_rect(linetype = 'dashed', fill = NA), - legend.key = element_rect(fill = NA), - title = element_text(family = 'Lato', color = flatly[5], size = 16), - plot.title.position = 'plot', plot.title = element_text(hjust = .5), - line = element_line(color = flatly[1]), - rect = element_rect(color = flatly[2]), - text = element_text(family = 'Lato', color = flatly[4]), - axis.text = element_text(color = flatly[5], size = 7), - axis.title = element_text(color = flatly[4], size = 11) - ) -} - - - - - - - - diff --git a/R/humdrumR-class.R b/R/humdrumR-class.R index a8121636..743babfc 100644 --- a/R/humdrumR-class.R +++ b/R/humdrumR-class.R @@ -1333,7 +1333,7 @@ names.humdrumR <- function(humdrumR) fields(humdrumR)[ , Name] #' In addition to controlling what [fields()] you "see" in the console printout, #' the select fields are the fields that many [humdrumR][humdrumR] functions will automatically #' apply themselves to. -#' For example, if you call [ditto()], [tally()], or [kern()] on a [humdrumR data object][humdrumRclass], +#' For example, if you call [ditto()], [timecount()], or [kern()] on a [humdrumR data object][humdrumRclass], #' these functions will be applied the selected field(s). #' (However, most such functions are only applied to the *first* selected field, #' if there is more than one; see their own manuals for details.) @@ -1412,8 +1412,8 @@ names.humdrumR <- function(humdrumR) fields(humdrumR)[ , Name] #' #' # effect of selection #' -#' humData |> select(Token) |> tally() -#' humData |> select(Spine) |> tally() +#' humData |> select(Token) |> count() +#' humData |> select(Spine) |> count() #' #' @seealso {Use [fields()] to see what fields are available, and how they are ordered. #' To actually *extract* fields, see [pullFields()].} @@ -1619,7 +1619,7 @@ pullPrintable <- function(humdrumR, fields, dataTypes = 'D', null = 'NA2dot', useToken = c('G', 'L', 'I', 'M', 'S', 'E'), collapse = TRUE){ - fieldTable <- pullFields(humdrumR, union(fields, c('Token', 'Type')), dataTypes = dataTypes, null = if (collapse) 'dot2NA' else null) + fieldTable <- pullFields(humdrumR, union(fields, c('Type')), dataTypes = dataTypes, null = if (collapse) 'dot2NA' else null) Exclusives <- Filter(length, lapply(fieldTable[ , fields, with = FALSE], getExclusive)) tandems <- unlist(unique(lapply(fieldTable, getTandem))) @@ -1646,7 +1646,7 @@ pullPrintable <- function(humdrumR, fields, })] if (!collapse) return(fieldTable[ , fields, with = FALSE]) - # field <- do.call('.paste', c(fieldTable[, fields, with = FALSE], list(sep = ''))) + ## collapse == TRUE: field <- Reduce(\(a, b) { ifelse(fieldTable$Type %in% c('I', 'M', 'd', 'S') & a == b, a, .paste(a, b, sep = '', na.if = all)) @@ -1658,15 +1658,16 @@ pullPrintable <- function(humdrumR, fields, if (length(useToken) && any(grepl(captureRE(useToken), dataTypes))) { # humtab[, !Type %in% c('D', 'd')] fill <- Type %in% useToken & (is.na(field) | is.nullToken(field)) + token <- getHumtab(humdrumR, dataTypes = dataTypes)$Token # get token separately because we always want null = 'asis' if (length(tandems)) { tandemRE <- knownInterpretations[Name %in% tandems, RE] - targets <- Type == 'I' & !is.na(fieldTable$Token) + targets <- Type == 'I' & !is.na(token) fill[targets] <- fill[targets] & Reduce('|', lapply(tandemRE, stringi::stri_detect_regex, - str = fieldTable$Token[targets])) + str = token[targets])) } - field[fill] <- fieldTable$Token[fill] + field[fill] <- token[fill] } if (length(Exclusives)) { diff --git a/R/humdrumR-package.R b/R/humdrumR-package.R index 4a42f8c5..1ef80fc2 100644 --- a/R/humdrumR-package.R +++ b/R/humdrumR-package.R @@ -45,18 +45,18 @@ #' @importFrom utils combn #' @importFrom glue glue glue_collapse #' @importFrom abind abind -#' @importFrom stringr str_count str_detect str_dup str_extract str_match str_pad str_replace str_split str_sub +#' @importFrom stringr str_count str_detect str_dup str_extract str_match str_pad str_replace str_split str_sub str_sort #' @importFrom stringi stri_enc_detect2 stri_read_raw stri_trans_totitle #' @importFrom rlang %|% %||% #' @importFrom bit64 as.integer64 is.integer64 #' @importFrom numbers primeFactors -#' @importFrom data.table data.table rbindlist setorder setindex set setorderv setcolorder copy as.data.table is.data.table frank CJ setnames setkey +#' @importFrom data.table data.table rbindlist setorder setindex set setorderv setcolorder copy as.data.table is.data.table frank CJ setnames setkey dcast #' @importFrom scales ContinuousRange -#' @importFrom dplyr summarise select filter mutate pull reframe group_by ungroup summarize tally +#' @importFrom dplyr summarise select filter mutate pull reframe group_by ungroup summarize count #' @importFrom tidyselect eval_select #' @importFrom tidyr pivot_wider pivot_longer #' @importFrom ggplot2 ggplot update_geom_defaults scale_color_gradientn scale_type aes geom_bar geom_point -#' @export summarise select filter mutate pull reframe group_by ungroup summarize tally +#' @export summarise select filter mutate pull reframe group_by ungroup summarize count NULL @@ -84,7 +84,7 @@ autoArgTable <- rbind(data.table(Argument = 'groupby', Type = 'melodic', Function = c('hint', 'sonority'), Expression = list(quote(list(Piece = Piece, Record = Record)))), data.table(Argument = 'groupby', Type = 'structure', - Function = c('timeline', 'timestamp', 'count', 'onbeat', 'subpos', 'metlev', 'metcount'), + Function = c('timeline', 'timestamp', 'timecount', 'onbeat', 'subpos', 'metlev', 'metcount'), Expression = list(quote(list(Piece = Piece, Spine = Spine, Path = Path, ParentPath = ParentPath, Record = Record, Stop = Stop)))), data.table(Argument = 'orderby', Type = 'harmonic', Function = 'hint', @@ -99,7 +99,9 @@ autoArgTable <- rbind(data.table(Argument = 'groupby', Type = 'melodic', Function = c('mint', 'hint', 'int', 'sonority'), Expression = list(quote(Key))), data.table(Argument = 'Exclusive', Type = 'Exclusive', - Function = c('mint', 'hint', 'int'), + Function = c('mint', 'hint', 'int', + 'metcount', 'metsubpos', 'metlev', + 'timecount', 'timeline', 'timestamp'), Expression = list(quote(Exclusive))), data.table(Argument = 'BPM', Type = 'tempo', Function = 'timestamp', diff --git a/R/messages.R b/R/messages.R index 9509e553..6af8d845 100644 --- a/R/messages.R +++ b/R/messages.R @@ -300,8 +300,8 @@ xrange <- function(min, max) xnumber & argCheck(\(arg) all(arg >= min & arg <= m glue::glue("must be between {min} and {max} (inclusive)"), \(arg) .show_values(arg[arg > max | arg < min])) -xpositive <- xnumber & argCheck(\(arg) all(arg > 0), "must be greater than zero", \(arg) .show_values(arg[arg <= 0])) -xpositiveorzero <- xnumber & argCheck(\(arg) all(arg >= 0), "must be zero or more", \(arg) .show_values(arg[arg < 0])) +xpositive <- xnumber & argCheck(\(arg) all(arg > 0, na.rm = TRUE), "must be greater than zero", \(arg) .show_values(arg[arg <= 0])) +xpositiveorzero <- xnumber & argCheck(\(arg) all(arg >= 0, na.rm = TRUE), "must be zero or more", \(arg) .show_values(arg[arg < 0])) xnegative <- xnumber & argCheck(\(arg) all(arg > 0), "must be less than zero", \(arg) .show_values(arg[arg <= 0])) diff --git a/R/tools.R b/R/tools.R index 809b3dba..cd843281 100644 --- a/R/tools.R +++ b/R/tools.R @@ -1884,7 +1884,7 @@ makeCumulative <- function(n, groupby = list()) { if (length(groupby) == 0L) return(n) diff <- delta.default(n, init = 0L) - diff[diff <= 0 & do.call('changes', groupby)] <- 1 + diff[diff != 1L & do.call('changes', groupby)] <- 1 # diff[which(!is.na(n))[1]] <- 1 sigma.default(diff, init = NA) } @@ -1996,7 +1996,7 @@ ints2baltern <- function(n, ntrits = 8L) { # integers to balanced ternary tern <- ints2nits(abs(n), it = 3L, nits = ntrits) - if (any(abs(n) > (3L ^ ntrits))) .stop("In call ints2baltern, the {which(n > (3L ^ ntrits))}th value is too large to reprsent in {ntrits} trits.") + if (any(abs(n) > (3L ^ ntrits))) .stop("In call ints2baltern, the {which(n > (3L ^ ntrits))}th value is too large to repersent in {ntrits} trits.") while(any(tern == 2L, na.rm = TRUE)) { twos <- which(tern == 2L, arr.ind = TRUE) diff --git a/_pkgdown.yml b/_pkgdown.yml index ea540eec..57423924 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -181,7 +181,7 @@ reference: - tactus - nbeats - tatum - - count + - timecount - metlev - syncopation - subtitle: "Lyrics" @@ -213,12 +213,11 @@ reference: desc: "Other useful tools." - contents: - draw - - tally + - distributions - subtitle: "Information theory" contents: - - p + - pdist - entropy - - crossEntropy - mutualInfo - subtitle: "Numeric values" contents: diff --git a/docs/404.html b/docs/404.html index 52104016..616ffdf8 100644 --- a/docs/404.html +++ b/docs/404.html @@ -25,7 +25,7 @@ Georgia Tech CCMLab humdrumR - 0.7.0.2 + 0.7.0.3 - - - - - -
- - - - -
-
- - - -

humdrumR contains a rich set of built in representations and functions for representing musical tonality and meter. A core philosophical/design choice is that both structures are conceptualized/encoded as intervals.

-
-

Intervals -

-

Both pitch and time are perceived relatively: as a relationship between two points. Thus we represent them, conceptually, as an interval—a difference. In the case of pitch, an interval between two frequencies (or more abstractly, a “tonal” interval on the circle-of-fifths). In the case of rhythm, an interval between two time points—between two onsets, or between an onset and an offset. In humdrumR, these abstract intervals are represented as tonalInterval and rhythmInterval objects, respectively.

-

To concretize our abstract intervals, we must establish what they are relative to. What is the reference? There are actually multiple, useful ways we can represent the same information. Since all intervals are relative, there is always an implicit reference—the origin, zero. For tonalIntervals the origin is the unison interval. For rhythmIntervals the origin is zero.

-

tonalIntervals and rhythmIntervals constitute each constitute an algebraic module over integers (module over \(\mathbb{Z}\)), with appropriate arithmetic operations fully defined in humdrumR. Since intervals can be added/subtracted, any interval can be thought of as a combination (addition) of other intervals. In humdrumR, we often “partition” a interval as a combination of useful sub-intervals.

-
-

Serial and Fixed Reference -

-

Vectors of humdrumR intervals can be represented in two fundamental ways: serial reference representation and fixed reference representation. To illustrate, we will use two examples, one rhythmic, one pitched:

-
    -
  1. The major scale
  2. -
  3. The “tag-line” rhythm 3+3+3+3+2+2 -
  4. -
-
-

Fixed reference -

-

In a fixed-reference representation, all intervals are interpreted relative to a fixed point (implicitly 0). Fixed-reference representations of the major scale (using semitones) and the tag-line rhythm are as follows:

-
-referenceMajor   <- c(0, 2, 4, 5, 7, 9, 11, 12)
-
-referenceTagline <- c(0, 3, 6, 9, 12, 14, 16)
-

Note that this representation is quite commonly used by musicians, in various forms, for pitch. In contrast, fixed-reference representations are not commonly used by musicians for rhythm. However, they are used in some contexts, as they essentially represent “time from the beginning” of a piece: Thus, the timestamps/timelines in DAWs or “offsets” in music21 are examples of fixed-reference representations of time.

-
-
-

Serial reference -

-

An alternative representation is a serial-reference representation, where each interval is measured relative to the previous one. Thus, each data point represents the local change (\(\Delta\)) in the parameter. To be lossless (as explained below) the first element of a serial data vector is measured relative to the implicit reference (0). Serial representations of our tag-line and the major scale are as follows:

-
-serialMajor   <- c(0, 2, 2, 1, 2, 2, 2, 1)
-
-serialTagline <- c(0, 3, 3, 3, 3, 2, 2) 
-

This representations is also fairly intuitive to musicians—representing “melodic intervals”—but is not how music notation or sequencers represent pitch. In contrast, the serial-reference representation for rhythm is normative: traditional music notation note values are a serial-reference representation.

-
-
-

Transformations (“Interval Calculus”) -

-

Since addition is defined for intervals fixed-reference and serial-reference representations can be translated between each other.

-

A serial-reference representation can be calculated as the pairwise differences between elements in a fixed-reference data, with the first reference element appended at the beginning. Conversely, a fixed-reference representation can be calculated as the cumulative sum of serial-reference data. Thus:

-
-identical(cumsum(serialMajor), referenceMajor)
-
## [1] TRUE
-
-identical(cumsum(serialTagline), referenceTagline)
-
## [1] TRUE
-
-identical(c(referenceMajor[1], diff(referenceMajor)), serialMajor)
-
## [1] TRUE
-
-identical(c(referenceTagline[1], diff(referenceTagline)), serialTagline)
-
## [1] TRUE
-

In humdrumR, we refer to these two transformations as delta (\(\Delta\)) and sigma (\(\Sigma\)), for differences and sums respecticely. In humdrumR, we note that the relationship between fixed- and serial-reference representations is analogous to the relationship between a function and its derivative. The fixed-reference representation represents a sequence of independent values as a function of (serial) index position. The serial-reference representation represents the same sequence as the differences between adjacent elements at each index—how much the fixed representation changes at each index. (Since the first element in a serial-reference representation is relative to a fixed (implicit) reference, the normal one-to-many relationship between derivatives and functions is removed, and a lossless one-to-one relationship is maintained.) These two transformations are thus inverses of each other: \(x = sigma(delta(x))\) and \(x = delta(sigma(x))\).

- - -
## Loading required package: rlang
-
## 
-## Attaching package: 'humdrumR'
-
## The following objects are masked from 'package:stats':
-## 
-##     lag, sigma, step
-
## The following object is masked from 'package:methods':
-## 
-##     signature
-
-identical(referenceMajor, sigma(delta(referenceMajor)))
-
## [1] TRUE
-
-identical(referenceMajor, delta(sigma(referenceMajor)))
-
## [1] FALSE
-
-Derivative/Integral relationship in Major Scale

-Derivative/Integral relationship in Major Scale -

-
-
-Derivative/Integral relationship in Tagline Rhythm

-Derivative/Integral relationship in Tagline Rhythm -

-
-
-
-
-

Dynamic Reference -

-

Since intervals are additive, another possibilty is to use one vector of intervals as the reference point for a second vector of intervals. This allows us to create dynamic reference points.

-

Some examples

-
    -
  • -Figured Bass: the bass voice in the texture is encoded (in serial/reference reprsentation) while other voices are represented relative to the bass voice.
  • -
  • -Chord Tones: the abstract “root” of chord progression is represented in one vector, with other vectors representing the “chord tones” (root, 3rd, 5th, 7th, etc.) relative to chord root.
  • -
  • -Scale degrees: The tonic of the key is used as a dynamic reference, with notes calculated relative to the tonic.
  • -
-
-Figured Bass

-Figured Bass -

-
-

Calculating dynamic reference intervals is lossless only if the complete information on the reference intervals are maintained. For instance, we we keep track of the serial or fixed intervals off the bass voice. Or, if we keep track of the local key (usually using a fixed interval to “C”).

-
-
-
-
- - - - -
- - - - - - - diff --git a/docs/articles/Intervals_files/figure-html/unnamed-chunk-5-1.png b/docs/articles/Intervals_files/figure-html/unnamed-chunk-5-1.png deleted file mode 100644 index 37b5f538..00000000 Binary files a/docs/articles/Intervals_files/figure-html/unnamed-chunk-5-1.png and /dev/null differ diff --git a/docs/articles/Intervals_files/figure-html/unnamed-chunk-6-1.png b/docs/articles/Intervals_files/figure-html/unnamed-chunk-6-1.png deleted file mode 100644 index faf59547..00000000 Binary files a/docs/articles/Intervals_files/figure-html/unnamed-chunk-6-1.png and /dev/null differ diff --git a/docs/articles/Intervals_files/figure-html/unnamed-chunk-7-1.png b/docs/articles/Intervals_files/figure-html/unnamed-chunk-7-1.png deleted file mode 100644 index 7f45a88d..00000000 Binary files a/docs/articles/Intervals_files/figure-html/unnamed-chunk-7-1.png and /dev/null differ diff --git a/docs/articles/IntroForCoders.html b/docs/articles/IntroForCoders.html deleted file mode 100644 index 8d6114d1..00000000 --- a/docs/articles/IntroForCoders.html +++ /dev/null @@ -1,141 +0,0 @@ - - - - - - - - -HumdrumR for coders • humdrumR - - - - - - - - - - Skip to contents - - -
- - - - -
-
- - - -

This document is tutorial/introduction for the humdrum\(_{\mathbb{R}}\) package aimed at -experienced programmers/coders. It moves along relatively quickly and -uses more technical programming jargon. If you are looking for something -easier and less technical, check out (Getting started with -humdrumR)[GettingStarted.html “Getting started with humdrumR”] instead. -This tutorial assumes you understand many major principles of coding, -but not necessarily that you are experienced coding in R -specifically—thus, we will take time to introduce an explain -some R-specific syntax and philosophies.

-

UNDER CONSTRUCTION

-
-
- - - -
- - - -
-
- - - - - - - diff --git a/docs/articles/KeysAndChord.html b/docs/articles/KeysAndChord.html index d7a9d585..19c0893a 100644 --- a/docs/articles/KeysAndChord.html +++ b/docs/articles/KeysAndChord.html @@ -27,7 +27,7 @@ Georgia Tech CCMLab humdrumR - 0.7.0.0 + 0.7.0.3 - - - - - -
- - - - -
-
- - - -

Welcome to “Overview of humdrumR”! This document will give you a very -brief overview of what the humdrum\(_{\mathbb{R}}\) package is, and what it can -do. If you’re all ready to start learning how to use humdrum\(_{\mathbb{R}}\), checkout getting -started with humdrumR.

-
-

What is humdrum\(_{\mathbb{R}}\)? -

-

Humdrum\(_{\mathbb{R}}\) is is an R package—a “library” -of preexisting code for the [R programming language](https://en.wikipedia.org/wiki/R_(programming_language). -Humdrum\(_{\mathbb{R}}\) code provides -tools for the visualization, manipulation, and analysis of humdrum data.

-
-

Note: The name of the package is pronounced -hum-drum-ARRRRR, with the last syllable -emphasized in pirate fashion. The name should always be typeset \(h\mu m_{\mathbb{R}}^{\Delta r \mu m}\). -Failure to pronounce or typeset \(h\mu -m_{\mathbb{R}}^{\Delta r \mu m}\) correctly will void the -warranty.

-
-
-

What is humdrum? -

-

Humdrum is a system for computational musicology developed by David -Huron. The Humdrum “universe” includes two parts: a data format called -the humdrum syntax and musicological software system called the -humdrum toolkit, humdrum\(_{\mathbb{R}}\) is meant as a modernized -replacement for the original humdrum toolkit, leveraging the power of R -to give us unprecedented power to manipulate and analyze humdrum data -using concise, expressive syntax.

-

humdrum\(_{\mathbb{R}}\) is mainly -used to manipulate and analyze data encoded in the humdrum syntax and/or -humdrum interpretations like “**kern”. The humdrum syntax is an -incredibly flexible, and powerful, scheme for encoding musical data. -Tens of thousands of musical scores (and other musical data) have been -encoded in the humdrum syntax, many available online through -repositories such as KernScores. -Our the humdrum -syntax vignette gives a detailed introduction to the data -format.

-
-
-

What is R? -

-

R is programming language, designed from the ground up for -statistical computing and data analysis. R has many features which make -it ideal for data analysis, particularly if research and analysis is -your only background in programming. Our R primer introduces the core -concepts of R programming.

-
-
-
-

What can humdrum\(_{\mathbb{R}}\) -do? -

-

Humdrum\(_{\mathbb{R}}\) provides a -number of tools for working with humdrum data and more -generally, musicological analysis. The package has seven main -components:

-
-

Representing humdrum in R -

-

To represent humdrum data in R, humdrum\(_{\mathbb{R}}\) defines a special data -type—the humdrumR class—which we call “humdrumR objects” -throughout the documentation. The most important part of a -humdrumR object is the “humdrum table” which it -contains. You can read about how the humdrum-syntax data is represented -in the getting started with humdrumR -vignette.

-

-

For more details, read the humdrumR -class and humdrum -table; If humdrum\(_{\mathbb{R}}\) -is installed and loaded, you can read these directly in an R session by -typing ?humdrumR-class or ?humTable.

-
-
-

Reading and writing humdrum data -

-

To create humdrumR data objects, humdrum\(_{\mathbb{R}}\) includes a humdrum data -parser, which finds humdrum data on your local machine, reads it into R, -then creates a humdrumR object from the data. The Reading and writing -data vignette is the best place to learm how this works. You can get -more details from the readHumdrum() -and writeHumdrum() -documentation; If humdrum\(_{\mathbb{R}}\) is installed and loaded, -you can read these directly in an R session by typing -?readHumdrum or ?writeHumdrum.

-
-
-

Shaping humdrum data -

-

Once you’ve imported humdrum data in R, the next step is often to -organize and prepare your data. You’ll often want to pick out specific -subsets of the data, or rearrange the data representations to be easier -to work with. Humdrum\(_{\mathbb{R}}\) -gives us a number of powerful tools for “shaping the data”: The Shaping -humdrum data and Filtering humdrum data -vignettes are the best places to learn about these processes. You can -find more details on subsetting data in the subset.humdrumR() -and indexHumdrum -documentation; If humdrum\(_{\mathbb{R}}\) is installed and loaded, -you can read these directly in an R session by typing -?subset.humdrumR or ?indexHumdrum.

-
-
-

Working with humdrum data -

-

Humdrum\(_{\mathbb{R}}\) makes it -very easy to manipulate, modify, and analyze humdrum data. The Working with humdrum -data vignette gives an overview of this functionality. You can find -more details in the withinHumdrum -documentation; If humdrum\(_{\mathbb{R}}\) is installed and loaded, -you can read this directly in an R session by typing -?withinHumdrum.

-
-
-

Pitch -

-

Humdrum\(_{\mathbb{R}}\) defines -tools for manipulating numerous representations of pitch and tonality, -including diatonic -keys and tertian -harmonies. The Pitch and tonality vignette -explains how to work with pitch data in humdrum\(_{\mathbb{R}}\). You can find more details -in the humdrumPitch -documentation; If humdrum\(_{\mathbb{R}}\) is installed and loaded, -you can read this directly in an R session by typing -?humdrumPitch.

-
-
-

Rhythm -

-

Humdrum\(_{\mathbb{R}}\) defines -tools for manipulating numerous representations of rhythm, timing, and -meter. The Rhythm and meter vignette -explains how to work with rhythmic information humdrum\(_{\mathbb{R}}\). You can find more details -in the humdrumRhythm -documentation; If humdrum\(_{\mathbb{R}}\) is installed and loaded, -you can read this directly in an R session by typing -?humdrumRhythm.

-
-
-

Development -

-

To facilitate the development of new functions to work with -humdrum tokens—which are simple character strings packed with -information—, humdrum\(_{\mathbb{R}}\) -provides several useful development tools, including our struct -data type and a useful API we call our regular-expression -dispatch system, which makes it easy to dispatch different methods -based on matches to regular expressions.

-
-
-
-
- - - -
- - - -
-
- - - - - - - diff --git a/docs/articles/PitchAndTonality.html b/docs/articles/PitchAndTonality.html index f006e24b..18e5914a 100644 --- a/docs/articles/PitchAndTonality.html +++ b/docs/articles/PitchAndTonality.html @@ -27,7 +27,7 @@ Georgia Tech CCMLab humdrumR - 0.7.0.0 + 0.7.0.3
- - - - - -
- - - - -
-
- - - -
-library(humdrumR)
-#> Loading required package: rlang
-#> 
-#> Attaching package: 'humdrumR'
-#> The following objects are masked from 'package:stats':
-#> 
-#>     lag, sigma, step
-#> The following object is masked from 'package:graphics':
-#> 
-#>     segments
-#> The following object is masked from 'package:methods':
-#> 
-#>     signature
-
-
- - - -
- - - -
-
- - - - - - - diff --git a/docs/articles/WorkingWithData.html b/docs/articles/WorkingWithData.html deleted file mode 100644 index 882d3b8e..00000000 --- a/docs/articles/WorkingWithData.html +++ /dev/null @@ -1,1350 +0,0 @@ - - - - - - - - -Working with humdrum data • humdrumR - - - - - - - - - - Skip to contents - - -
- - - - -
-
- - - -

Once we have imported humdrum data into -R, we’ll be ready to actually work with our data! The humdrum data -you’ve imported is stored in a humdrum\(_{\mathbb{R}}\) data object, which gives us -some special tools for working with it.

-

Let’s load some of the prepackaged data, as we learned in the read/write vignette.

-
chorales <- readHumdrum(humdrumRroot, 'HumdrumData/BachChorales/chor.*.krn')
-
-chorales
->    ############################# vvv chor001.krn vvv #############################
->         1:  !!!COM: Bach, Johann Sebastian
->         2:  !!!CDT: 1685/02/21/-1750/07/28/
->         3:  !!!OTL@@DE: Aus meines Herzens Grunde
->         4:  !!!OTL@EN:      From the Depths of My Heart
->         5:  !!!SCT: BWV 269
->         6:  !!!PC#: 1
->         7:  !!!AGN: chorale
->         8:          **kern        **kern        **kern        **kern
->         9:          *ICvox        *ICvox        *ICvox        *ICvox
->        10:          *Ibass       *Itenor        *Ialto       *Isoprn
->        11:         *I"Bass      *I"Tenor       *I"Alto    *I"Soprano
->        12:       *>[A,A,B]     *>[A,A,B]     *>[A,A,B]     *>[A,A,B]
->        13:    *>norep[A,B]  *>norep[A,B]  *>norep[A,B]  *>norep[A,B]
->        14:             *>A           *>A           *>A           *>A
->        15:         *clefF4      *clefGv2       *clefG2       *clefG2
->        16:          *k[f#]        *k[f#]        *k[f#]        *k[f#]
->        17:             *G:           *G:           *G:           *G:
->        18:           *M3/4         *M3/4         *M3/4         *M3/4
->        19:          *MM100        *MM100        *MM100        *MM100
->        20:             4GG            4B            4d            4g
->        21:              =1            =1            =1            =1
->        22:              4G            4B            4d            2g
->        23:              4E           8cL            4e             .
->        24:               .           8BJ             .             .
->        25:             4F#            4A            4d           4dd
->        26:              =2            =2            =2            =2
->        27:              4G            4G            2d           4.b
->        28:              4D           4F#             .             .
->        29:               .             .             .            8a
->        30:              4E            4G            4B            4g
->        31:              =3            =3            =3            =3
->        32:              4C           8cL           8eL           4.g
->        33:               .           8BJ            8d             .
->        34:            8BBL            4c            8e             .
->        35:            8AAJ             .          8f#J            8a
->        36:             4GG            4d            4g            4b
->        37:              =4            =4            =4            =4
->        38:             2D;           2d;          2f#;           2a;
->        39:             4GG            4d            4g            4b
->        40:              =5            =5            =5            =5
->    41-133:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
->    ############################# ^^^ chor001.krn ^^^ #############################
->    
->           (8 more pieces...)
->    
->    ############################# vvv chor010.krn vvv #############################
->      1-60:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
->        61:            2AA;           2c;           2e;           2a;
->        62:              2A            2e            2a           2cc
->        63:              =9            =9            =9            =9
->        64:              4E            4e            4g            4b
->        65:             8DL            4e            4g           4cc
->        66:             8CJ             .             .             .
->        67:             4BB            4d           8gL           4dd
->        68:               .             .           8fJ             .
->        69:              4C            4c            4e            4g
->        70:             =10           =10           =10           =10
->        71:              4D           8F#            4d            4b
->        72:               .            4G             .             .
->        73:              4D             .            4c            4a
->        74:               .           8F#             .             .
->        75:            2GG;           2G;           2B;           2g;
->        76:             =11           =11           =11           =11
->        77:              2C            2G            2e            2g
->        78:             4AA            4A            4e           4cc
->        79:              4E           4G#           8eL            4b
->        80:               .             .           8dJ             .
->        81:             =12           =12           =12           =12
->        82:              4F            4A            4c            4a
->        83:              4C            4G            4c            4e
->        84:            4BB-            4G           [2d            4g
->        85:             4AA            4A             .            4f
->        86:             =13           =13           =13           =13
->        87:            4GG#            4B           4d]           1e;
->        88:             4AA            4A            4c             .
->        89:            2EE;         2G#X;           2B;             .
->        90:              ==            ==            ==            ==
->        91:              *-            *-            *-            *-
->        92:  !!!hum2abc: -Q ''
->        93:  !!!title: @{PC#}. @{OTL@@DE}
->        94:  !!!YOR1: 371 vierstimmige Choralges&auml;nge von Johann Sebastian B***
->        95:  !!!YOR2: 4th ed. by Alfred D&ouml;rffel (Leipzig: Breitkopf und H&a***
->        96:  !!!YOR2: c.1875). 178 pp. Plate "V.A.10".  reprint: J.S. Bach, 371 ***
->        97:  !!!YOR4: Chorales (New York: Associated Music Publishers, Inc., c.1***
->        98:  !!!SMS: B&H, 4th ed, Alfred D&ouml;rffel, c.1875, plate V.A.10
->        99:  !!!EED:  Craig Stuart Sapp
->       100:  !!!EEV:  2009/05/22
->    ############################# ^^^ chor010.krn ^^^ #############################
->                           (***four global comments truncated due to screen size***)
->    
->       humdrumR corpus of ten pieces.
->        Data fields: 
->               *Token :: character
-

So we’ve got ten files of **kern data. How do we access -or manipulate this data?

-
-

With and within you -

-

The key functions for working with humdrum\(_{\mathbb{R}}\) data objects are -with and within. These functions allow you to -get “inside” your data. Basically, once you call -with/within, all the data fields within the -data become available to you, to do with as you will.

-
-

With -

-

Let’s start with with. You should call with -with a humdrum\(_{\mathbb{R}}\) data -object as the first argument. Your second argument can be any arbitrary -R expression you like, and can use fields of the humdrum data. -An “expression” is just a valid bit of R code; for example, -2 + 2, c(1, 2, 3), -(x - mean(x))^2, and -nchar(letters) |> mean() are all examples of -expressions. So “any arbitrary R expression” is just a fancy way of -saying any R code that you want. When we execute an R command, -R “evaluates” the expression(s) and returns their results.

-

As a starting point, lets tabulate all the tokens we have in our -dataset. When you first load humdrum data, the original data tokens are -always put into a field called Token. We will use the -awesome built-in R function, table, to tabulate -the unique tokens in our Token field. However, we can’t -access the Token field directly; if we just write the -expression on the table(Token) we’ll get an error—R will -not be able to “find”, and will say -“Error: object 'Token' not found.” Instead, we use -with to enter the data, where R can “see” -Token (and all other data fields):

-
with(chorales, table(Token))
->    Token
->        [2d     [2e     [4a     [4A     [4B     [4c     [4d     [4e     [4E     [4f 
->          1       1       1       2       1       1       1       2       2       1 
->       [4f#     [4g     [4G    [8cJ    [8CJ    [8gJ    16AL  16B-Jk 16b-XJJ  16BBJJ 
->          1       4       2       1       1       1       1       1       1       1 
->      16BJJ   16C#L  16c#LL   16ccL  16ccLL  16d#JJ  16ddJJ   16dJJ   16EJJ    16eL 
->          1       1       1       1       1       1       1       2       1       2 
->      16F#L     1e;    2.a;    2.A;   2.AA;     2.b    2.b;    2.B;   2.BB;    2.c; 
->          1       1       1       1       1       1       1       1       1       1 
->       2.C#   2.c#;     2.d    2.d;     2.e    2.e;    2.ee    2.f;   2.f#;   2.FF; 
->          1       1       1       2       1       1       1       1       1       1 
->       2.g;   2.GG;      2a      2A    2a-;    2A-;     2a;     2A;   2AA-;    2AA; 
->          1       1      17      10       1       2       9       2       1       4 
->         2b      2B    2b-;     2b;     2B;     2BB   2BB-;    2BB;      2c      2C 
->         10       2       1       3       8       2       1       1       1       1 
->        2c;     2C;     2c#    2c#;     2cc    2cc#   2cc#;      2d      2D    2d-; 
->          6       1       5       3       3       3       1       6       5       1 
->        2d;     2D;     2d#     2D#    2d#;     2dd    2DnX      2e      2E    2e-; 
->          6       5       1       1       1       8       1      13       4       1 
->        2e;     2E;     2E#    2EE;     2f;     2f#     2F#    2f#;    2F#;    2FF; 
->          9       5       1       1       4      10       4       5       1       3 
->      2FF#;      2g      2G     2g;     2G;     2g#     2G#    2g#;    2G#;   2G#X; 
->          1       7       1       3       1       4       1       1       4       1 
->       2GG;     4.a    4.a-     4.b     4.B    4.b-    4.BB     4.c   4.cc#     4.d 
->          2       2       1       2       3       2       2       1       1       1 
->       4.dd     4.e    4.e-    4.ee     4.f    4.f#     4.g      4a      4A     4a- 
->          1       1       1       1       3       1       5      90      58      15 
->        4A-    4a-;    4A-;    4a-X     4a;     4A;     4a#     4A#     4AA    4AA- 
->          4       2       2       1       8       3       4       2      20       2 
->      4AA-;    4AA;    4AA#    4anX      4b      4B     4b-     4B-    4B-X     4b; 
->          1       5       1       1      81      56      16      10       1       9 
->        4B;     4B]     4BB    4BB-    4BB;      4c      4C     4c;     4C;     4c] 
->          9       1      25       7       3      49      23       8       2       1 
->        4c#     4C#    4c#;    4C#;     4cc    4cc;    4cc#   4ccnX    4CnX      4d 
->         27      19       2       1      48       3      28       1       1      53 
->         4D     4d-     4D-     4d;     4D;     4d]     4d#     4D#    4d#;     4dd 
->         30       9       4       8       4       1       9       8       3      29 
->        4DD    4dd-    4dd;    4dd#    4dnX    4DnX      4e      4E     4e-     4E- 
->          1       7       2       3       2       1     103      43       7       6 
->       4e-;     4e;     4E;     4e]     4E]     4e#     4E#    4e#;    4E#X     4ee 
->          2      14       7       1       1       3       2       1       1      20 
->        4EE    4ee-   4ee-X    4ee;    4EE;    4enX    4EnX      4f      4F     4f; 
->          2       3       1       1       2       1       1      50      12       2 
->        4F;     4f#     4F#    4f#;    4F#;    4F#X   4F#X;     4ff     4FF    4ff; 
->          2      47      30       4       2       1       1       6       3       1 
->       4FF;    4ff#    4FF#      4g      4G     4g-     4g;     4G;     4g]     4G] 
->          1       1       2      66      33       1       8       3       2       1 
->        4g#     4G#    4g#;    4G#;    4G#X   4G#X;     4gg     4GG    4GG;    4GG# 
->         34      21       5       3       1       1       1      15       5       2 
->       4gnX    4GnX      4r     4ry    8.cL      8a      8A     8a-     8A-    8a-J 
->          1       1       2       2       1       5       5       2       1       2 
->       8A-J    8a-L    8A-L   8a-XJ     8A#    8a#J    8A#J     8AA    8AA-    8AAJ 
->          1       4       1       1       2       1       1       3       1       7 
->       8AAL     8aJ     8AJ     8aL     8AL    8aL]    8AL]   8AnXL      8b     8b- 
->          4      15      17      10      12       1       2       1       2       1 
->        8B-    8b-J    8B-J    8b-L     8BB   8BB-J   8BB-L    8BBJ    8BBL     8bJ 
->          1       1       6       2       3       6       3       6       4      11 
->        8BJ     8bL     8BL      8c      8C     8C#    8c#J    8C#J    8c#L    8C#L 
->         15      14      18       1       5       2       3       2       5       4 
->      8c#XJ     8cc   8cc#J   8cc#L    8ccJ    8ccL     8cJ     8CJ     8cL     8CL 
->          1       2       2       2       7       7      15       8      14      14 
->       8cL]    8CL]   8cnXJ      8d      8D     8d-     8D-    8D-J    8D-L   8d-XJ 
->          1       1       1       5       3       1       6       4       2       1 
->       8d#J    8D#J    8d#L    8D#L     8dd   8dd#J    8ddJ    8ddL     8dJ     8DJ 
->          5       1       1       2       1       1       2       4      19      15 
->        8dL     8DL    8dL]    8dnJ      8e      8E     8E-    8e-J    8E-J    8e-L 
->         24       6       1       1       6       2       6       1       1       1 
->       8E-L    8EEJ    8eeL    8EEL     8eJ     8EJ     8eL     8EL    8eL]    8EL] 
->          4       1       2       1      20       7      35      15       2       1 
->         8f      8F     8f#     8F#    8f#J    8F#J    8f#L    8F#L   8f#L]   8F#XJ 
->          3       4       2       3      26      14      12       7       1       1 
->      8f#XL   8FF#J    8FFL     8fJ     8FJ     8fL     8FL    8fL]   8FnXL      8g 
->          1       2       2       4       5       5       6       1       1       3 
->         8G     8g#     8G#    8g#J    8G#J    8g#L    8G#L   8g#XJ     8GG    8GGJ 
->          4       4       4       7       7       5       3       1       2       2 
->       8GGL     8gJ     8GJ     8gL     8GL    8gL]    8GL]   8gnXL   8GnXL 
->          2      14       8      14      17       3       1       2       1
-

It worked! However, there’s a lot of unique tokens in this -data, so its kind of a mess. Let’s take a page from the pitch -vignette and extract just the pitch information using the -kern function:

-
with(chorales, 
-     kern(Token) |> table())
->    
->      a   A  a-  A-  a#  A#  AA AA- AA#   b   B  b-  B-  BB BB-   c   C  c#  C#  cc 
->    160 114  29  11   5   5  44   5   1 134 115  24  19  48  17 102  57  48  30  73 
->    cc#   d   D  d-  D-  d#  D#  dd  DD dd- dd#   e   E  e-  E-  e#  E#  ee  EE ee- 
->     37 134  70  12  16  21  12  48   1   7   4 213  89  13  17   4   4  25   7   4 
->      f   F  f#  F#  ff  FF ff# FF#   g   G  g-  g#  G#  gg  GG GG# 
->     74  30 111  65   7  10   1   5 134  73   1  61  46   1  29   2
-

Much better! Notice that the whole expression -kern(Token) |> table() is executed within the data. Our -expressions can get as complicated as we want. Let’s add a call to -sort:

-
with(chorales, 
-     kern(Token) |>  table() |>  sort())
->    
->    AA#  DD ff#  g-  gg GG# dd#  e#  E# ee-  a#  A# AA- FF# dd-  EE  ff  FF  A-  d- 
->      1   1   1   1   1   2   4   4   4   4   5   5   5   5   7   7   7  10  11  12 
->     D#  e-  D- BB-  E-  B-  d#  b-  ee  a-  GG  C#   F cc#  AA  G#  BB  c#  dd   C 
->     12  13  16  17  17  19  21  24  25  29  29  30  30  37  44  46  48  48  48  57 
->     g#  F#   D  cc   G   f   E   c  f#   A   B   b   d   g   a   e 
->     61  65  70  73  73  74  89 102 111 114 115 134 134 134 160 213
-

Ah, now we can clearly see that the top 10 most common notes in the -data are e, a, g, d, b, B, A, f#, c, E.

-
-

The last step might be to make a plot. We’ll use the built in -tail function to grab the top (last) 10, and the built-in -barplot function.

-
-with(chorales, 
-     kern(Token) |> 
-       table() |> 
-       sort() |> 
-       tail(n = 10) |> 
-       barplot())
-

-

Notice that when writing a big complex expression like

-
 kern(Token) |> 
-       table() |> 
-       sort() |> 
-       tail(n = 10) |> 
-       barplot()
-

it’s helpful to spread the expression across multiple lines. After -the |> (pipe) operator is a great place for this.

-
-
-

Within -

-

In many cases, we want to apply functions to our humdrum data, but -keep the humdrum/humdrum\(_{\mathbb{R}}\) structure in place. This is -what within is for. Where with just returns -what ever the output of your commands are, within will -(attempt to) put the results of your commands “back inside” the -humdrum\(_{\mathbb{R}}\) data object, -as a new field.

-

For example, let’s say we want to translate our kern -data into a different pitch representation, like scientific pitch. We -can do this with the pitch command. If we use -with

-
with(chorales, pitch(Token))
->    **pitch (character)
->       [1] G2  G3  E3  F#3 G3  D3  E3  C3  B2  A2  G2  D3  G2  F#2 G2  A2  B2  C3 
->      [19] D3  G2  G2  G2  A2  B2  B2  A2  G2  D3  E3  E3  D3  C3  B2  C3  D3  G2 
->      [37] A2  B2  G2  C3  G2  F#2 G2  A2  B2  G2  D3  E3  D3  C3  B2  A2  G2  D3 
->      [55] G3  G3  F#3 E3  E3  D3  C3  D3  G2  B3  B3  C4  B3  A3  G3  F#3 G3  C4 
->      [73] B3  C4  D4  D4  D4  A3  B3  C4  D4  E4  D4  C4  B3  D4  D4  C4  B3  A3 
->      [91] B3  C4  D4  D4  D4  B3  G3  B3  E4  D4  D4  D4  C4  D4  D4  C4  B3  C4 
->     [109] D4  D4  C4  B3  C4  D4  D4  D4  D4  E4  E4  D4  C4  B3  D4  D4  E4  D4 
->     [127] D4  B3  E4  D4  E4  F#4 G4  F#4 G4  D4  E4  F#4 G4  F#4 D4  G4  G4  F#4
->     [145] E4  F#4 G4  G4  A4  G4  F#4 G4  F#4 E4  E4  F#4 G4  A4  A4  G4  F#4 G4 
->     [163] F4  E4  G4  A4  G4  F#4 G4  F#4 F#4 E4  E4  F#4 G4  F#4 G4  A4  G4  F#4
->     [181] G4  F#4 D4  G4  G4  D5  B4  A4  G4  G4  A4  B4  A4  B4  D5  C5  B4  A4 
->     [199] G4  B4  B4  C5  D5  D5  C5  B4  A4  G4  B4  C5  D5  C5  B4  G4  B4  D5 
->     [217] C5  B4  A4  G4  A4  B4  A4  B4  D5  C5  B4  A4  G4  A3  G#3 F#3 C#3 D3 
->     [235] D#3 E3  B2  E2  E3  A3  B3  C#4 B3  A3  B3  B2  E3  C#3 F#3 G#3 A3  E3 
->     [253] C#3 A2  D3  C#3 D3  E3  C#3 B2  C#3 D3  G3  F#3 B2  C#3 F#3 E3  D3  C#3
->     [271] B2  A2  B2  C#3 D3  E3  B2  F#3 G#3 A3  G#3 E3  A3  A3  G#3 F#3 E3  D3 
->     [289] E3  A2  C#4 C#4 C#4 B3  A3  G#3 F#3 B3  A3  G#3 E4  E4  D#4 C#4 B3  A3 
->     [307] G#3 G#3 A3  B3  C#4 D4  E4  E4  D4  E4  E4  E4  F#4 B3  A#3 B3  C#4 D4 
->     [325] G#3 A3  G#3 F#3 E3  E4  D4  C#4 D4  E4  F#4 G#4 F#4 F#4 E4  D4  C#4 B3 
->     [343] C#4 D4  E4  D4  C#4 F#4 B3  E4  D4  C#4 E4  F#4 E4  F#4 F#4 E4  D#4 B3 
->     [361] G#4 A4  G#4 F#4 E4  E4  D#4 B3  C#5 B4  A4  A4  G#4 A4  G4  F#4 G#4 A4 
->     [379] G#4 A#4 B4  A4  G4  F#4 E4  F#4 F#4 E4  D4  C#4 D4  E4  F#4 G#4 A4  E4 
->     [397] B4  A4  G#4 F#4 E4  F#4 G#4 A4  A4  G#4 E4  A4  A4  A4  A4  B4  G4  F#4
->     [415] E4  B4  C#5 B4  A4  G#4 F#4 G#4 F#4 E4  E5  D5  C#5 B4  A4  A4  B4  C#5
->     [433] B4  C#5 D5  C#5 B4  A#4 B4  E4  A4  B4  C#5 D5  E5  D5  C#5 B4  D5  C#5
->     [451] B4  E5  D5  C#5 B4  A4  B4  C#5 B4  A4  E3  A3  B3  C4  B3  A3  G#3 A3 
->     [469] E3  B2  C3  D3  E3  F3  E3  D3  E3  A2  F#3 G3  F#3 E3  B3  A3  G3  F#3
->     [487] E3  D3  C3  B2  E3  F3  C3  D3  E3  A2  B2  C3  D3  E3  A2  A3  G#3 A3 
->     [505] G3  F3  E3  D3  C#3 D3  D#3 E3  E4  E4  D4  E4  D4  C4  B3  C4  D4  E4 
->     [523] F4  E4  E4  D4  C4  D4  G#3 A3  G#3 C4  A3  G3  A3  B3  B3  B3  A3  B3 
->     [541] C4  F#3 E4  D4  C4  D4  E4  E4  E4  D4  C4  B3  C4  C4  B3  A3  A3  Bb3
->     [559] A3  E3  F#3 G#3 G#4 A4  G#4 A4  G#4 A4  B4  E4  F#4 G#4 G#4 A4  G#4 A4 
->     [577] G4  F4  E4  E4  D4  D4  D#4 E4  D#4 E4  D#4 E4  G4  F#4 E4  D#4 B3  A3 
->     [595] A4  G#4 A4  E4  E4  E4  E4  E4  E4  F4  G4  F#4 G#4 A4  E4  B4  C5  B4 
->     [613] A4  E5  E5  D5  C5  B4  D5  C5  B4  A4  B4  C5  D5  C5  B4  A4  A4  B4 
->     [631] A4  G4  F#4 E4  F#4 G4  A4  B4  G4  A4  B4  C5  B4  C5  B4  A4  G#4 A4 
->     [649] A4  E5  C5  D5  E5  D5  C5  B4  E3  D#3 B2  E3  F#3 G#3 A3  E3  A2  D#3
->     [667] E3  F#3 G#3 C#3 F#3 E3  D#3 C#3 B2  F#3 B2  E3  C#3 D#3 E3  F#3 B3  G#3
->     [685] E3  F#3 B2  E3  G#3 E3  A3  B3  C#4 E#3 F#3 C#3 E3  B2  C#3 D#3 E3  B2 
->     [703] A#2 B2  E2  E4  F#4 E4  D#4 E4  A3  B3  C#4 D4  E4  D4  C#4 B3  B3  A3 
->     [721] G#3 F#3 F#3 B3  A#3 F#3 G#3 C#4 F#3 F#4 F#4 B3  A#3 D#4 E4  B3  E4  E4 
->     [739] C#4 D4  C#4 C#4 B3  B3  E4  E4  D#4 B3  F#3 G#3 G#4 F#4 G#4 A4  G#4 F#4
->     [757] E4  A4  G#4 E4  F#4 E4  E4  D#4 E4  C#4 F#4 E4  D#4 E4  D#4 E4  F#4 G#4
->     [775] A#4 B4  A#4 B4  B4  F#4 F#4 G#4 E4  F#4 G#4 A4  E4  F#4 G#4 F#4 E#4 E4 
->     [793] D#4 C#4 B3  B3  C#4 B3  B3  B4  B4  B4  B4  D5  C#5 B4  A4  B4  G#4 E4 
->     [811] F#4 G#4 A#4 B4  C#5 B4  B4  E5  D#5 C#5 D#5 E5  D#5 C#5 B4  B4  E5  B4 
->     [829] C#5 G#4 A4  B4  A4  G#4 G#4 F#4 A4  G#4 F#4 C#4 D#4 E4  G3  C3  D3  E3 
->     [847] F#3 G3  A3  D3  G3  F#3 G3  F#3 E3  B2  C3  D3  G2  G3  F#3 E3  D3  E3 
->     [865] F#3 G3  D3  G2  C3  G3  F3  E3  D3  C3  B2  C3  D3  E3  A2  D3  G3  A3 
->     [883] B3  C4  F#3 G#3 A3  A2  E3  G#2 A2  B2  C3  A2  F3  E3  D3  E3  E2  A2 
->     [901] A2  B2  C#3 D3  C#3 D3  A2  D3  B2  E3  D3  G3  F#3 E3  B2  A2  G2  C3 
->     [919] B2  A2  D3  C3  D3  G2  B3  C4  B3  A3  E4  A3  B3  C4  E4  D4  D4  D4 
->     [937] D4  G3  A3  B3  G3  E4  A3  B3  C4  B3  B3  A3  D4  G3  A3  A3  G3  G3 
->     [955] F#3 G3  G3  G3  G3  G3  G3  G3  A3  B3  A3  A3  G3  G3  D4  C4  B3  A3 
->     [973] E4  E4  E4  E4  D4  C4  B3  A3  A3  G#3 A3  B3  G#3 A3  G#3 A3  E3  F#3
->     [991] G3  D4  C4  Bb3 A3  A3  G3  F#3 F#3 G3  D4  D4  E4  D4  E4  D4  C4  D4 
->    [1009] E4  D4  C4  B3  G4  G4  F#4 G4  F#4 E4  G4  G4  F#4 G4  A4  G4  G4  G4 
->    [1027] G4  G4  F#4 D4  D4  D4  E4  F#4 E4  E4  D4  D4  C4  B3  E4  D4  E4  F4 
->    [1045] E4  F#4 G4  F#4 G4  A4  F#4 D4  G4  G4  A4  B4  B4  G#4 A4  G#4 B4  A4 
->    [1063] E4  C4  D4  E4  F4  E4  D4  C4  A4  D4  E4  F4  E4  D4  E4  E4  D4  D4 
->    [1081] C#4 D4  D4  C4  B3  A3  G3  F#3 G3  G4  F#4 G4  G4  G4  G4  F#4 D4  D5 
->    [1099] E5  D5  C5  B4  C5  D5  C5  B4  C5  B4  A4  B4  C5  D5  C5  B4  A4  G4 
->    [1117] A4  G4  G4  A4  B4  C5  B4  A4  B4  A4  G4  G4  A4  B4  C5  D5  E5  B4 
->    [1135] C#5 D5  B4  C5  D5  E5  D5  C5  B4  C5  B4  E5  E5  E5  A4  D5  C5  B4 
->    [1153] A4  C5  B4  A4  G4  A4  G4  F4  E4  D4  D4  G4  A4  B4  C5  D5  C5  B4 
->    [1171] A4  B4  C5  A4  G4  F2  F3  E3  Eb3 D3  C3  D3  E3  C3  F3  Bb2 A2  G2 
->    [1189] F2  A2  C3  F2  .   A3  B2  C3  D3  F3  A3  G3  F3  D3  G3  C3  F2  G2 
->    [1207] A2  Bb2 C3  F2  A3  C4  C4  C4  D4  G3  C4  C4  Bb3 C4  D4  Bb3 C4  C4 
->    [1225] C4  .   F4  F4  E4  E4  D4  C4  C4  B3  E4  C4  Bb3 C4  D4  C4  Bb3 A3 
->    [1243] C4  F4  G4  F4  F4  E4  F4  G4  E4  F4  F4  F4  E4  F4  E4  F4  .   A4 
->    [1261] A4  G4  G4  C5  Bb4 A4  A4  A4  G4  G4  F4  F4  E4  F4  F4  E4  C4  F4 
->    [1279] A4  G4  A4  Bb4 C5  A4  D5  C5  Bb4 A4  G4  A4  .   C5  D5  E5  F5  E5 
->    [1297] D5  C5  A4  Bb4 A4  G4  G4  F4  A3  F#3 C#3 D3  D3  C#3 D3  E3  A2  A3 
->    [1315] E#3 C#3 F#3 E3  D3  C#3 D3  E3  A2  A2  D3  A3  G#3 F#3 E#3 F#3 B2  C#3
->    [1333] F#2 D#3 E3  D3  C#3 A2  B2  E3  C#3 F#3 E3  F#3 G#3 F#3 E3  A3  A2  D3 
->    [1351] C#3 B2  B3  A3  G3  F#3 E3  F#3 G3  F#3 B2  E3  A3  G#3 F#3 B3  A3  G3 
->    [1369] C#3 D3  A3  D#3 E3  A2  B2  E3  C#3 F#3 E3  B2  C#3 D3  D#3 E3  E#3 F#3
->    [1387] G#3 A3  E3  F#3 D3  E3  E3  A2  C#4 C#4 C#4 A3  G#3 A3  G#3 A3  C#4 C#4
->    [1405] B3  A3  E4  E4  F#4 E4  C#4 C#4 D4  E4  E#4 F#4 G#4 C#4 D4  C#4 A3  A3 
->    [1423] G#3 A3  B3  C#4 B3  A3  G#3 G#3 A3  D4  B3  E4  E4  D4  E4  F#4 B3  E4 
->    [1441] D4  C#4 B3  A#3 B3  G#3 A3  A3  B3  B3  B3  A3  G3  F#3 A3  F#3 B3  C#4
->    [1459] B3  A3  G#3 G#3 A3  B3  B3  A3  G#3 D4  A3  G#3 C#4 F#4 E4  E4  E4  D4 
->    [1477] C#4 F#4 E4  D4  C#4 E4  F#4 E4  D4  C#4 B3  E4  F#4 E4  E4  E4  F#4 G#4
->    [1495] E#4 F#4 G#4 A4  G#4 E4  E4  F#4 G#4 A4  B4  A4  G#4 F#4 E#4 C#4 F#4 E4 
->    [1513] E4  E4  D#4 B3  C#4 C#4 F#4 E4  F#4 G#4 A4  G4  F#4 A4  B4  B4  A4  G4 
->    [1531] A4  B4  E4  D#4 E4  E4  F#4 F#4 G4  G4  F#4 E4  D4  E4  F#4 E4  E4  D#4
->    [1549] B3  C#4 C#4 D#4 E4  D#4 E4  F#4 G#4 A4  E4  G#4 A4  B4  A4  G#4 A4  A4 
->    [1567] G#4 E4  A4  A4  G#4 F#4 E4  A4  B4  C#5 C#5 C#5 B4  C#5 C#5 B4  A4  B4 
->    [1585] A4  A4  A4  B4  C#5 B4  C#5 A4  G#4 F#4 B4  B4  A4  G#4 A4  F#4 E4  E4 
->    [1603] A4  A4  B4  B4  C#5 B4  C#5 A4  A4  D5  D5  C#5 B4  C#5 B4  B4  C#5 C#5
->    [1621] D5  D5  E5  A4  C#5 B4  A4  G#4 A4  F#4 E4  E4  A4  G#4 F#4 E4  B4  C#5
->    [1639] B4  C#5 D5  C#5 B4  A4  G#4 A4  B4  C#5 B4  A4  F3  Eb3 Db3 C3  Bb2 C3 
->    [1657] F2  F2  F3  Eb3 Db3 C3  Bb2 Ab2 Db3 Eb3 Ab2 Db3 C3  Bb2 C3  Db3 Bb2 C3 
->    [1675] F2  F3  G3  A3  F3  Bb2 C3  Db3 Bb2 Eb3 F3  G3  Eb3 Ab3 Ab3 Db3 Eb3 F3 
->    [1693] Db3 Bb2 C3  Db3 Bb2 Eb3 C3  F3  Bb2 Bb3 Ab3 G3  F3  Bb2 C3  F2  F2  G2 
->    [1711] Ab2 Bb2 C3  D3  Eb3 F3  G3  G2  C3  C3  F3  Eb3 Db3 C3  Bb2 C3  Db3 Eb3
->    [1729] Eb3 Ab2 Ab2 Db3 Eb3 Db3 C3  Bb2 C3  Db3 Bb2 Eb3 F3  Eb3 Db3 C3  Db3 Eb3
->    [1747] C3  F3  E3  F3  G3  Ab3 G3  Ab3 Bb3 C4  C3  F3  F3  Ab3 A3  Bb3 C4  Db4
->    [1765] C4  Bb3 Ab3 Ab3 C4  Bb3 Eb4 Eb4 F4  Eb4 Db4 C4  Db4 Db4 G3  F3  Ab3 Db4
->    [1783] C4  Bb3 Ab3 A3  Bb3 C4  A3  Bb3 Bb3 Bb3 Eb4 Eb4 Eb4 Db4 Db4 F4  F4  Gb4
->    [1801] F4  Eb4 Db4 C4  C4  Bb3 C4  Db4 C4  Bb3 Ab3 C4  C4  C4  C4  C4  B3  C4 
->    [1819] G3  A3  A3  Bb3 Eb3 Eb4 Db4 C4  C4  Db4 Db4 F4  F4  Eb4 Eb4 G4  G4  F4 
->    [1837] F4  C4  F3  C4  C4  C4  C4  F4  F4  F4  E4  F4  F4  G4  E4  F4  C4  F4 
->    [1855] G4  Ab4 Ab4 Ab4 F4  G4  Eb4 F4  G4  E4  F4  F4  E4  C4  F4  F4  F4  F4 
->    [1873] G4  Ab4 Bb4 G4  Ab4 Ab4 F4  F4  Bb4 Bb4 Bb4 A4  F4  E4  F4  F4  E4  F4 
->    [1891] F4  E4  C4  Ab4 Ab4 G4  G4  G4  Ab4 G4  F4  E4  E4  F4  F4  F4  G4  Ab4
->    [1909] Ab4 F4  G4  Eb4 Eb4 F4  F4  Bb4 Bb4 G4  G4  C5  C5  Ab4 G4  Ab4 Bb4 C5 
->    [1927] Bb4 Ab4 G4  G4  A4  A4  C5  C5  Bb4 Ab4 G4  F4  F4  F4  Ab4 Bb4 C5  C5 
->    [1945] Bb4 Ab4 Ab4 Ab4 Bb4 Bb4 Ab4 G4  F4  F4  C5  C5  Db5 Db5 Bb4 Bb4 C5  C5 
->    [1963] Ab4 Ab4 Db5 Db5 C5  C5  Bb4 G4  Ab4 Bb4 Ab4 G4  G4  F4  F5  F5  Eb5 Eb5
->    [1981] D5  D5  C5  C5  C5  C5  Db5 C5  Bb4 Ab4 Ab4 Ab4 Ab4 Ab4 Db5 Db5 Bb4 Bb4
->    [1999] Eb5 Eb5 C5  C5  F5  F5  E5  E5  F5  F5  G3  F#3 E3  D3  G3  F#3 G3  A3 
->    [2017] A2  D3  D#3 E3  F#3 G3  C3  C3  B2  A2  G2  D3  D2  G2  D3  D4  C4  B3 
->    [2035] A3  D#3 E3  E2  A2  E3  F#3 G3  G#3 A3  A#3 B3  B2  E3  D3  C3  B2  A2 
->    [2053] G2  F#2 E2  F#2 G2  E2  C#3 B2  C#3 A2  D3  B2  C3  D3  E3  F#3 G3  C3 
->    [2071] D3  G2  B3  B3  C4  C4  D4  B3  A3  A3  A3  A3  A3  B3  A3  G3  A3  B3 
->    [2089] C4  D4  D4  D4  C4  B3  D4  E4  F#4 E4  D4  E4  F#4 B3  E4  D4  C4  G3 
->    [2107] A3  B3  C4  D4  E4  C4  C#4 B3  B3  B3  D4  D4  G4  G3  A3  B3  E4  D4 
->    [2125] E4  C#4 D4  D4  D4  C4  C4  B3  B3  A3  G3  G3  F#3 E3  F#3 C4  B3  D4 
->    [2143] E4  F#4 D4  E4  F#4 E4  F#4 G4  F#4 F#4 E4  D4  E4  F#4 G4  G4  F#4 D4 
->    [2161] F#4 G4  A4  G#4 A4  G#4 A4  A4  G#4 E4  E4  D#4 E4  E4  E4  F#4 G4  F#4
->    [2179] F#4 G4  A4  G4  A4  G4  A4  B4  B4  A4  A4  G4  G4  F#4 G4  F#4 E4  F#4
->    [2197] G4  A4  D4  E4  D4  D4  G4  G4  A4  B4  C#5 D5  D5  C#5 D5  B4  C5  B4 
->    [2215] A4  B4  A4  G4  A4  A4  B4  C5  C5  B4  A4  B4  A4  B4  B4  B4  C5  D5 
->    [2233] E5  E5  D#5 E5  F#5 G5  B4  C5  B4  A4  A4  D5  E5  D5  C5  B4  C5  A4 
->    [2251] G4  D3  C3  B2  A2  B2  C3  B2  A2  E3  E3  A3  G3  F3  E3  F3  C3  D3 
->    [2269] E3  C4  B3  A3  G3  F3  E3  D3  E3  A2  A3  E3  D3  C3  B2  C3  D3  D3 
->    [2287] G2  C3  A2  E3  F3  C3  Bb2 A2  G#2 A2  E2  G#3 A3  D4  E4  D4  E4  D4 
->    [2305] C4  B3  E4  E4  F4  G4  C4  C4  B3  A3  G#3 A3  B3  C4  D4  D4  E4  F4 
->    [2323] B3  C4  E4  E4  E4  D4  C4  F#3 G3  F#3 G3  G3  A3  G#3 A3  G3  G3  A3 
->    [2341] B3  A3  G#3 E4  E4  G#4 A4  G4  F#4 E4  F#4 G#4 G#4 A4  B4  C5  F4  E4 
->    [2359] D4  C4  B3  E4  D4  E4  F#4 G4  A4  B4  A4  G#4 E4  A4  G4  G4  G4  F4 
->    [2377] E4  D4  C4  B3  E4  E4  E4  D4  C4  C4  D4  D4  C4  B3  B4  E4  B4  C5 
->    [2395] B4  A4  G4  A4  B4  B4  C5  D5  C5  B4  A4  G4  F4  E4  A4  G4  C5  B4 
->    [2413] A4  D5  C5  B4  A4  C5  B4  C5  D5  G4  B4  A4  G4  G4  C5  B4  A4  E4 
->    [2431] G4  F4  E4
-

we just get all the notes! Let’s try within instead:

-
within(chorales, pitch(Token))
->    ############################# vvv chor001.krn vvv #############################
->         1:  !!!COM: Bach, Johann Sebastian
->         2:  !!!CDT: 1685/02/21/-1750/07/28/
->         3:  !!!OTL@@DE: Aus meines Herzens Grunde
->         4:  !!!OTL@EN:      From the Depths of My Heart
->         5:  !!!SCT: BWV 269
->         6:  !!!PC#: 1
->         7:  !!!AGN: chorale
->         8:         **pitch       **pitch       **pitch       **pitch
->         9:          *ICvox        *ICvox        *ICvox        *ICvox
->        10:          *Ibass       *Itenor        *Ialto       *Isoprn
->        11:         *I"Bass      *I"Tenor       *I"Alto    *I"Soprano
->        12:       *>[A,A,B]     *>[A,A,B]     *>[A,A,B]     *>[A,A,B]
->        13:    *>norep[A,B]  *>norep[A,B]  *>norep[A,B]  *>norep[A,B]
->        14:             *>A           *>A           *>A           *>A
->        15:         *clefF4      *clefGv2       *clefG2       *clefG2
->        16:          *k[f#]        *k[f#]        *k[f#]        *k[f#]
->        17:             *G:           *G:           *G:           *G:
->        18:           *M3/4         *M3/4         *M3/4         *M3/4
->        19:          *MM100        *MM100        *MM100        *MM100
->        20:              G2            B3            D4            G4
->        21:              =1            =1            =1            =1
->        22:              G3            B3            D4            G4
->        23:              E3            C4            E4             .
->        24:               .            B3             .             .
->        25:             F#3            A3            D4            D5
->        26:              =2            =2            =2            =2
->        27:              G3            G3            D4            B4
->        28:              D3           F#3             .             .
->        29:               .             .             .            A4
->        30:              E3            G3            B3            G4
->        31:              =3            =3            =3            =3
->        32:              C3            C4            E4            G4
->        33:               .            B3            D4             .
->        34:              B2            C4            E4             .
->        35:              A2             .           F#4            A4
->        36:              G2            D4            G4            B4
->        37:              =4            =4            =4            =4
->        38:              D3            D4           F#4            A4
->        39:              G2            D4            G4            B4
->        40:              =5            =5            =5            =5
->    41-133:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
->    ############################# ^^^ chor001.krn ^^^ #############################
->    
->           (8 more pieces...)
->    
->    ############################# vvv chor010.krn vvv #############################
->      1-60:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
->        61:              A2            C4            E4            A4
->        62:              A3            E4            A4            C5
->        63:              =9            =9            =9            =9
->        64:              E3            E4            G4            B4
->        65:              D3            E4            G4            C5
->        66:              C3             .             .             .
->        67:              B2            D4            G4            D5
->        68:               .             .            F4             .
->        69:              C3            C4            E4            G4
->        70:             =10           =10           =10           =10
->        71:              D3           F#3            D4            B4
->        72:               .            G3             .             .
->        73:              D3             .            C4            A4
->        74:               .           F#3             .             .
->        75:              G2            G3            B3            G4
->        76:             =11           =11           =11           =11
->        77:              C3            G3            E4            G4
->        78:              A2            A3            E4            C5
->        79:              E3           G#3            E4            B4
->        80:               .             .            D4             .
->        81:             =12           =12           =12           =12
->        82:              F3            A3            C4            A4
->        83:              C3            G3            C4            E4
->        84:             Bb2            G3            D4            G4
->        85:              A2            A3             .            F4
->        86:             =13           =13           =13           =13
->        87:             G#2            B3            D4            E4
->        88:              A2            A3            C4             .
->        89:              E2           G#3            B3             .
->        90:              ==            ==            ==            ==
->        91:              *-            *-            *-            *-
->        92:  !!!hum2abc: -Q ''
->        93:  !!!title: @{PC#}. @{OTL@@DE}
->        94:  !!!YOR1: 371 vierstimmige Choralges&auml;nge von Johann Sebastian B***
->        95:  !!!YOR2: 4th ed. by Alfred D&ouml;rffel (Leipzig: Breitkopf und H&a***
->        96:  !!!YOR2: c.1875). 178 pp. Plate "V.A.10".  reprint: J.S. Bach, 371 ***
->        97:  !!!YOR4: Chorales (New York: Associated Music Publishers, Inc., c.1***
->        98:  !!!SMS: B&H, 4th ed, Alfred D&ouml;rffel, c.1875, plate V.A.10
->        99:  !!!EED:  Craig Stuart Sapp
->       100:  !!!EEV:  2009/05/22
->    ############################# ^^^ chor010.krn ^^^ #############################
->                           (***four global comments truncated due to screen size***)
->    
->       humdrumR corpus of ten pieces.
->        Data fields: 
->                Token   :: character
->               *Result1 :: character (**pitch tokens)
-

Now we get our humdrum\(_{\mathbb{R}}\) data back, but the -Token data has been transformed from **kern to -**pitch. Notice, that there are now two “Data fields” -listed at the bottom: Token (the original data) and -Result1—this is the default name for new fields produced by -within, and yes, they will get higher numbers if you keep -making new fields.

-

“Result1” is not very informative, so it’s actually a good idea to -give our new fields specific names. We can do this by assigning our -expressions in within:

-
within(chorales, Pitch <- pitch(Token)) -> chorales
-
-chorales
->    ############################# vvv chor001.krn vvv #############################
->         1:  !!!COM: Bach, Johann Sebastian
->         2:  !!!CDT: 1685/02/21/-1750/07/28/
->         3:  !!!OTL@@DE: Aus meines Herzens Grunde
->         4:  !!!OTL@EN:      From the Depths of My Heart
->         5:  !!!SCT: BWV 269
->         6:  !!!PC#: 1
->         7:  !!!AGN: chorale
->         8:         **pitch       **pitch       **pitch       **pitch
->         9:          *ICvox        *ICvox        *ICvox        *ICvox
->        10:          *Ibass       *Itenor        *Ialto       *Isoprn
->        11:         *I"Bass      *I"Tenor       *I"Alto    *I"Soprano
->        12:       *>[A,A,B]     *>[A,A,B]     *>[A,A,B]     *>[A,A,B]
->        13:    *>norep[A,B]  *>norep[A,B]  *>norep[A,B]  *>norep[A,B]
->        14:             *>A           *>A           *>A           *>A
->        15:         *clefF4      *clefGv2       *clefG2       *clefG2
->        16:          *k[f#]        *k[f#]        *k[f#]        *k[f#]
->        17:             *G:           *G:           *G:           *G:
->        18:           *M3/4         *M3/4         *M3/4         *M3/4
->        19:          *MM100        *MM100        *MM100        *MM100
->        20:              G2            B3            D4            G4
->        21:              =1            =1            =1            =1
->        22:              G3            B3            D4            G4
->        23:              E3            C4            E4             .
->        24:               .            B3             .             .
->        25:             F#3            A3            D4            D5
->        26:              =2            =2            =2            =2
->        27:              G3            G3            D4            B4
->        28:              D3           F#3             .             .
->        29:               .             .             .            A4
->        30:              E3            G3            B3            G4
->        31:              =3            =3            =3            =3
->        32:              C3            C4            E4            G4
->        33:               .            B3            D4             .
->        34:              B2            C4            E4             .
->        35:              A2             .           F#4            A4
->        36:              G2            D4            G4            B4
->        37:              =4            =4            =4            =4
->        38:              D3            D4           F#4            A4
->        39:              G2            D4            G4            B4
->        40:              =5            =5            =5            =5
->    41-133:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
->    ############################# ^^^ chor001.krn ^^^ #############################
->    
->           (8 more pieces...)
->    
->    ############################# vvv chor010.krn vvv #############################
->      1-60:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
->        61:              A2            C4            E4            A4
->        62:              A3            E4            A4            C5
->        63:              =9            =9            =9            =9
->        64:              E3            E4            G4            B4
->        65:              D3            E4            G4            C5
->        66:              C3             .             .             .
->        67:              B2            D4            G4            D5
->        68:               .             .            F4             .
->        69:              C3            C4            E4            G4
->        70:             =10           =10           =10           =10
->        71:              D3           F#3            D4            B4
->        72:               .            G3             .             .
->        73:              D3             .            C4            A4
->        74:               .           F#3             .             .
->        75:              G2            G3            B3            G4
->        76:             =11           =11           =11           =11
->        77:              C3            G3            E4            G4
->        78:              A2            A3            E4            C5
->        79:              E3           G#3            E4            B4
->        80:               .             .            D4             .
->        81:             =12           =12           =12           =12
->        82:              F3            A3            C4            A4
->        83:              C3            G3            C4            E4
->        84:             Bb2            G3            D4            G4
->        85:              A2            A3             .            F4
->        86:             =13           =13           =13           =13
->        87:             G#2            B3            D4            E4
->        88:              A2            A3            C4             .
->        89:              E2           G#3            B3             .
->        90:              ==            ==            ==            ==
->        91:              *-            *-            *-            *-
->        92:  !!!hum2abc: -Q ''
->        93:  !!!title: @{PC#}. @{OTL@@DE}
->        94:  !!!YOR1: 371 vierstimmige Choralges&auml;nge von Johann Sebastian B***
->        95:  !!!YOR2: 4th ed. by Alfred D&ouml;rffel (Leipzig: Breitkopf und H&a***
->        96:  !!!YOR2: c.1875). 178 pp. Plate "V.A.10".  reprint: J.S. Bach, 371 ***
->        97:  !!!YOR4: Chorales (New York: Associated Music Publishers, Inc., c.1***
->        98:  !!!SMS: B&H, 4th ed, Alfred D&ouml;rffel, c.1875, plate V.A.10
->        99:  !!!EED:  Craig Stuart Sapp
->       100:  !!!EEV:  2009/05/22
->    ############################# ^^^ chor010.krn ^^^ #############################
->                           (***four global comments truncated due to screen size***)
->    
->       humdrumR corpus of ten pieces.
->        Data fields: 
->                Token :: character
->               *Pitch :: character (**pitch tokens)
-
-
-
-

With/Within Subsets -

-

The real power of the with and within -functions, comes from additional “control” arguments you can pass. Two -prominent examples allow you to automatically work with subsets of your -data: the subset and by arguments. We can -indicate a subset, or multiple subsets, and our expressions will be -evaluated separately within each subset.

-
-

With Subset -

-

The subset argument indicates a subset of the data which -we should evaluate “within.” There subset argument must be -an expression which evaluates to a logical vector (TRUE of -FALSE). The main command is then evaluated wherever the -subset expression evaluates to TRUE.

-

Let’s try it with our tabling example. Maybe we want to table only -tokens in the first spine:

-
with(chorales, 
-     kern(Token) |> table() |> sort() |> tail(n = 10),
-     subset = Spine == 1)
->    
->    GG C#  A  G AA F# BB  C  D  E 
->    29 30 36 36 44 44 48 57 70 85
-

We only see the low notes, which is what we’d expect from the bass -voice.

-

What about this, let’s only tabulate the even-numbered bars: For -datasets which have measures indicated (with = tokens), the -Bar field is integers counting these bars within each -piece. If we want even numbers, we can ask where is Bar -modulo 2 zero?: Bar %% 2 == 0.

-
with(chorales, 
-     kern(Token) |> table() |> sort() |> tail(n = 10),
-     subset = Bar %% 2 == 0)
->    
->      G   c  f#   A   B   b   d   g   a   e 
->     41  53  57  59  62  63  63  72  82 111
-

If we use the subset argument with within, -parts of the data which don’t match the within expression -are returned as null:

-

-within(chorales,
-       Pitch <- pitch(Token, simple = TRUE),
-       subset = Spine == 1)
->    ############################# vvv chor001.krn vvv #############################
->         1:  !!!COM: Bach, Johann Sebastian
->         2:  !!!CDT: 1685/02/21/-1750/07/28/
->         3:  !!!OTL@@DE: Aus meines Herzens Grunde
->         4:  !!!OTL@EN:      From the Depths of My Heart
->         5:  !!!SCT: BWV 269
->         6:  !!!PC#: 1
->         7:  !!!AGN: chorale
->         8:         **pitch       **pitch       **pitch       **pitch
->         9:          *ICvox        *ICvox        *ICvox        *ICvox
->        10:          *Ibass       *Itenor        *Ialto       *Isoprn
->        11:         *I"Bass      *I"Tenor       *I"Alto    *I"Soprano
->        12:       *>[A,A,B]     *>[A,A,B]     *>[A,A,B]     *>[A,A,B]
->        13:    *>norep[A,B]  *>norep[A,B]  *>norep[A,B]  *>norep[A,B]
->        14:             *>A           *>A           *>A           *>A
->        15:         *clefF4      *clefGv2       *clefG2       *clefG2
->        16:          *k[f#]        *k[f#]        *k[f#]        *k[f#]
->        17:             *G:           *G:           *G:           *G:
->        18:           *M3/4         *M3/4         *M3/4         *M3/4
->        19:          *MM100        *MM100        *MM100        *MM100
->        20:               G             .             .             .
->        21:              =1            =1            =1            =1
->        22:               G             .             .             .
->        23:               E             .             .             .
->        24:               .             .             .             .
->        25:              F#             .             .             .
->        26:              =2            =2            =2            =2
->        27:               G             .             .             .
->        28:               D             .             .             .
->        29:               .             .             .             .
->        30:               E             .             .             .
->        31:              =3            =3            =3            =3
->        32:               C             .             .             .
->        33:               .             .             .             .
->        34:               B             .             .             .
->        35:               A             .             .             .
->        36:               G             .             .             .
->        37:              =4            =4            =4            =4
->        38:               D             .             .             .
->        39:               G             .             .             .
->        40:              =5            =5            =5            =5
->    41-133:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
->    ############################# ^^^ chor001.krn ^^^ #############################
->    
->           (8 more pieces...)
->    
->    ############################# vvv chor010.krn vvv #############################
->      1-60:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
->        61:               A             .             .             .
->        62:               A             .             .             .
->        63:              =9            =9            =9            =9
->        64:               E             .             .             .
->        65:               D             .             .             .
->        66:               C             .             .             .
->        67:               B             .             .             .
->        68:               .             .             .             .
->        69:               C             .             .             .
->        70:             =10           =10           =10           =10
->        71:               D             .             .             .
->        72:               .             .             .             .
->        73:               D             .             .             .
->        74:               .             .             .             .
->        75:               G             .             .             .
->        76:             =11           =11           =11           =11
->        77:               C             .             .             .
->        78:               A             .             .             .
->        79:               E             .             .             .
->        80:               .             .             .             .
->        81:             =12           =12           =12           =12
->        82:               F             .             .             .
->        83:               C             .             .             .
->        84:              Bb             .             .             .
->        85:               A             .             .             .
->        86:             =13           =13           =13           =13
->        87:              G#             .             .             .
->        88:               A             .             .             .
->        89:               E             .             .             .
->        90:              ==            ==            ==            ==
->        91:              *-            *-            *-            *-
->        92:  !!!hum2abc: -Q ''
->        93:  !!!title: @{PC#}. @{OTL@@DE}
->        94:  !!!YOR1: 371 vierstimmige Choralges&auml;nge von Johann Sebastian B***
->        95:  !!!YOR2: 4th ed. by Alfred D&ouml;rffel (Leipzig: Breitkopf und H&a***
->        96:  !!!YOR2: c.1875). 178 pp. Plate "V.A.10".  reprint: J.S. Bach, 371 ***
->        97:  !!!YOR4: Chorales (New York: Associated Music Publishers, Inc., c.1***
->        98:  !!!SMS: B&H, 4th ed, Alfred D&ouml;rffel, c.1875, plate V.A.10
->        99:  !!!EED:  Craig Stuart Sapp
->       100:  !!!EEV:  2009/05/22
->    ############################# ^^^ chor010.krn ^^^ #############################
->                           (***four global comments truncated due to screen size***)
->    
->       humdrumR corpus of ten pieces.
->        Data fields: 
->                Token :: character
->               *Pitch :: character (**pitch tokens)
-

Notice, our new Pitch field is NULL in all -spine except spine 1.

-
-

Or else… -

-

In many cases when using subset we’d like to do -something with, or at least keep, the “other” part of the data—the -complement of the subset. We can do this by expressing a “complement” -expression. The idea is that we say “evaluate X where Y is true, -otherwise evaluate this instead.” This is easiest to understand if we -use our within example from above. Maybe we want to -calculate the simple pitch of the first spine, but leave the other -spines unchanged:

-

-within(chorales,
-       Pitch <- pitch(Token, simple = TRUE),
-       subset = Spine == 1,
-       complement = Token)
->    ############################# vvv chor001.krn vvv #############################
->         1:  !!!COM: Bach, Johann Sebastian
->         2:  !!!CDT: 1685/02/21/-1750/07/28/
->         3:  !!!OTL@@DE: Aus meines Herzens Grunde
->         4:  !!!OTL@EN:      From the Depths of My Heart
->         5:  !!!SCT: BWV 269
->         6:  !!!PC#: 1
->         7:  !!!AGN: chorale
->         8:         **pitch       **pitch       **pitch       **pitch
->         9:          *ICvox        *ICvox        *ICvox        *ICvox
->        10:          *Ibass       *Itenor        *Ialto       *Isoprn
->        11:         *I"Bass      *I"Tenor       *I"Alto    *I"Soprano
->        12:       *>[A,A,B]     *>[A,A,B]     *>[A,A,B]     *>[A,A,B]
->        13:    *>norep[A,B]  *>norep[A,B]  *>norep[A,B]  *>norep[A,B]
->        14:             *>A           *>A           *>A           *>A
->        15:         *clefF4      *clefGv2       *clefG2       *clefG2
->        16:          *k[f#]        *k[f#]        *k[f#]        *k[f#]
->        17:             *G:           *G:           *G:           *G:
->        18:           *M3/4         *M3/4         *M3/4         *M3/4
->        19:          *MM100        *MM100        *MM100        *MM100
->        20:               G            4B            4d            4g
->        21:              =1            =1            =1            =1
->        22:               G            4B            4d            2g
->        23:               E           8cL            4e             .
->        24:               .           8BJ             .             .
->        25:              F#            4A            4d           4dd
->        26:              =2            =2            =2            =2
->        27:               G            4G            2d           4.b
->        28:               D           4F#             .             .
->        29:               .             .             .            8a
->        30:               E            4G            4B            4g
->        31:              =3            =3            =3            =3
->        32:               C           8cL           8eL           4.g
->        33:               .           8BJ            8d             .
->        34:               B            4c            8e             .
->        35:               A             .          8f#J            8a
->        36:               G            4d            4g            4b
->        37:              =4            =4            =4            =4
->        38:               D           2d;          2f#;           2a;
->        39:               G            4d            4g            4b
->        40:              =5            =5            =5            =5
->    41-133:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
->    ############################# ^^^ chor001.krn ^^^ #############################
->    
->           (8 more pieces...)
->    
->    ############################# vvv chor010.krn vvv #############################
->      1-60:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
->        61:               A           2c;           2e;           2a;
->        62:               A            2e            2a           2cc
->        63:              =9            =9            =9            =9
->        64:               E            4e            4g            4b
->        65:               D            4e            4g           4cc
->        66:               C             .             .             .
->        67:               B            4d           8gL           4dd
->        68:               .             .           8fJ             .
->        69:               C            4c            4e            4g
->        70:             =10           =10           =10           =10
->        71:               D           8F#            4d            4b
->        72:               .            4G             .             .
->        73:               D             .            4c            4a
->        74:               .           8F#             .             .
->        75:               G           2G;           2B;           2g;
->        76:             =11           =11           =11           =11
->        77:               C            2G            2e            2g
->        78:               A            4A            4e           4cc
->        79:               E           4G#           8eL            4b
->        80:               .             .           8dJ             .
->        81:             =12           =12           =12           =12
->        82:               F            4A            4c            4a
->        83:               C            4G            4c            4e
->        84:              Bb            4G           [2d            4g
->        85:               A            4A             .            4f
->        86:             =13           =13           =13           =13
->        87:              G#            4B           4d]           1e;
->        88:               A            4A            4c             .
->        89:               E         2G#X;           2B;             .
->        90:              ==            ==            ==            ==
->        91:              *-            *-            *-            *-
->        92:  !!!hum2abc: -Q ''
->        93:  !!!title: @{PC#}. @{OTL@@DE}
->        94:  !!!YOR1: 371 vierstimmige Choralges&auml;nge von Johann Sebastian B***
->        95:  !!!YOR2: 4th ed. by Alfred D&ouml;rffel (Leipzig: Breitkopf und H&a***
->        96:  !!!YOR2: c.1875). 178 pp. Plate "V.A.10".  reprint: J.S. Bach, 371 ***
->        97:  !!!YOR4: Chorales (New York: Associated Music Publishers, Inc., c.1***
->        98:  !!!SMS: B&H, 4th ed, Alfred D&ouml;rffel, c.1875, plate V.A.10
->        99:  !!!EED:  Craig Stuart Sapp
->       100:  !!!EEV:  2009/05/22
->    ############################# ^^^ chor010.krn ^^^ #############################
->                           (***four global comments truncated due to screen size***)
->    
->       humdrumR corpus of ten pieces.
->        Data fields: 
->                Token :: character
->               *Pitch :: character (**pitch tokens)
-

This pattern, with comp = Token is quite common.

-
-
-
-

With Subgroups -

-

The by keyword arguments is short for “group by.” We can -indicate a field in our data to group the data by—our main call will -then be applied separately to each group, and all the groups will be -returned.

-

Let’s try it with our tabling example. Let’s group the data by spine, -so we tabulate each spine of data (voice) separately!

-
with(chorales, 
-     kern(Token) |> table() |> sort() |> tail(n = 10),
-     by = Spine)
->    $`1`
->    
->    GG C#  A  G AA F# BB  C  D  E 
->    29 30 36 36 44 44 48 57 70 85 
->    
->    $`2`
->    
->    B- F# G# c#  G  e  A  c  B  d 
->    17 20 29 34 35 75 76 83 85 88 
->    
->    $`3`
->    
->      B  d#   b   d  g#   f   a  f#   g   e 
->     16  16  19  43  44  50  67  83  88 121 
->    
->    $`4`
->    
->     b-  g#   e  ee cc#   g  dd  cc   a   b 
->     15  15  17  25  36  42  48  68  93 115
-

We get four different tables, one of each spine. If we want -to make multiple plots, we can just add barplot like -before.

-
-with(chorales, 
-     kern(Token) |> table() |> sort() |> tail(n = 10) |> barplot(),
-     by = Spine)
-

-
-

Let’s try something slightly different: Since our four spines -represent bass, tenor, alto, and soprano voices (in that order) I’d -expect that average pitch of each spine to be significantly different. -Let’s convert our pitch data to semitones (using semits) -and add a call to the base-R function mean: Watch

-
with(chorales, 
-     semits(Token) |> mean(),
-     by = Spine)
->             1          2          3          4 
->    -9.6692308 -0.1143317  5.6065041 10.5672192
-

Oops, that didn’t work! It’s because some of our Token -data are rests, like 4r. The semits function -doesn’t know how to read 4r so it returns an -NA value. Don’t worry, this is an easy fix, and a good -lesson for data analysis in general and R in particular. We always need -to keep in mind what data is “missing” or Not Applicable -(NA). Most R math functions have an option to ignore -missing data by setting na.rm = TRUE (NA remove):

-
with(chorales, 
-     semits(Token) |> mean(na.rm = TRUE),
-     by = Spine)
->             1          2          3          4 
->    -9.6692308 -0.1143317  5.6065041 10.5672192
-

That’s what we expected! Of course, we might prefer a plot.

-
-with(chorales, 
-     semits(Token) |> mean(na.rm = TRUE),
-     by = Spine) |> barplot(names.arg = c('Bass', 'Tenor', 'Alto', 'Soprano'), 
-                            main = 'Mean Pitch by Voice',
-                            ylab = 'Semitones')
-

-

Notice that we did something differently here! We don’t want to make -a separate plot of each group-by calculation—that would just be -a single number for each spine. We want to make a plot of all four -numbers, so we put the barplot call after -(outside) the with call. Notice we can also control the -labels for our barplot using the names.arg argument.

-

Maybe we want to inspect a whole histogram of the pitches in each -voice. We can use the base-R hist function; Since we want a -histogram of each spine, we will want to put hist -“inside” the call to with again:

-
-with(chorales, 
-     semits(Token) |> hist(xlim = c(-24, 24), main = Instrument[1]),
-     by = Spine) 
-

-

Pro tips: by setting the xlim (x-limit) argument, we -make sure that all four barplots cover the same ranges of pitches, so -they are comparable. We also set the main argument (figure title) using -the Instrument field, which gives us the names of each -instrument. That’s right, we can refer to any/all fields in the humdrum -data! (Not all datasets have this tandem interpretation, so it wouldn’t -always work.)

-
-

Advanced Grouping -

-

We can group our data by any arbitrary grouping in our data. In fact -the by keyword argument can be a complex expression, as -long as its output is the same length as its input. For example, maybe -we’d like to reproduce our histograms, but lumping the male (bass/tenor) -and female (alto/soprano) voices together.

-
-with(chorales, 
-     semits(Token) |> hist(xlim = c(-24, 24), main = paste(unique(Instrument), sep = ' and ')),
-     by = Spine < 3) 
-

-

The expression Spine < 3 returns FALSE -for spines 1 and 2 (bass and tenor) and TRUE for spines 3 -and 4. These two categories (FALSE and TRUE) -are then used to group the data.

-
-
-
-

Recycling results -

-

In some cases, we’d like to perform a command within our -data which might output a smaller vector than in the input, including a -single value, but we still want to reconstruct the data fully. In R, -this is usually called “recycling”—i.e., repeating a value until it -matches a certain length. When using with or -within, we can cause our results to be recycled to the full -field length by using the recycle argument for our -expression. This is useful if you want to group by something, and fill -each group with the same value:

-

-within(chorales,
-       Semits <- semits(Token)) -> chorales
-
-within(chorales, 
-       recycle = BarBassNote <- min(Semits),
-       by = list(File, Bar)) -> chorales
-
-chorales
->    ############################# vvv chor001.krn vvv #############################
->         1:  !!!COM: Bach, Johann Sebastian
->         2:  !!!CDT: 1685/02/21/-1750/07/28/
->         3:  !!!OTL@@DE: Aus meines Herzens Grunde
->         4:  !!!OTL@EN:      From the Depths of My Heart
->         5:  !!!SCT: BWV 269
->         6:  !!!PC#: 1
->         7:  !!!AGN: chorale
->         8:        **semits      **semits      **semits      **semits
->         9:          *ICvox        *ICvox        *ICvox        *ICvox
->        10:          *Ibass       *Itenor        *Ialto       *Isoprn
->        11:         *I"Bass      *I"Tenor       *I"Alto    *I"Soprano
->        12:       *>[A,A,B]     *>[A,A,B]     *>[A,A,B]     *>[A,A,B]
->        13:    *>norep[A,B]  *>norep[A,B]  *>norep[A,B]  *>norep[A,B]
->        14:             *>A           *>A           *>A           *>A
->        15:         *clefF4      *clefGv2       *clefG2       *clefG2
->        16:          *k[f#]        *k[f#]        *k[f#]        *k[f#]
->        17:             *G:           *G:           *G:           *G:
->        18:           *M3/4         *M3/4         *M3/4         *M3/4
->        19:          *MM100        *MM100        *MM100        *MM100
->        20:             -17           -17           -17           -17
->        21:              =1            =1            =1            =1
->        22:              -8            -8            -8            -8
->        23:              -8            -8            -8             .
->        24:               .            -8             .             .
->        25:              -8            -8            -8            -8
->        26:              =2            =2            =2            =2
->        27:             -10           -10           -10           -10
->        28:             -10           -10             .             .
->        29:               .             .             .           -10
->        30:             -10           -10           -10           -10
->        31:              =3            =3            =3            =3
->        32:             -17           -17           -17           -17
->        33:               .           -17           -17             .
->        34:             -17           -17           -17             .
->        35:             -17             .           -17           -17
->        36:             -17           -17           -17           -17
->        37:              =4            =4            =4            =4
->        38:             -17           -17           -17           -17
->        39:             -17           -17           -17           -17
->        40:              =5            =5            =5            =5
->    41-133:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
->    ############################# ^^^ chor001.krn ^^^ #############################
->    
->           (8 more pieces...)
->    
->    ############################# vvv chor010.krn vvv #############################
->      1-60:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
->        61:             -15           -15           -15           -15
->        62:             -15           -15           -15           -15
->        63:              =9            =9            =9            =9
->        64:             -13           -13           -13           -13
->        65:             -13           -13           -13           -13
->        66:             -13             .             .             .
->        67:             -13           -13           -13           -13
->        68:               .             .           -13             .
->        69:             -13           -13           -13           -13
->        70:             =10           =10           =10           =10
->        71:             -17           -17           -17           -17
->        72:               .           -17             .             .
->        73:             -17             .           -17           -17
->        74:               .           -17             .             .
->        75:             -17           -17           -17           -17
->        76:             =11           =11           =11           =11
->        77:             -15           -15           -15           -15
->        78:             -15           -15           -15           -15
->        79:             -15           -15           -15           -15
->        80:               .             .           -15             .
->        81:             =12           =12           =12           =12
->        82:             -15           -15           -15           -15
->        83:             -15           -15           -15           -15
->        84:             -15           -15           -15           -15
->        85:             -15           -15             .           -15
->        86:             =13           =13           =13           =13
->        87:             -20           -20           -20           -20
->        88:             -20           -20           -20             .
->        89:             -20           -20           -20             .
->        90:              ==            ==            ==            ==
->        91:              *-            *-            *-            *-
->        92:  !!!hum2abc: -Q ''
->        93:  !!!title: @{PC#}. @{OTL@@DE}
->        94:  !!!YOR1: 371 vierstimmige Choralges&auml;nge von Johann Sebastian B***
->        95:  !!!YOR2: 4th ed. by Alfred D&ouml;rffel (Leipzig: Breitkopf und H&a***
->        96:  !!!YOR2: c.1875). 178 pp. Plate "V.A.10".  reprint: J.S. Bach, 371 ***
->        97:  !!!YOR4: Chorales (New York: Associated Music Publishers, Inc., c.1***
->        98:  !!!SMS: B&H, 4th ed, Alfred D&ouml;rffel, c.1875, plate V.A.10
->        99:  !!!EED:  Craig Stuart Sapp
->       100:  !!!EEV:  2009/05/22
->    ############################# ^^^ chor010.krn ^^^ #############################
->                           (***four global comments truncated due to screen size***)
->    
->       humdrumR corpus of ten pieces.
->        Data fields: 
->                Token       :: character
->                Pitch       :: character (**pitch tokens)
->                Semits      :: integer (**semits tokens)
->               *BarBassNote :: integer (**semits tokens)
-

What did this command do? It looks at every bar in each file and -finds the lowest note, then “fills” all the notes in that bar with that -same, single, note. We could then calculate the harmonic interval above -the bar’s bass note:

-

-within(chorales,
-       Semits - BarBassNote)
->    ############################# vvv chor001.krn vvv #############################
->         1:  !!!COM: Bach, Johann Sebastian
->         2:  !!!CDT: 1685/02/21/-1750/07/28/
->         3:  !!!OTL@@DE: Aus meines Herzens Grunde
->         4:  !!!OTL@EN:      From the Depths of My Heart
->         5:  !!!SCT: BWV 269
->         6:  !!!PC#: 1
->         7:  !!!AGN: chorale
->         8:        **semits      **semits      **semits      **semits
->         9:          *ICvox        *ICvox        *ICvox        *ICvox
->        10:          *Ibass       *Itenor        *Ialto       *Isoprn
->        11:         *I"Bass      *I"Tenor       *I"Alto    *I"Soprano
->        12:       *>[A,A,B]     *>[A,A,B]     *>[A,A,B]     *>[A,A,B]
->        13:    *>norep[A,B]  *>norep[A,B]  *>norep[A,B]  *>norep[A,B]
->        14:             *>A           *>A           *>A           *>A
->        15:         *clefF4      *clefGv2       *clefG2       *clefG2
->        16:          *k[f#]        *k[f#]        *k[f#]        *k[f#]
->        17:             *G:           *G:           *G:           *G:
->        18:           *M3/4         *M3/4         *M3/4         *M3/4
->        19:          *MM100        *MM100        *MM100        *MM100
->        20:               0            16            19            24
->        21:              =1            =1            =1            =1
->        22:               3             7            10            15
->        23:               0             8            12             .
->        24:               .             7             .             .
->        25:               2             5            10            22
->        26:              =2            =2            =2            =2
->        27:               5             5            12            21
->        28:               0             4             .             .
->        29:               .             .             .            19
->        30:               2             5             9            17
->        31:              =3            =3            =3            =3
->        32:               5            17            21            24
->        33:               .            16            19             .
->        34:               4            17            21             .
->        35:               2             .            23            26
->        36:               0            19            24            28
->        37:              =4            =4            =4            =4
->        38:               7            19            23            26
->        39:               0            19            24            28
->        40:              =5            =5            =5            =5
->    41-133:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
->    ############################# ^^^ chor001.krn ^^^ #############################
->    
->           (8 more pieces...)
->    
->    ############################# vvv chor010.krn vvv #############################
->      1-60:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
->        61:               0            15            19            24
->        62:              12            19            24            27
->        63:              =9            =9            =9            =9
->        64:               5            17            20            24
->        65:               3            17            20            25
->        66:               1             .             .             .
->        67:               0            15            20            27
->        68:               .             .            18             .
->        69:               1            13            17            20
->        70:             =10           =10           =10           =10
->        71:               7            11            19            28
->        72:               .            12             .             .
->        73:               7             .            17            26
->        74:               .            11             .             .
->        75:               0            12            16            24
->        76:             =11           =11           =11           =11
->        77:               3            10            19            22
->        78:               0            12            19            27
->        79:               7            11            19            26
->        80:               .             .            17             .
->        81:             =12           =12           =12           =12
->        82:               8            12            15            24
->        83:               3            10            15            19
->        84:               1            10            17            22
->        85:               0            12             .            20
->        86:             =13           =13           =13           =13
->        87:               4            19            22            24
->        88:               5            17            20             .
->        89:               0            16            19             .
->        90:              ==            ==            ==            ==
->        91:              *-            *-            *-            *-
->        92:  !!!hum2abc: -Q ''
->        93:  !!!title: @{PC#}. @{OTL@@DE}
->        94:  !!!YOR1: 371 vierstimmige Choralges&auml;nge von Johann Sebastian B***
->        95:  !!!YOR2: 4th ed. by Alfred D&ouml;rffel (Leipzig: Breitkopf und H&a***
->        96:  !!!YOR2: c.1875). 178 pp. Plate "V.A.10".  reprint: J.S. Bach, 371 ***
->        97:  !!!YOR4: Chorales (New York: Associated Music Publishers, Inc., c.1***
->        98:  !!!SMS: B&H, 4th ed, Alfred D&ouml;rffel, c.1875, plate V.A.10
->        99:  !!!EED:  Craig Stuart Sapp
->       100:  !!!EEV:  2009/05/22
->    ############################# ^^^ chor010.krn ^^^ #############################
->                           (***four global comments truncated due to screen size***)
->    
->       humdrumR corpus of ten pieces.
->        Data fields: 
->                Token       :: character
->                Pitch       :: character (**pitch tokens)
->                Semits      :: integer (**semits tokens)
->                BarBassNote :: integer (**semits tokens)
->               *Result1     :: integer (**semits tokens)
-
-with(chorales, hist(Semits - BarBassNote))
-

-
-
-
-
- - - -
- - - -
-
- - - - - - - diff --git a/docs/articles/WorkingWithData_files/figure-html/unnamed-chunk-15-1.png b/docs/articles/WorkingWithData_files/figure-html/unnamed-chunk-15-1.png deleted file mode 100644 index ff198890..00000000 Binary files a/docs/articles/WorkingWithData_files/figure-html/unnamed-chunk-15-1.png and /dev/null differ diff --git a/docs/articles/WorkingWithData_files/figure-html/unnamed-chunk-18-1.png b/docs/articles/WorkingWithData_files/figure-html/unnamed-chunk-18-1.png deleted file mode 100644 index 4172db27..00000000 Binary files a/docs/articles/WorkingWithData_files/figure-html/unnamed-chunk-18-1.png and /dev/null differ diff --git a/docs/articles/WorkingWithData_files/figure-html/unnamed-chunk-19-1.png b/docs/articles/WorkingWithData_files/figure-html/unnamed-chunk-19-1.png deleted file mode 100644 index 19529ecd..00000000 Binary files a/docs/articles/WorkingWithData_files/figure-html/unnamed-chunk-19-1.png and /dev/null differ diff --git a/docs/articles/WorkingWithData_files/figure-html/unnamed-chunk-20-1.png b/docs/articles/WorkingWithData_files/figure-html/unnamed-chunk-20-1.png deleted file mode 100644 index 56554c45..00000000 Binary files a/docs/articles/WorkingWithData_files/figure-html/unnamed-chunk-20-1.png and /dev/null differ diff --git a/docs/articles/WorkingWithData_files/figure-html/unnamed-chunk-22-1.png b/docs/articles/WorkingWithData_files/figure-html/unnamed-chunk-22-1.png deleted file mode 100644 index 07c29b4a..00000000 Binary files a/docs/articles/WorkingWithData_files/figure-html/unnamed-chunk-22-1.png and /dev/null differ diff --git a/docs/articles/WorkingWithData_files/figure-html/unnamed-chunk-6-1.png b/docs/articles/WorkingWithData_files/figure-html/unnamed-chunk-6-1.png deleted file mode 100644 index db083e18..00000000 Binary files a/docs/articles/WorkingWithData_files/figure-html/unnamed-chunk-6-1.png and /dev/null differ diff --git a/docs/articles/index.html b/docs/articles/index.html index 48cbf4c0..61465496 100644 --- a/docs/articles/index.html +++ b/docs/articles/index.html @@ -10,7 +10,7 @@ Georgia Tech CCMLab humdrumR - 0.7.0.2 + 0.7.0.3 diff --git a/docs/reference/pitch.html b/docs/reference/pitch.html index b32f5bcf..f9cee905 100644 --- a/docs/reference/pitch.html +++ b/docs/reference/pitch.html @@ -18,7 +18,7 @@ Georgia Tech CCMLab humdrumR - 0.7.0.2 + 0.7.0.3 - - - - - -
-
-
- -
-

Tabulate and cross proportions

-
- -
-

Usage

-
ptable(...)
-
-# S3 method for default
-ptable(..., margin = NULL)
-
-# S3 method for table
-ptable(tab, margin = NULL, na.rm = TRUE)
-
-# S3 method for ptable
-[(ptab, i, j, ..., drop = FALSE)
-
-# S3 method for ptable
-print(x, digits = 4)
-
- - -
- - -
- - - -
- - - - - - - diff --git a/docs/reference/pullHumdrum.html b/docs/reference/pullHumdrum.html index be084e1c..f3f693c3 100644 --- a/docs/reference/pullHumdrum.html +++ b/docs/reference/pullHumdrum.html @@ -16,7 +16,7 @@ Georgia Tech CCMLab humdrumR - 0.7.0.2 + 0.7.0.3