Wednesday, April 18, 2012

text wrapping

And speaking of aesthetic matters, here are a few more screenshots I had on my desktop from when I was trying to figure out what to do with too-long text:

Rotating text vertically looks cool but turns out it's just not as readable as plain word wrapping.

It occurred to me that another solution would be to write the scores in Chinese.  And as a bonus, where else can you find a ready-made supply of 10,000 symbols that already have (more or less) definite meanings?

hex vs. decimal

One of the things I do a lot is writing numbers between 0 and 1.  All control values are normalized between 0 and 1 and it turns out scores have a lot of control values.  However, I haven't been entirely happy just writing plain decimal numbers for a couple of reasons.

Their values are not very visually apparent.  1.7 looks very similar to .17, but they are entirely different values.  I'm usually writing between 0 and 1 so it's annoying to have to write that decimal point every single time.  It shouldn't even be possible to accidentally write 1.7.  They're also variable length, so for example if I divide all the values by 2 some are likely to turn into big long numbers that no longer fit.  I already trim them to 3 decimal places, but a resolution of 1000 is still more than I need---MIDI only has 7 bits of resolution for most control values anyway.  It's also annoying how you have to type all those backspaces to change a value, and it's especially slow because it means you actually have to look at what you're doing to know how many backspaces to type.  That's the variable length thing.

So I thought I could take a page from the trackers and represent values as fixed width values between 00 and ff.  It's faster to type because each key can insert its higit on the right side, and bump off the higit on the left side.  I can't automatically advance the cursor after 2 higits because I don't keep track of a cursor position, but I know from experience it's pretty fast to keep one hand on the top digit row and another on the arrows.  a-f is a bit more problematic one-handed, especially since my one-handed dvorak is a bit awkward.

Of course not all values are normalized from 0 to 1, so I have an unusual syntax: all hex literals are two-higit and are divided by 255.  Actually, what I wound up with is `0x`## so the 'x' can be a small superscript, since it's noisy and space hogging to have every line with a leading 0x.

This is one of those places where a score is not much like a general purpose programming language.  Where else would you be writing so many numeric literals that the appearance, width, and ease of them was so important that it's worth building it into the syntax?

The result?  I don't know.  I think I've gotten used to decimal.  The hex actually winds up being a tiny bit wider, since decimal's leading decimal point is actually quite narrow.  I'm lukewarm at the moment but I'll have to work with hex for a while and see if I start to like it more.  Actually I seem to recall that back in the Amiga tracker day there were only 6 bits of resolution, and that 10, 20, 30, and 40 made convenient rough points of reference.  I suppose I can do the same with 40, 80, c0, and ff.


With decimal:

With hex:

Sunday, March 11, 2012

records and lenses

So I've been experimenting with lenses lately.  Haskell's records and especially record update are always a low-grade annoyance.

Lenses haven't been the unqualified success that I'd hoped they would be.

None of the lens libraries really name operators how I want, but they all have a small set of primitives, so I made a little wrapper module with my own names.  As a bonus it means I can switch between lens libraries easily.

I decided on fclabels to start with, simply because it has some Template Haskell to define the lenses automatically.  However, it's not quite right: they expect you to use their own hardcoded naming scheme, which is never going to work for an existing project where you can't convert the entire thing in one go.  So I sent a patch to give control over the naming scheme.

fclabels has a bunch of operators for operating on StateMonad, but it turns out I can't use them because in a couple places I have layered state monads.  I can't use the overloaded 'get' and 'put' because they wouldn't know which state to look in.  I fussed around for a bit trying to do some typeclass magic but eventually decided it was too complicated since I can lift the pure operators into the monad with the appropriate 'modify' function.  So I wound up with (using fclabels infix TypeOperators thing, which I don't like, but oh well):

