arca-0.1.0.0: A digital implementation of Athanasius Kircher's device for automatic music composition, the Arca musarithmica of 1650

Copyright(c) 2022 Andrew A. Cashner
StabilityExperimental
Safe HaskellNone
LanguageHaskell2010

Cogito.Musarithmetic

Contents

Description

This module provides the tools used in the main Cogito module to adjust music created by the ark and to store it in internal structures that will then be used by the Scribo modules.

The stepwiseVoiceInRange function tests all the possible permutations of octaves for the pitches in a phrase and finds the best path, with the minimum of large leaps and notes out of range.

Kircher's specification for how to put voices in range is incomplete, and his own implementation is inconsistent, as demonstrated by his examples. He says to find the next closest pitch "within the octave" among the notes on the staff (i.e., the notes within range), but he doesn't define "within the octave." Sometimes he leaps a fifth instead of a fourth, which would break that rule.

Sometimes a gesture requires the voice to go out of range. Kircher says in that case you can switch clefs. But that doesn't change the notes a singer can sing. If he means to change to transposing clefs, that might work, but no one ever changed to transposing clefs for only a single phrase and then went back.

Instead, this module provides an algorithm that works every time to produce an optimal melody with a small ambitus, minimum number of notes outside of range, and small leaps. This seems very close to what Kircher probably thought musicians would do intuitively, but did not fully specify programmatically.

Synopsis

Data structures

For storing and adjusting pitches and rhythms, not including the sung text

data Voice Source #

A Voice is a list of pitches with an identifier for the voice type.

Constructors

Voice 

Fields

Instances
Eq Voice Source # 
Instance details

Defined in Cogito.Musarithmetic

Methods

(==) :: Voice -> Voice -> Bool #

(/=) :: Voice -> Voice -> Bool #

Ord Voice Source # 
Instance details

Defined in Cogito.Musarithmetic

Methods

compare :: Voice -> Voice -> Ordering #

(<) :: Voice -> Voice -> Bool #

(<=) :: Voice -> Voice -> Bool #

(>) :: Voice -> Voice -> Bool #

(>=) :: Voice -> Voice -> Bool #

max :: Voice -> Voice -> Voice #

min :: Voice -> Voice -> Voice #

Show Voice Source # 
Instance details

Defined in Cogito.Musarithmetic

Methods

showsPrec :: Int -> Voice -> ShowS #

show :: Voice -> String #

showList :: [Voice] -> ShowS #

type Chorus = [Voice] Source #

A Chorus is a group (list) of four Voice items

TODO: We don't actually define it as being four items. TODO: Do we still need this with new MEI setup?

For storing music including the sung text

data Note Source #

A Note contains a pitch and a syllable, equivalent to MEI note

Constructors

Note 
Instances
Eq Note Source # 
Instance details

Defined in Cogito.Musarithmetic

Methods

(==) :: Note -> Note -> Bool #

(/=) :: Note -> Note -> Bool #

Ord Note Source # 
Instance details

Defined in Cogito.Musarithmetic

Methods

compare :: Note -> Note -> Ordering #

(<) :: Note -> Note -> Bool #

(<=) :: Note -> Note -> Bool #

(>) :: Note -> Note -> Bool #

(>=) :: Note -> Note -> Bool #

max :: Note -> Note -> Note #

min :: Note -> Note -> Note #

Show Note Source # 
Instance details

Defined in Cogito.Musarithmetic

Methods

showsPrec :: Int -> Note -> ShowS #

show :: Note -> String #

showList :: [Note] -> ShowS #

data Syllable Source #

A Syllable is a single syllable to be paired with a Pitch, including its position in the word.

Constructors

Syllable 
Instances
Eq Syllable Source # 
Instance details

Defined in Cogito.Musarithmetic

Ord Syllable Source # 
Instance details

Defined in Cogito.Musarithmetic

Show Syllable Source # 
Instance details

Defined in Cogito.Musarithmetic

data SyllablePosition Source #

What is the position of the syllable relative to the word? Beginning, middle, or end? This determines hyphenation.

Constructors

First 
Middle 
Last 
Only 
Tacet

no syllable

data MusicPhrase Source #

A MusicPhrase contains all the notes set using one permutation drawn from the ark, for a single voice.

Constructors

MusicPhrase 

type MusicSentence = [MusicPhrase] Source #

A list of MusicPhrase items

data MusicSection Source #

A MusicSection contains all the music for one section in the input XML document, for a single voice, together with the parameters set in the input file.

data MusicChorus Source #

A MusicChorus is a four-voice SATB structure of MusicSection data. TODO do we really need it to be structured this way?

type MusicScore = [MusicChorus] Source #

The full MusicScore is a list of SATB MusicChorus structures.

Manipulating the Pitch

newRest Source #

Arguments

:: Dur

Rhythmic duration for this note

-> Pitch 

Create a rest (that is, a Pitch with duration only)

