Copyright | (c) 2022 Andrew A. Cashner |
---|---|
Stability | Experimental |
Safe Haskell | None |
Language | Haskell2010 |
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 Voice = Voice {}
- type Chorus = [Voice]
- data Note = Note {}
- data Syllable = Syllable {}
- data SyllablePosition
- data MusicPhrase = MusicPhrase {
- phraseVoiceID :: VoiceName
- notes :: [Note]
- type MusicSentence = [MusicPhrase]
- data MusicSection = MusicSection {}
- data MusicChorus = MusicChorus {}
- type MusicScore = [MusicChorus]
- newRest :: Dur -> Pitch
- toneMollis :: Tone -> ToneSystem -> Bool
- pnumAccidInTone :: Int -> ToneList -> Tone -> PnumAccid
- modalFinal :: ToneList -> Tone -> Pitch
- isRest :: Dur -> Bool
- isPitchRest :: Pitch -> Bool
- anyRests :: [Pitch] -> Bool
- absPitch :: Pitch -> Int
- dia2chrom :: Pnum -> Int
- absPitch7 :: Pitch -> Int
- pitchMath :: (Int -> Int -> Int) -> Pitch -> Pitch -> Int
- pitchMath7 :: (Int -> Int -> Int) -> Pitch -> Pitch -> Int
- pitchTest :: (Int -> Int -> Bool) -> Pitch -> Pitch -> Bool
- pEq :: Pitch -> Pitch -> Bool
- pnumAccidEq :: Pnum -> Accid -> Pitch -> Bool
- pGt :: Pitch -> Pitch -> Bool
- pLt :: Pitch -> Pitch -> Bool
- pGtEq :: Pitch -> Pitch -> Bool
- pLtEq :: Pitch -> Pitch -> Bool
- p12diff :: Pitch -> Pitch -> Int
- p12diffMod :: Pitch -> Pitch -> Int
- p7diff :: Pitch -> Pitch -> Int
- p7diffMod :: Pitch -> Pitch -> Int
- absInterval :: Pitch -> Pitch -> Int
- changePnumOctave :: Int -> Pitch -> Pitch
- p7inc :: Pitch -> Int -> Pitch
- octaveChange :: Pitch -> Int -> Pitch
- octaveInc :: Pitch -> Int -> Pitch
- octaveUp :: Pitch -> Pitch
- octaveDown :: Pitch -> Pitch
- pitchTooLow :: VoiceRange -> Pitch -> Bool
- pitchTooHigh :: VoiceRange -> Pitch -> Bool
- pitchInRange :: VoiceRange -> Pitch -> Bool
- legalLeap :: Pitch -> Pitch -> Bool
- lowestInRange :: VoiceRange -> Pitch -> Pitch
- octavesInRange :: VoiceRange -> [Int]
- pitchesInRange :: VoiceRange -> Pitch -> [Pitch]
- pitchCandidates :: VoiceRange -> [Pitch] -> [[Pitch]]
- data Btree a
- tree :: [[a]] -> Btree a
- testTree :: (a -> a -> Bool) -> Maybe a -> [[a]] -> Btree a
- paths :: [[a]] -> Btree a -> [[a]]
- sameLengths :: [[a]] -> Bool
- fullPaths :: [a] -> [[b]] -> Maybe [[b]]
- ambitus :: [Pitch] -> Int
- intervals :: [Pitch] -> [Int]
- sumBigIntervals :: [Pitch] -> Int
- sumBeyondRange :: VoiceRange -> [Pitch] -> Int
- badness :: VoiceRange -> [Pitch] -> Int
- bestPath :: VoiceRange -> [Pitch] -> [[Pitch]] -> [Pitch]
- leastBadPath :: VoiceRange -> [[Pitch]] -> [Pitch]
- stepwiseTree :: [[Pitch]] -> Btree Pitch
- stepwiseVoiceInRange :: VoiceRanges -> Voice -> Voice
Data structures
For storing and adjusting pitches and rhythms, not including the sung text
A Voice
is a list of pitches with an identifier for the voice type.
For storing music including the sung text
A Note
contains a pitch and a syllable, equivalent to MEI note
Constructors
Note | |
Fields
|
Constructors
Syllable | |
Fields |
data SyllablePosition Source #
What is the position of the syllable relative to the word? Beginning, middle, or end? This determines hyphenation.
Instances
data MusicPhrase Source #
A MusicPhrase
contains all the notes set using one permutation drawn
from the ark, for a single voice.
Constructors
MusicPhrase | |
Fields
|
Instances
Eq MusicPhrase Source # | |
Defined in Cogito.Musarithmetic | |
Ord MusicPhrase Source # | |
Defined in Cogito.Musarithmetic Methods compare :: MusicPhrase -> MusicPhrase -> Ordering # (<) :: MusicPhrase -> MusicPhrase -> Bool # (<=) :: MusicPhrase -> MusicPhrase -> Bool # (>) :: MusicPhrase -> MusicPhrase -> Bool # (>=) :: MusicPhrase -> MusicPhrase -> Bool # max :: MusicPhrase -> MusicPhrase -> MusicPhrase # min :: MusicPhrase -> MusicPhrase -> MusicPhrase # | |
Show MusicPhrase Source # | |
Defined in Cogito.Musarithmetic Methods showsPrec :: Int -> MusicPhrase -> ShowS # show :: MusicPhrase -> String # showList :: [MusicPhrase] -> ShowS # |
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.
Constructors
MusicSection | |
Fields
|
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?
Constructors
MusicChorus | |
Fields
|
type MusicScore = [MusicChorus] Source #
The full MusicScore
is a list of SATB MusicChorus
structures.
Manipulating the Pitch
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)
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.)
"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 Pitch
es the same chromatic pitch, enharmonically equivalent?
pnumAccidEq :: Pnum -> Accid -> Pitch -> Bool Source #
Test the pitch label and accidental of a Pitch
Differences, intervals
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
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
Just change the octave to a given number, no calculation required
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.
Binary tree. We use a left-child/right-sibling binary tree to evaluate any number of candidates for each element in the series.
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
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
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?
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.