-- | Reverse compose lenses.
(#) :: (a :-> b) -> (b :-> c) -> (a :-> c)
infixr 9 #


-- | Get: @bval = a#b $# record@
($#) :: (f :-> a) -> f -> a
infixr 1 $#


-- | Set: @a#b =# 42 record@
(=#) :: (f :-> a) -> a -> f -> f
infix 1 =#


-- | Modify: @a#b =% (+1) record@
(=%) :: (f :-> a) -> (a -> a) -> f -> f
infix 1 =%


-- | Use like @a#b #> State.get@.
(#>) :: (Functor f) => (a :-> b) -> f a -> f b
infixl 4 #> -- same as <$>

So that all makes nice looking code, but it turns out TH is a pain.

First, it would fail to compile even though it worked fine from ghci.  I use .hs.o suffixes for .o files, and eventually I figured out that without -osuf, TH can't find the modules it needs to link in at compile time.  With -osuf, ghc emits _stub.c files with weird names, they don't get linked in, and link errors ensue.  7.4.1 would fix that, but last time I tried that I spent a day trying to get it to stop recompiling everything (and that's a showstopper because then hint wants to reinterpret everything all the time).  So I added a special hack to omit -osuf for the FFI-wrapper-using modules that generate stubs.

TH also imposes limitations on the order of declarations.  All of the types in a record have to be declared before the TH splice, and you have to put uses of template-generated variables below the splice.  This is quite inconvenient for my layout convention, which is to group each type and its functions together.  To make TH happy, I would have to rearrange all my modules to put all the type declarations at the top, the TH lens splices in the middle, and all the functions at the bottom.  Not only is it awkward to rearrange everything, I don't like that layout, since it means you have to do a lot of jumping back and forth from the data declarations to the functions on them.

Then of course there are the name clashes, which have been the subject of much ballyhoo on the ghc mailing list lately.  My record naming convention is that a record 'Record' will have fields like 'record_field1', 'record_field2', etc., so the obvious thing to do is strip the 'record_' part off.  I had to tweak the derivers for three kinds of clashes.  Out of 330 modules, of which 74 are tests, containing 107 record declarations, there was one clash with a Haskell keyword ("default", that's the most awkward Haskell keyword, especially annoying because it's almost never used).  Then there were 4 clashes with "constructors".  Given a "Synth" I tend to create a function called "synth" to insulate callers from the record's implementation.  When I add a field or change a type I can change the lowercase "synth" constructor and all is well.  But if a record in the same module has that type as a field, say "inst_synth" then there will be a clash.  Then I had 2 clashes which really were two records in the same module that wanted to have the same field name.  One was "name" (not surprising, lots of things have names), and the other was "control_map".

And TH slows down compilation, and slows down interpretation significantly.  I recompile a lot, and re-interpret (with ghci) just about constantly, so it's a bit disconcerting to lose ground on this front.

Once I finally got this all working, I ran into another problem.  Compiling normally worked, but compiling with optimization and profiling crashed, in different ways on different platforms.

On OS X:

ghc: internal error: PAP object entered!
    (GHC version 7.0.3 for x86_64_apple_darwin)
    Please report this as a GHC bug:  http://www.haskell.org/ghc/reportabug

On linux:

ghc: build/profile/obj/Util/Lens.hs.o: unknown symbol `CCCS'

So at this point, I gave up.  TH had already used up much more time that it would have saved, I wasn't looking forward to having to rearrange all my modules, so I just wrote out the lens declarations by hand.  With vim's '.' command it took a few minutes to crank out about 50.  I don't need lenses for many fields, only the ones that are likely to by modified in a nested way.

The result?  This:

set_keymap :: [(Score.Attributes, Midi.Key)] -> Patch -> Patch
set_keymap kmap patch = patch { patch_instrument = (patch_instrument patch)
    { inst_keymap = Map.fromList kmap } }

Looks nicer like this:

set_keymap :: [(Score.Attributes, Midi.Key)] -> Patch -> Patch
set_keymap kmap = instrument_#keymap =# Map.fromList kmap

One of the original motivations was this:

add_wdev :: String -> String -> Cmd.CmdL ()
add_wdev from to = modify_wdev_map $
    Map.insert (Midi.WriteDevice from) (Midi.WriteDevice to)


remove_wdev :: String -> Cmd.CmdL ()
remove_wdev from = modify_wdev_map $ Map.delete (Midi.WriteDevice from)


modify_wdev_map :: (Map.Map Midi.WriteDevice Midi.WriteDevice ->
        Map.Map Midi.WriteDevice Midi.WriteDevice)
    -> Cmd.CmdL ()
modify_wdev_map f = State.modify_config $ \config -> config
    { State.config_midi = modify (State.config_midi config) }
    where
    modify midi = midi
        { Instrument.config_wdev_map = f (Instrument.config_wdev_map midi) }

I have replaced that with:

add_wdev :: String -> String -> Cmd.CmdL ()
add_wdev from to = State.modify $ State.config#State.midi#Instrument.wdev_map
    =% Map.insert (Midi.WriteDevice from) (Midi.WriteDevice to)


remove_wdev :: String -> Cmd.CmdL ()
remove_wdev from = State.modify $ State.config#State.midi#Instrument.wdev_map
    =% Map.delete (Midi.WriteDevice from)


So I save a few lines of code.  More importantly, I save having to think about whether to write a 'modify_*' and where to put it.

Was it worth it?  Estimating for time saved in the future against the time to set all this up, I'm very unlikely to break even.  I'm not motivated to change existing code because the gains are only significant when I have nested updates, and most of those are already encapsulated in a 'modify_xyz' function, and I like to keep those around anyway for the insulation they provide.

So my conclusions about haskell's records and lenses:

Yes, Haskell records are annoying.  But it's a small annoyance, spread across a lot of code.  Lenses seem promising, but so far they've been underwhelming because to solve a diffuse annoyance you need something automatic, unobtrusive, and effortless.  I believe lenses could do that but they would have to be built-in, so no TH awkwardness and a guarantee they'll be consistently defined for every record from the very beginning.

About name clashes: two records with the same name in the same module is rare for me.  In fact all clashes are rare, but more common are clashes with other variables.  For instance, the constructor convention above.  It's also common to name a variable based on a record field, e.g. 'alloc = config_alloc config'.

The lens libraries are just getting started.  They're still fiddling around with various flavors operators and theoretically interesting but (to me) not so essential gee-gaws without a lot of attention to practical matters (like built-in Map or List lenses, or control over lens name derivation).  But the set of core operations is just get, set, and modify, so I have no doubt that they will mature rapidly if people start using them a lot.  But my impression is that not many people use them yet.

I'm not even sure I'll merge the lens branch.  It adds a certain amount of complexity (especially since the old record syntax is not going to go away) and the benefits are not unambiguously compelling.  Since you still can't use them unqualified for field access they're not that much more concise than standard haskell.  So they pretty much win only for record update and especially for nested record update.  Probably the next time I have to write some annoying nested record update I'll get motivated to go merge in the branch.

On the subject of the record discussion on the ghc mailing lately, I still think my own idea (http://hackage.haskell.org/trac/ghc/wiki/Records/SyntaxDirectedNameResolution) is the only one that would be an improvement over the existing situation (for me, of course).  All the focus over sharing record field names is of little use to me because most of my clashes are rare, and most of them are not between record field names.

If we had SDNR then the most important result is that I'd have lenses everywhere without having to think about them.  And they'd be worth using even for field access.

remove_wdev :: String -> Cmd.CmdL ()
remove_wdev from = State.modify $
    #wdev_map.#midi.#config =% Map.delete (Midi.WriteDevice from)
    -- See, I can't decide whether composition should go right or left...


lookup_wdev :: Midi.WriteDevice -> Cmd.CmdL (Maybe Midi.WriteDevice)
lookup_wdev wdev = Lens.map wdev . #wdev_map.#midi.#config wdev #> State.get

It would look even nicer if we could somehow get rid of the qualification and
for fun throw in []s for a map lens like in other languages:

lookup_wdev wdev = [wdev]wdev_map.midi.config #> State.get

But I'd settle for the # version since I can't see how the bottom one could happen without designing a new incompatible language.

Saturday, January 28, 2012

It works!

Here's the result of the giant previous post in 5 lines of a test:

    -- Trill transitions from 2 semitones to 1 semitone.
    equal (run
        [ ("*twelve", [(0, 0, "4a"), (4, 0, "i (4b)")])
        , ("t-diatonic", [(0, 0, "tr _ 1")])
        ])
        ([[(0, 69), (1, 71.25), (2, 70), (3, 71.75), (4, 71), (5, 72)]], [])

It's actually a little bit surprising when I try out the original motivation and what comes out is exactly what should.

Thursday, January 19, 2012

Pitch and transposition

I got stuck for a long time trying to implement a feature to support diatonic transposition (very relevant for e.g. trills) and enharmonics (not too relevant in 12TET but just scales can make use of them).  After getting halfway through an implementation which didn't work out, I decided I needed to step back a bit and analyze the problem, and only start implementation again once I knew what I was doing.  Unfortunately it wasn't all that successful, but I did wind up writing a lot:

A scale is just a mapping from a symbol to a certain frequency, but it gets complicated once transposition is involved.

Integral transposition is not very hard.  (4c) transposed chromatically becomes (4c#).  Once you introduce enharmonics it becomes more complicated because it could also be (4db), so you need to know the key.  In any scale other than 12TET these are likely different frequencies, so the distinction is important.

Now once you have a key of course diatonic transposition comes to mind.  It's important to support that because many ornaments (e.g. trill) are defined diatonically.  But if you have what you need to properly support chromatic transposition then you also have what you need for diatonic transposition.

This isn't so bad, but it means that transposition must be applied to symbolic pitches.  If a note is mapped to a frequency then it's too late to transpose it, so the obvious tactic of reducing a pitch to a frequency signal and then adding a transposition signal to it, is not going to work.  It doesn't even work if the frequency signal is instead measured in scale steps, because the information about the key has been lost.

Things become more complicated when I add the requirement than pitches are not integral.  For instance, a perfectly square trill will alternate between [+0, +1] diatonic steps, but many kinds of trills are rounded, and will thus be adding a fractional number of diatonic steps.  This means that the added interval should be scaled appropriately.  Adding .5 to (4a) should add a half-step, while adding .5 to (4b) would be a quarter-step.  Adding 1.5 should hence figure out the integral transposition, and then scale the remaining fraction apporpriately.

But then of course the underlying pitch itself may not be integral.  What does it mean to add a diatonic step to a note halfway between (4a) and (4b)?  Well, the interval it adds must be scaled between the whole step of (4a) and the half step of (4b), so it would be 0.75 of a step.  The example I like to use to visualize this is a trill on a note that is sliding from (4a) to (4b) in C major.  It should gradually change from a whole step alternation to a half step alternation.

Of course, since trills are not necessarily integral, now I have to combine both a non-integral pitch and a non-integral transposition.  It's a bit head-hurting at this point, but if adding 1 to a pitch halfway between (4a) and (4b) is 0.75 of a step, then adding half of that should be 0.375 of a step.  A half-diatonic trill from (4a) to (4b) should then change from a half step alternation to a quarter step.

Naturally, when I say "adding 0.375" it's not as simple as just adding two numbers.  The actual frequency change it represents depends, once again, on the note and scale.  The only place it doesn't is, by definition, any equal tempered scale.  This, I think, is the same problem as diatonic transposition, namely converting a scale of irregularly spaced intervals to a scale of regular ones.  So I think the same process can be applied twice: In the case of diatonic transposition, transposition is applied to notes of a certain mode and reduced to chromatic transposition in a certain scale.  Then the process is repeated to reduce chromatic transposition to NoteNumbers.

The question then is how to represent pitches so this can happen, so that each scale can have its own rules for how transposition works.  A simple scale is really just a mapping from symbols to frequencies, with no separate concept of diatonic and chromatic and no enharmonics.  A complex scale has all of those and can even be retuned over time so the pitches and intervals depend on the position in the score.  In addition, pitch is a continuously variable signal, not a scalar value.  Each pitch call produces a chunk of signal which may be a single sample or a section of a curve, and they have to be pieced together into a complete curve.  There is also a distinction between absolute and relative pitches.  A relative pitch signal is independent of scale, and has zero and negative values.


So.

    Approach #1

The simplest option is a sequence of reals representing scale degrees for both absolute and relative pitches.  This breaks down pretty quickly though.  A note halfway between A (0) and B (2) would sound at A# (1).  But if you transpose that by one diatonic step you need to know the 1 is halfway between A and B and should therefore be halfway between B and C, so 2.5.  But if the 1 was just an A#, the result should be B#, sounding at 3.  The same problem exists for non equal tempered scales at the chromatic level.

This implies that I need to keep the "from" and "to" pitches around, so pitch can be a sequence of triples: (from, to, distance).  A note halfway between A and B is (A, B, .5).  Now if I want to transpose I can apply the transposition to the from and to notes, and end up with (B, C, .5).  Then reduce to a frequency by interpolating between the frequencies of B and C.  But this only works when the transposition is integral.  How would I transpose by half a step?  The transposition would be halfway between the transposition for A->B and B->C, but there's no way to represent that in a triple if 'from' and 'to' are scale degrees.  In the same way that collapsing (A, B, .5) into A# loses information important for transposition, collapsing ((A, B, .5), (B, C, .5), .5) into (A+.5, B+.5, .5) also loses information.

The key thing is that only scale degrees can be transposed, so I can't have something like A+.5.  So I should be able to apply the (from, to, distance) pattern recursively:

scale_to_frequency = [(A, 0), (B, 2), (C, 3), (D, 5)]

(A, B, .5) + 1d --> 1 + 1.5 = 2.5
    ((A, B, 1), (B, C, 1), .5) -> (2, 3, .5) -> 2.5
    ((A, B, .5), (B, C, .5), 1) -> (1, 2.5, 1) -> 2.5

(A, B, .5) + .75d --> 1.5 * .75 = 1.125, so 1 + 1.125 = 2.125
    ((A, B, .5), (B, C, .5), .75) -> (1, 2.5, .75) -> 2.125
    ((A, B, .75), (B, C, .75), .5) -> (1.5, 2.75, .5) -> 2.125

(A, C, .5) + .75d --> 1.5 + .75d --> 1.5 + .75 * 2 = 3
    ((A, B, .75), (C, D, .75), .5) -> ((0, 2, .75), (3, 5, .75), .5) -> 3
    ((A, C, .5), (B, D, .5), .75) -> ((0, 3, .5), (2, 5, .5), .75) -> 3

As reflected above, there are two ways to apply a transposition.  Either apply it to each of the branches, splitting each into a (from, to, distance) transposition, or apply it at the top, leaving the left branch the same and transposing the right branch, and expressing 'distance' as the distance between the two expressions.  Experimentally they seem to give the same answers, though I don't understand the math well enough yet to understand why this is the case.  Distributive law, I expect.

Of course, this is given a simple scale where each degree has a direct mapping to a frequency.  In practice a degree would have to be an abstract object that can be transposed chromatically and diatonically:

class Scale d where
    chromatic :: Int -> d -> d
    diatonic :: Int -> d -> d
    reduce :: d -> Frequency

Hence:

data Degree d = I d | F (Degree d) (Degree d) Double
data Signal = forall d. (Scale d) => [(RealTime, Degree d)]

A more complicated scale, say just intonation tuned to a particular base frequency, would have degrees as follows:

data JustDegree { base :: Frequency, degree :: LetterPitch }
data LetterPitch = A | B | ... -- no accidentals

-- Or if accidentals are desired, with resolution to an arbitrary number
-- of flats or sharps:
data LetterPitch = Letter AtoG Accidental
data AtoG = A | B | ...
data Accidental = Neutral | Flat Int | Sharp Int

There is also the problem that the scale type variable will have to be reified into the data so the dynamic environment can remain monomorphic.  Presumably it could be sealed inside an existential type.

This appears to work, but it seems complicated.  A variable size structure like this doesn't lend itself to an efficient representation, which is desirable since I'll have arrays of these.  I am especially likely to have arrays where only the distance fraction is changing, and it seems like I should be able to avoid redundantly transposing all the note fields when they are mostly the same.

On the other hand, if I try to preserve sharing then each scale degree is just an extra pointer, and transposition is cheap if it's just consulting a table.  However, the recursive definition and type variable of 'Degree' ensure that those must be pointers, so I won't be able to get much space savings from unpacking.  It seems like I should be able to do some hackery like splitting the numeric values into separate vectors, but it's hard to think of how right now.

    Approach #2

Another approach is to represent pitch signals entirely as code, rather than data.

A pitch signal would just be 'type AbstractPitch = Deriver Signal.Pitch', i.e.  a function from the Dynamic state to a frequency signal.  Chromatic and diatonic transposition would be represented as signals in the dynamic environment.  This has some appeal because it means that "chromatic" and "diatonic" are simply conventions.  Scales can support or not support arbitrary forms of transposition at their choosing.  Of course, I suppose #1 can do this perfectly well too, just provide a 'transpose :: Transposition -> Int -> d' method.

This effectively delays the application of the transposition to the note in the same way as the (from, to, distance) representation does.  However, it's not nested in the same way, so all diatonic transpositions and chromatic transpositions would be collapsed.  So +1c +1d +1c would be collapsed to +2d +1c.  Are the results always the same?  I suppose whether or not they are depends on the definition of diatonic and chromatic for the particular scale, but I think for sensible scales they should be.

This is appealing because it's the same solution I use for the score in general.  Trying to create a data structure to represent the score in a way which can be later manipulated in the way I want is probably impossibly complicated given the range of manipulations.  In the same way, creating a pitch signal data type that can be later manipulated by transposition in the way I want winds up being complicated but still somewhat inflexible.  Delaying evaluation and presenting the manipulations as parameters to an abstract bit of code is similar idea to data-directed row extension as espoused by OO.

It also means that there is no concept of a "scale degree" and scales need not even have a concept of degrees, but simply names that will generate a given frequency in a given context that may include transposition.  This is apropos because I already have a "scale", Ratio, that is relative to another pitch and has no notion of steps or degrees or any of the usual structure that goes along with a scale.  In this world, pitches come from something a little more abstract and general than a scale.

However, this approach brings its own complications and difficulties.

Firstly, a pitch signal is actually generated by calls that themselves take the actual notes as arguments.  So a note call produces a single frequency, while a pitch call may create a constant segment, or interpolate from the previous note, or something more complicated.  Since only notes may be transposed, the pitch (a signal of frequencies in time) must also be delayed with the notes (a single frequency) delayed inside.  So there are two types: 'Note = Derive NoteNumber', and 'Pitch = Derive Signal.Pitch'.

As mentioned above, many pitch calls use the previous value, e.g. "interpolate linearly from previous value".  However, the previous value can't be a NN.  If the interpolate signal is changing, I need to reapply the interpolation to the source note on each sample.  Effectively, I consider the interpolate a distance from prev note and next note.  So the source note has to be a note, not an NN.  It also has to be a note representing the last sample emitted.  So if the interpolator is working at the NN level, say (b)-3nn, it has to construct that as a note and pass it to the next call.  I already have this same mechanism for control calls since they do exactly the same thing, but unfortunately I apparently can't reuse that for pitch calls because the previous value is threaded as part of the CallInfo by the track evaluator.  However, in this new system the call simply returns an unevaluated Pitch, and the actual sample values are not produced until later when all the CallInfos are long gone.  So instead of 'Pitch = Derive Signal.Pitch' I add an arg to thread the previous value explicitly: 'Pitch = (RealTime, Note) -> Derive (Signal.Pitch, (RealTime, Note))'.  Of course the Note actually has to be 'Maybe Note' since the first pitch call will not have a preceding note.  A Monoid instance for Pitch can thread them together.  But each pitch call still has to explicitly construct and return its "last note", which is a burden but can be hidden inside higher level utilities.

A further complication is that since pitch calls are now responsible for emitting the final pitch signal, they are responsible for taking the transpose signals into account.  This means that even a call that wants to emit a constant pitch must combine that with the interpolate signal in scope and may not be very constant at all!  On the flip side, it means pitch calls have control over whether transposition happens or not.  Unfortunately this doesn't mean much since pitch calls are not note calls, are not part of a scale, and thus likely not to have any interesting opinions on the matter.  Of course some note calls may very well ignore transpositions, but unless the pitch calls are going to be able to inspect the scale in effect for certain bits of hardcoded magic, the pitch call doesn't know that the note doesn't want to transpose and is thus obliged to call it at every transposition point even if the answer will always be the same.  Actually, there may well be some inspection of the scale since the pitch call still needs to know which signals the note considers transposition and should hence trigger a re-evaluation of the Note.  A scale that is not prepared to admit any transposition can simply give an empty set.

To eliminate this complexity I could think about trying to merge pitch and note calls and put everything into the scale, but the division is rather essential to factoring out scale independent functionality, like simple interpolation.

Efficiency is an additional concern.  Previously, a pitch signal would be evaluated once and various events underneath it could chop out the bits they are interested in.  However, if the pitch signal is now a deriver that is sensitive to the environment, every event that wants the signal will have to evaluate the whole thing again and there is no way for the deriver to know the event is only interested in a portion.  When the pitch track is below the event track (it's usually called the note track, but that's confusing in this context) it gets chopped up into one section per note, but if an event wants to know the pitch of a *following* event, it needs the pitch track already evaluated.  This is a particularly nettlesome aspect of score evaluation in general and I hope for a better solution, but if there isn't one then the redundant evaluation of a whole signal for every event may be too inefficient.  In that case, I may be able to hack on a cache to reuse the previous signal given that the transposition signals haven't changed since the last evaluation.

And of course this hints at a general problem when moving from data to code: I lose the ability to inspect the data.  I can evaluate a Pitch and look for a particular frequency, but if I want a "scale degree" out of it then there's no way.  In fact there's no real concept of a scale degree at all so I can't even ask simple questions like "what was the pitch class of the previous note" or, even harder due to the above evaluation order problem, "what is the pitch class of the next note".  But these are perfectly normal questions that I *will* need to ask.  Of course, given note calls in their full glory there may very well be no simple answer to that question.  But for scales which *do* have well defined dergees, which is after all likely to be most all of them, I will need a function 'NoteNumber -> Expr' where Expr is the textual representation of call that will generate that NoteNumber, or the nearest scale degree.  And then I could transpose a given Expr at the score level via the roundabout route of evaluating it to a NoteNumber in the proper transposing context and then converting that NoteNumber back to an Expr.  Such is the price of generality.

But such a price!  There is additional complexity in that tracklang Vals can now have Derivers inside them, courtesy of Note, which introduces a circular dependency between TrackLang and Derive.  Much of the similarity between control and pitch signals is lost; I can no longer deal with a pitch signal as easily as a control signal.  The simple concept of a signal of numbers or scale degree numbers is replaced by abstract pitches and notes which need to be evaluated in the correct context or there's a bug.  Also, a lot of code needs to be changed.

On the other hand, some code can be removed as well.  All of the PitchSignal data structure can be dropped, which is price already paid for trying (though failing) to support scales and transposition.

Another approach is to simply say transposition is not worth all the trouble and abandon it entirely.  But just about all pitch oriented ornaments are defined in terms of relative pitches, e.g. trill so that would be abandoning most of what I want to do.  Once I've accepted that I want a trill, wanting a diatonic trill is the next obvious step, since after all they mostly are.  Then, given that supporting continuously changing pitches is also central, I need to solve a diatonic trill on a changing base note.  Following that path seems to lead me inevitably to this conclusion.

Unfortunately, there's another feature missing: I need to be able to look at the pitch of other notes.  For example, a mordent needs to create new notes based on the initial pitch of a note.  But an abstract pitch signal doesn't give access to the individual notes inside.

Even more unfortunately I got most of the way through approach 2 before figuring this out.  And it was quite a bit of work because it changes quite a lot.

    Approach #3

Based on this, I modified approach 2 to be somewhat more conservative.  A PitchSignal remains a signal, that is, a vector of (RealTime, Pitch) pairs.  A pitch is a function Controls -> NoteNumber.  It can be transposed by composing a function that modifies the controls in front of it.

I get to reuse some of the old PitchSignal stuff (which treated a pitch as a (from, to, distance) triple) since this is a signal too.  But I keep the part from approach #2 where transposition is normal control signals with conventional names.  Though I could modify the pitch signals directly as before, if I want to support various different kinds of transpositions (so far I have chromatic, diatonic, and hz) I need something flexible that I can write directly into the score, and control signal names are just that.


It's a little frustrating to have spent so much time with ultimately rejected approaches before coming up with one that worked.  The whole idea behind writing down the requirements was to think of all the possibilities before spending time implementing something.  But even so I missed some requirements until it came time to actually convert existing code.  After thinking about the problem for such a long time I do have a kind of better handle on the problem and requirements, but of course that's only after the fact.  It would have been nice to have that perspective when it actually could have been useful.

It's interesting to observe that so much of the difficulty in interpreting the music score is controlling the order of evaluation.  With pitches, the problem is that the reduction of scale degrees to their frequencies must be delayed until all transpositions have been applied.  The purpose of the Deriver after all is to delay the evaluation of score elements until the full context, including pitch, is available, so they can make decisions based on that context.

This is somewhat related to the other key theme I've noticed, which is the tension between code and data.  Code is opaque but flexible, data is transparent but rigid.  To increase one is to diminish the other.  Since my main goal is flexibility in notation, I wind up favoring code over data.  The result is seemingly unavoidable complexity on the editing side, since this is where the lack of transparency hurts the most.

Sunday, October 23, 2011

State of things

I'm finally, and slowly, beginning to work with actual music.  Here's the state of things as of a few weeks back.  Pedal is working (and a bug there just fixed), and I just added a bounced note as a sub-block, but hadn't yet added the function to rename BlockIds, so it's still called "b28" instead of "bounce".  And of course this triggered yet another bughunt when I realized that the cached bounce sub-block results weren't being invalidated by editing.

It's still primitive, awkward, and buggy, but this "bounce" thing is the first hint of my plans actually beginning to be realized.  The bounced note has a composed tempo and itself is the second note of an arpeggio, which is beginning to show how these ornaments can be composed.  That's the real test: do I reach for these tools when I want a certain musical effect.  And of course I don't know of any other sequencer that could do this sort of thing.


It turns out that the mechanisms to make things compose nicely, namely track slicing and inversion, make evaluation more complicated and lead to tricky bugs.  I wish I knew a more principled way to get the effect I want, but so it goes.  The point is not to have a pretty program, but a useful one.

Friday, October 14, 2011

Tests did their job!

Debugging a problem with a sustain pedal going down too early made me realize that I had changed the definition of the value of a signal before the first sample from zero to the first sample.  So that had the counter-intuitive effect that a pedal beginning at a certain time would actually be counted as always being down if it didn't explicitly start with zero.  So there are two solutions to that, either change the pedal call so it puts a zero sample at time 0 if there isn't already a previous sample, or change the various sample functions so a signal is considered to be 0 before the first sample.  The first way is clearly ad-hoc and gross and just delaying the problem in some other call when I've forgotten about this behaviour, so it's time to change the signal definition yet again.

I seemed to recall I had added that "take the first value" behaviour at some point for some specific reason, and some fiddling with darcs annotate I found a couple of patches from more than a year ago that fixed bugs when I forgot to update everything, but couldn't find the patch that originally introduced it.  Stupid.  I should always document choices like that.  Turns out the documentation for the Signal module was several design changes out of date, and still documented the previous zero behaviour.

While I didn't find the rationale, checking the patches meant I had the set of functions that would have to be changed in mind, so I was more confident I wouldn't miss something.  So I made the change and ran the tests.

This sort of subtle interpretation change is pretty scary because it yields no type errors, and no crashes, but just that the derivation will be incorrect in some subtle way, such as the original sustain pedal problem.  But fortunately, every time I track down some subtle derivation bug I added a test for it, and when I ran the tests, sure enough the pitch signal sharing function failed its test.

Two notes overlapping but with different pitches have different pitch curves, as expected.  If those pitch curves are parallel, they can share a channel.  However, if a pitch signal is considered to start with the note, and signals are zero before the first sample, it looks like the second note jumps from pitch 0 to its real pitch, which is not parallel!

But why does the overlap check even look at the pitch curve before the beginning of the second note?  It turns out there's a bit of time inserted called the "control lead time" so controls can be set a little in advance of the note start.  MIDI has no concept of simultaneity and a pitch bend sent at the same time as a note on will cause an audible artifact.  There are tests explicitly verifying that notes that are overlapping after taking the control lead time into account still can't share channels so it was definitely intentional, and it seems to still makes sense, but I think adding the lead time to the end of the decay of the previous note rather than subtracting from the start of the current note would achieve the same effect without having to look at the pitch signal before the note begins.

So, give it a lot of thought, make the two line change of moving an addition from one expression to another, write a comment explaining why, rerun the tests.  This is exactly the kind of subtle issue that without the presence of the tests there is absolutely no way I would have foreseen.


And I have to say, a 13 line function with 22 lines of comments, 7 arguments, and a long history of minor tweaks is definitely a sore point.  I've thought a lot at various points about how to make it easier to explain, clearer, less error-prone, but failed every time.

As an aside, I have a new method of debugging derivation issues lately.  I wrote a function to dump the score from the UI in the simplified haskell literal format that the test functions use to construct a test score.  Then copy and paste that into a test, and see if running it there reproduces the problem.  I write a little predicate to characterize the problem (e.g. this particular note is on the wrong channel) or just eyeball it, and start cutting out score to see what happens to the result.  It seems to work ok but is still more clumsy than I want.  Hopefully I can think of something better later.

Since often the problem is that something *sounds* wrong, and has to be turned into a series of messages that *look* wrong, I also wrote a synthesizer state emulator.  Given a list of messages and their times, it can signal things like stuck notes, who is overlapping who, who has pitch bends where, etc.  I haven't actually found this one very useful yet, but even if I don't use it much for testing it should eventually become part of the process to turn recorded MIDI back into high level score.


And so, the final practical result of all that debugging is that I can play the piece back and the pedal is finally in the right place.  But music being what it is, I've gotten rather used to the pedal going down earlier.  Is it better there or did I just get used to it?  Who knows...