We make a Pitch but set the pnum to Rest; oct and accid are set to special nil values (OctNil, AccidNil)

TODO: We are setting the octave using fromEnum OctNil: Isn't this the same as just setting it to zero? Is there a better way to mark this?

Adjust pitch for tone

toneMollis :: Tone -> ToneSystem -> Bool Source #

Is a tone in cantus mollis? Should there be a flat in the key signature?

pnumAccidInTone :: Int -> ToneList -> Tone -> PnumAccid Source #

Adjust a pitch to be in a given tone.

modalFinal :: ToneList -> Tone -> Pitch Source #

Get the modal final for this tone. What pitch = 0 in this tone? (In Kircher's 1-indexed vperms, the final is 1 or 8.)

Check for rests

isRest :: Dur -> Bool Source #

Check to see if a rhythmic duration is a rest type (the rest enums begin with LgR so we compare with that)

isPitchRest :: Pitch -> Bool Source #

Is the Pitch a rest?

anyRests :: [Pitch] -> Bool Source #

Are any of these pitches rests?

Measure distances between notes and correct bad intervals

Convert between diatonic and chromatic pitches for calculations

absPitch :: Pitch -> Int Source #

Convert Pitch to absolute pitch number, using chromatic calculations (base 12). Raise an error if it is a rest.

dia2chrom :: Pnum -> Int Source #

Get chromatic offset from C for diatonic pitch classes (PCc -> 0, PCd -> 2, PCe -> 4, etc.)

absPitch7 :: Pitch -> Int Source #

Absolute diatonic pitch (base 7). Raise an error if it is a rest.

"Musarithmetic": Differences, sums, conditionals with Pitch values

pitchMath :: (Int -> Int -> Int) -> Pitch -> Pitch -> Int Source #

Do mathematical operations on pitches (using their chromatic absPitch values)

pitchMath7 :: (Int -> Int -> Int) -> Pitch -> Pitch -> Int Source #

Do mathematical operations on pitches (using their diatonic absPitch7 values)

pitchTest :: (Int -> Int -> Bool) -> Pitch -> Pitch -> Bool Source #

Do boolean tests on pitches (using their absPitch values)

Conditional tests

pEq :: Pitch -> Pitch -> Bool Source #

Are two Pitches the same chromatic pitch, enharmonically equivalent?

pnumAccidEq :: Pnum -> Accid -> Pitch -> Bool Source #

Test the pitch label and accidental of a Pitch

pGt :: Pitch -> Pitch -> Bool Source #

Pitch greater than?

pLt :: Pitch -> Pitch -> Bool Source #

Pitch less than?

pGtEq :: Pitch -> Pitch -> Bool Source #

Pitch greater than or equal?

pLtEq :: Pitch -> Pitch -> Bool Source #

Pitch less than or equal?

Differences, intervals

p12diff :: Pitch -> Pitch -> Int Source #

Difference between pitches, chromatic interval

p12diffMod :: Pitch -> Pitch -> Int Source #

Chromatic difference between pitch classes (within one octave); p12diff modulo 12

p7diff :: Pitch -> Pitch -> Int Source #

Difference between pitches, diatonic interval Unison = 0, therefore results of this function are one less than the verbal names of intervals (p7diff = 4 means a fifth)

p7diffMod :: Pitch -> Pitch -> Int Source #

Diatonic difference between pitch classes (= pitch difference as though within a single octave); result is 0-indexed, so the interval of a "third" in speech has a p7diffMod of 2

absInterval :: Pitch -> Pitch -> Int Source #

Take the absolute value of an intervals, the difference between pitches. The interval between any note and a rest is zero.

Change a Pitch based on calculation

changePnumOctave :: Int -> Pitch -> Pitch Source #

Change the pitch class and octave of an existing Pitch to that of an absolute diatonic pitch number. Return rests unchanged.

Operate on pitch class and octave

p7inc Source #

Arguments

:: Pitch 
-> Int

diatonic interval, 0-indexed

-> Pitch 

Increase a pitch diatonically by a given interval (0-indexed diatonic, e.g., p7inc p 4 raises p by a diatonic third). Return rests unchanged.

Operate on octave alone

octaveChange Source #

Arguments

:: Pitch 
-> Int

Helmholtz octave number

-> Pitch 

Just change the octave to a given number, no calculation required

octaveInc :: Pitch -> Int -> Pitch Source #

Increase the octave number by the given amount

octaveUp :: Pitch -> Pitch Source #

Raise the octave by 1

octaveDown :: Pitch -> Pitch Source #

Lower the octave by 1

Test a Pitch relative to a VoiceRange

pitchTooLow :: VoiceRange -> Pitch -> Bool Source #

Is the pitch below the bottom limit of the voice range?

pitchTooHigh :: VoiceRange -> Pitch -> Bool Source #

Is the pitch above the upper limit of the voice range?

pitchInRange :: VoiceRange -> Pitch -> Bool Source #

Is the Pitch within the proper range for its voice? Rests automatically count as valid.

legalLeap :: Pitch -> Pitch -> Bool Source #

Is this an acceptable leap? Only intervals up to a sixth, or an octave are okay. If either note is a rest, then that also passes the test.

TODO: Ignoring rests like this is a bit of a cop-out, but Kircher usually puts rests at the beginning of a phrase, so they affect the interval between phrases, which we are not adjusting anyway. In an ideal scenario, we would.

Find an optimal version of the melody for a particular voice

Make lists of pitches in range

lowestInRange :: VoiceRange -> Pitch -> Pitch Source #

Find the lowest valid instance of a given Pitch within the VoiceRange. This is used to calculate an optimal path through the possible pitches in a phrase, and means that in most cases the melody will end up in the lower end of the voice's range.

octavesInRange :: VoiceRange -> [Int] Source #

List all the octaves within a voice range.

pitchesInRange :: VoiceRange -> Pitch -> [Pitch] Source #

List all the valid instances of a given pitch within a voice range.

pitchCandidates :: VoiceRange -> [Pitch] -> [[Pitch]] Source #

Given a list of pitches (taken from the vperms in the ark), return a list of list of all the valid instances of those pitches within a particular voice range. This determines the candidate pitches that we will test to find the optimal melody.

Decision trees for evaluation ordered permutations of a series.

data Btree a Source #

Binary tree. We use a left-child/right-sibling binary tree to evaluate any number of candidates for each element in the series.

Constructors

Empty 
Node a (Btree a) (Btree a) 
Instances
Show a => Show (Btree a) Source # 
Instance details

Defined in Cogito.Musarithmetic

Methods

showsPrec :: Int -> Btree a -> ShowS #

show :: Btree a -> String #

showList :: [Btree a] -> ShowS #

tree :: [[a]] -> Btree a Source #

Build a general tree, implemented as left-child/right-sibling binary tree that can take more than two options at each level

Test ordered permutations with a tree

testTree Source #

Arguments

:: (a -> a -> Bool)

test to determine if child is valid relative to parent

-> Maybe a

previous value to test

-> [[a]]

list of permutations at each level

-> Btree a 

Build a left-child/right-sibling tree from a list of the options at each level, only including options that pass a test function; the test function compares each parent to its child. If the value of the parent (previous good value) is Nothing then we know it is the beginning of the tree, there is no previous value to compare.

Traversal

paths Source #

Arguments

:: [[a]]

accumulator list

-> Btree a 
-> [[a]] 

Make a list of all good paths in an LCRS tree. If no good paths are found, the result will be [].

Test the paths

sameLengths :: [[a]] -> Bool Source #

Are all the elements of a list the same length?

fullPaths Source #

Arguments

:: [a]

list of items to permute

-> [[b]]

list of permutations

-> Maybe [[b]] 

Prune out paths that are shorter than the original list of items. If none are left after pruning (no viable paths), return Nothing.

Score a path for "badness" of different kinds

ambitus :: [Pitch] -> Int Source #

The ambitus is the widest range of pitches used; the difference between the highest and lowest pitches. Ignore rests.

intervals :: [Pitch] -> [Int] Source #

Calculate and list intervals between pitches in a list. The list will be one item shorter than the list of inputs.

sumBigIntervals :: [Pitch] -> Int Source #

Add up all the intervals larger than a fourth (where p7diff > 3 with 0-indexed intervals).

sumBeyondRange :: VoiceRange -> [Pitch] -> Int Source #

Find all the pitches that exceed a given range, and add up the interval by which they go above or below the limits.

badness :: VoiceRange -> [Pitch] -> Int Source #

Calculate weighted "badness" score for a list of pitches. Sum of ambitus, sum of large intervals (x 2), sum of degrees of notes out of range (x 10).

bestPath :: VoiceRange -> [Pitch] -> [[Pitch]] -> [Pitch] Source #

Find the best path (first with lowest "badness"), or raise error if none found

leastBadPath :: VoiceRange -> [[Pitch]] -> [Pitch] Source #

Choose the path with the lowest "badness"; if there are multiple with the same score, choose the first

Synthetic functions pulling together the above

stepwiseTree :: [[Pitch]] -> Btree Pitch Source #

Build a tree of all pitch sequences with appropriate leaps

stepwiseVoiceInRange :: VoiceRanges -> Voice -> Voice Source #

Find a melody for a voice with an optimal blend of avoiding bad leaps and staying within range. This is the main function used in Cogito.

Avoid large or illegal leaps and stay as much in range as possible. For example, some melodies have long stepwise ascents or descents which, in certain tones, will take the voice out of range, and if we adjust them in the middle, we will get an illegal seventh interval.

We build a list of candidate pitches within the range, then we build a tree of the ordered permutations of those pitches and test the paths according to a subjective "badness" rating, including the ambitus or total range of highest to lowest notes, the number and size of large intervals, and the number of notes out of range (and how much out of range they are). The first path with the lowest score wins.