[Haskell-beginners] help with music application
Dennis Raddle
dennis.raddle at gmail.com
Tue Dec 1 23:11:46 UTC 2015
I realize my current implementation of my music application is not pretty
and I want to learn to use Haskell more effectively.
My application reads a musical score exported by the typesetter Sibelius. A
score is a high-level representation of music that includes structures,
like grouping notes for each instrument, and directions that affect how the
musuc will be played, like tempo indications, indications to gradually
speed up, gradually get louder, etc. My program translates it into
low-level time stamped MIDI events, which say things like "turn on this
note", "turn off this note", "change the volume setting on this
instrument," etc.
The outline of my program is this:
1. Read the output of Sibeliusl
2. Construct a Score. A Score has several Parts (individual instruments). A
part consists of a list of time stamped Chords, which represent a list of
Notes that are sounded at the same time (roughly). There are expression
marks in the music that apply to Chords as a whole and some apply only to
Notes. Notes are the most basic sound-generating unit -- they have a pitch,
a duration, some indicators of musical expression like performance
techniques, a time offset (a Note may start slightly ahead or behind the
time stamp on the Chord), and more.
3. Modify the Score to account for forms of musical expression and for a
few strange things in the way Sibelius exports its data. Right now I
process the score through a LOT of passes, maybe two dozen. Also I have
given a ton of extra fields to Notes and Chords that essentially are cache
or memos of the results of processing that looks for patterns. So now
things are quite unwieldy.. I have two dozen extra fields between Chords
and Notes, and I need to make two dozen modification passes on the Score.
It's bug prone and hard to understand because the order of the passes is
important and I could easily put the Score into an invalid state.
So the data looks like this:
The basic time stamp type is Loc, indicating a measure number and a beat
within that measure.
type MeasureNum = Int
type MeasureBeat = Rational
data Loc = Loc MeasureNum MeasureBeat
type PartName = String
data Score = Score ScoreRelatedData (Map PartName Part)
data ScoreRelatedData = -- this is data that applies to the score as a
whole, like a measure-by-measure list of tempos, time signatures, and a
time map that allows the translation of Loc to a number of seconds.
-- The Chords in a Part occur in time at a Loc. There can be more
-- than one Chord at a Loc
data Part = PartRelatedData (Map Loc [Chord])
data PartRelatedData -- data about musical expression that applies to a
Part as a whole
data VoiceNumber Int -- even within a single Part there can be different
"voices" which means independent sequential Chords, independent in the
sense they may have different volume levels, different durations, etc.
data Chord = ChordRelatedData VoiceNumber [Note]
data ChordRelatedData -- this includes data about the Chord as a whole,
such as playback techniqes that applies to the whole Chord. It also
contains a list of Notes.
data Note = Note NoteRelatedData Pitch
data NoteRelatedData -- this includes expression markings that apply to a
Note only
So there are some patterns within the Score that need to be identified
before it can be translated to MIDI events. Here are some examples:
1. Tied notes. Some Notes within a Chord X are tied to a Note in the
following Chord Y. That means they represent a single sound unit that
should be sustained from the beginning of Chord X to the end of Chord Y.
But actually the note in Chord Y can be tied to Chord Z and so on for any
number of sequential Chords. When I want to find the true ending Loc of a
Note I need to follow the tie chain. Some Notes in the same Chord may be
tied, and others may not.
2. Double tremolos. Sometimes two sequential chords are actually supposed
to played together--actually the player will rapidly alternate between the
chords. When I first read the score there will be a marking on the first
chord X of the double tremolo. I have to look for a Chord Y that
immediately follows X, has the same VoiceNumber and the same duration, and
I can infer that's the second chord in the double tremolo. Note that the
timing and notes of a double tremolo, when translated to MIDI, look hardly
anything like the original data -- the original data just has two Chords,
but the playback will contains lots of sequential notes that are drawn from
both chords.
3. Arpeggios. Sometime the notes in a chord are "rolled" -- played with the
lowest note first, followed by a time-staggered playback of the other
notes, going up in pitch. There might be an arpeggio marking that spans
several Parts, meaning I have to look at all the parts to compute the time
offsets for the notes in an arpeggio
4. problems in the data export. Sibelius has some bugs, so I need to find
problems in its output and fix them by removing or altering certain Chords.
And that's just the beginning. There are something like two dozen patterns
that need to be identified.
What I'm doing now is adding all sorts of fields to Parts, Chords, and
Notes to hold memos of the results of this processing. These memo fields
have to be initialized to something, like zero, or an empty Map, or
whatever. Then I set them via the processing passes.
This is bug-prone and unwieldy.
What I am realizing is that I don't necessarily need to store every field.
For isntance, consider tie chains. I don't really need to process them all
ahead of time. I can follow a tie chain only in the places where I need to
know the full duration of a Note. I might end up with some redundant
processing, but the advantage is
(1) I don't need an extra field
(2) I don't have to worry about that extra field becoming invalid or
remaining uninitialized.
But there are some processing data that probably should be done once and
memoized. For instance, I need a Map of the END LOCATION of each Chord to
the Chord itself to look up certain things. That is expensive to construct.
So what data do I pass to my MIDI-conversion algorithm? I could create
something called ContextNote like this
data ContextNote = ContextNote Score Part Chord Note
Then I could write a function that converts a score to a list of context
notes, and pass all of them to the conversion routine.
allContextNotes :: Score -> [ContextNote]
This list would have one entry for every note in the Score.
Also I would probably use named fields. So I would have something like
data Note = Note
{ nPitch :: Int
, nVolume :: Int
, ...
}
When I'm accessing the data on ContextNote, I don't want to have to type
several accessor functions to get down to the Note fields. So I could do
something like
class HasNoteData a where
pitch :: a -> Int
Then
instance HasNoteData ContextNote where
pitch (ContextNote _ _ note) = nPitch note
I could create a bunch of accessor functions like that for every field in
the ContextNote.
For data that should probably be computed once, I could create Memo data.
Maybe I would have
data ContextNote = ContextNote Memo Score Part Chord Note
or something like that.
So if you have read this far, thank you very much. Any suggestions welcome.
D
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.haskell.org/pipermail/beginners/attachments/20151201/0fcc59aa/attachment.html>
More information about the Beginners
mailing list