Karya, built on Mon Jul 24 11:39:07 PDT 2017 (patch 33511aca01257b76b88de7c7a2763b7a965c084e)

Perform.Midi.Perform

Description

Main entry point for Perform.Midi. Render Deriver output down to actual midi events.

Synopsis

# Documentation

This winds up being 100, which is loud but not too loud and distinctive-looking.

data State Source #

Performance state. This is a snapshot of the state of the various functions in the performance pipeline. You should be able to resume performance at any point given a RealTime and a State.

I don't do that anymore, and this is left over from when I cached the performance. I removed the cache but left the state visible.

Constructors

 State Fields

Instances

 # Methods(==) :: State -> State -> Bool #(/=) :: State -> State -> Bool # # MethodsshowsPrec :: Int -> State -> ShowS #show :: State -> String #showList :: [State] -> ShowS # # MethodsformatList :: [State] -> Doc Source #

data Config Source #

Constructors

Instances

 # Methods(==) :: Config -> Config -> Bool #(/=) :: Config -> Config -> Bool # # MethodsshowsPrec :: Int -> Config -> ShowS #showList :: [Config] -> ShowS #

perform :: State -> Configs -> Events -> (MidiEvents, State) Source #

Render instrument tracks down to midi messages, sorted in timestamp order. This should be non-strict on the event list, so that it can start producing MIDI output as soon as it starts processing Events.

# constants

Turn on debug logging. This is hardcoded because debugging can generate lots of logs and performance has to be efficient.

This winds up being 100, which is loud but not too loud and distinctive-looking.

A keyswitch gets this much lead time before the note it is meant to apply to. Some synthesizers (kontakt at least) will occasionally not notice a keyswitch that comes too close to its note.

Most synths don't respond to control change and pitch bend instantly, but smooth it out, so if you set pitch bend immediately before playing the note you will get a little sproing. Put pitch bends before their notes by this amount.

control_lead_time can be flattened out if there isn't time for it. This happens when there is another note on the same previous channel that would overlap it. To avoid an audible artifact on the tail of the previous note, I omit the lead time in that case. However, I still need a minimum amount of lead time because some MIDI patches use the state of the controls at NoteOn time to configure the whole note. A tiny gap should be enough to make sure the control changes arrive first, but short enough that it's not audible on the previous note.

The root of the problem, like so many problems with MIDI, is that it's highly stateful, nothing happens simultaneously, and channels are precious.

Subtract this much from every NoteOff. Some synthesizers don't handle simultaneous note on and note off of the same pitch well. I actually only need the gap for a NoteOff followed by NoteOn of the same pitch, but it's simpler to just subtract it from all notes.

Each note will have at least this duration. The reason is that some synthesizers (kontakt at least) will sometimes not notice a note which is too short. Usually these notes are percussion and come in with zero duration.

Honestly nothing really surprises me about Kontakt anymore.

# perform

data State Source #

Performance state. This is a snapshot of the state of the various functions in the performance pipeline. You should be able to resume performance at any point given a RealTime and a State.

I don't do that anymore, and this is left over from when I cached the performance. I removed the cache but left the state visible.

Constructors

 State Fields

Instances

 # Methods(==) :: State -> State -> Bool #(/=) :: State -> State -> Bool # # MethodsshowsPrec :: Int -> State -> ShowS #show :: State -> String #showList :: [State] -> ShowS # # MethodsformatList :: [State] -> Doc Source #

data Config Source #

Constructors

Instances

 # Methods(==) :: Config -> Config -> Bool #(/=) :: Config -> Config -> Bool # # MethodsshowsPrec :: Int -> Config -> ShowS #showList :: [Config] -> ShowS #

perform :: State -> Configs -> Events -> (MidiEvents, State) Source #

Render instrument tracks down to midi messages, sorted in timestamp order. This should be non-strict on the event list, so that it can start producing MIDI output as soon as it starts processing Events.

# channelize

This isn't directly the midi channel, since it goes higher than 15, but will later be mapped to midi channels.

type ChannelizeState = [(T.Event, Channel)] Source #

Overlapping events and the channels they were given.

Assign channels. Events will be merged into the the lowest channel they can coexist with.

A less aggressive policy would be to distribute the instrument among all of its addrs and only share when out of channels, but it seems like this would quickly eat up all the channels, forcing a new note that can't share to snag a used one.

channelize_event :: Configs -> [(T.Event, Channel)] -> T.Event -> (Channel, [Log.Msg]) Source #

This doesn't pay any mind to instrument channel assignments, except as an optimization for instruments with only a single channel. Channels are actually assigned later by allot.

shareable_chan :: [(T.Event, Channel)] -> T.Event -> (Maybe.Maybe Channel, [(Channel, Text)]) Source #

Find a channel from the list of overlapping (T.Event, Channel) all of whose events can share with the given event. Return the rest of the channels and the reason why they can't be used.

Can the two events coexist in the same channel without interfering? The reason this is not commutative is so I can assume the start of old is equal to or precedes the start of new and save a little computation.

This is by far the most finicky function in the whole module, because this is the core decision when multiplexing channels.

Are the controls equal in the given range?

Notes with differing c_aftertouch can always share, since they are addressed by MIDI key. If the key is the same, they already can't share.

Previously I insisted that the controls be identical, but now I check within the overlapping range only. What's more, I only check where the events actually overlap, not including decay time.

Each event is supposed to only include the controls within its range. So given a series of notes with a changing control, each note includes a bit of control, which then becomes constant as soon as the next note begins, since the rest of the control belongs to the next note. This means the two notes can't share, because one has a flat signal during its decay while the other has the moving signal. But in practice this turns out to be inconvenient, because it means that a series of notes with a crescendo will be divided across multiple channels. That's ok if there are enough channels, but if there aren't, then this can introduce lots of bad-sounding channel stealing.

TODO However, not counting the decay means that very audible controls will be shared and cause artifacts. I think the real difference is that controls like dyn and mod are not very audible during the decay, so it's ok to share them. But another control, like a filter cutoff, might be very obvious. So perhaps there should be a per-control configuration, but I'll worry about that only if it ever becomes a problem.

# allot channels

allot :: AllotState -> Configs -> [LEvent.LEvent (T.Event, Channel)] -> ([LEvent.LEvent (T.Event, Patch.Addr)], AllotState) Source #

channelize will assign channels based on whether the notes can coexist without interfering with each other. allot reduces those channels down to the real midi channels assigned to the instrument, stealing if necessary. It steals from the longest-unused channel.

Events with instruments that have no address allocation in the config will be dropped.

Constructors

 AllotState Fieldsast_available :: !(Map Patch.Addr (RealTime.RealTime, AllotKey))Allocated addresses, and when they were last used. This is used by the voice stealer to figure out which voice is ripest for plunder. It also has the AllotKey so the previous allotment can be deleted.ast_allotted :: !(Map AllotKey Allotted)Map input channels to an instrument address in the allocated range. Once an (inst, chan) pair has been allotted to a particular Addr, it should keep going to that Addr, as long as voices remain.

Instances

 # Methods # MethodsshowList :: [AllotState] -> ShowS # # MethodsformatList :: [AllotState] -> Doc Source #

type AllotKey = (Instrument, Channel) Source #

Channelize makes sure that a (inst, ichan) key identifies events that can share channels.

data Allotted Source #

Constructors

 Allotted Fields_allotted_addr :: !Patch.Addr allotted_voices :: ![RealTime.RealTime]End time for each allocated voice._allotted_voice_count :: !Patch.VoicesMaximum length for allotted_voices.

Instances

 # Methods # MethodsshowList :: [Allotted] -> ShowS # # MethodsformatList :: [Allotted] -> Doc Source #

Try to find an Addr for the given Event. If that's impossible, return a log msg.

If channelize decided that two events have the same channel, then they can go to the same addr, as long as it has voices left. Otherwise, take over another channel.

Record this addr as now being allotted, and add its voice allocation.

Steal the least recently used address for the given instrument, and return how many voices it supports.

Nothing voices means no limit, and in this case it'll pick a big number. I initially feared keeping track of voice allocation would be wasteful for addrs with no limitation, but profiling revealed no detectable difference. So either it's not important or my profiles are broken.

# perform notes

As in WriteDeviceState, map an Addr to the Instrument active at that address.

Used to emit keyswitches or program changes.

Map from an address to the last time a note was playing on that address. This includes the last note's decay time, so the channel should be reusable after this time.

Used to give leading cc times a little breathing room.

It only needs to be 'min cc_lead (now - note_off)'

Pass an empty AddrInst because I can't make any assumptions about the state of the synthesizer. The one from the wdev state might be out of date by the time this performance is played.

Given an ordered list of note events, produce the appropriate midi msgs. The input events are ordered, but may overlap.

Arguments

 :: PerformState -> Maybe.Maybe RealTime.RealTime next note with the same addr -> (T.Event, Patch.Addr) -> (PerformState, MidiEvents)

Emit msgs to set the channel state, and msgs for a single note.

Figure out of any msgs need to be emitted to convert the channel state to the given event on the given addr. This means keyswitches and program changes.

If there's no chan state always emit msgs, since in general there's no way to know what state the synth is in. If I do know (e.g. playback will pass the current addr_inst) I can filter out expensive messages like program change. TODO implement playback with addr_inst when I implement pchange

Another strategy would be to always emit msgs and rely on playback filter, but that would triple the number of msgs, which seems excessive.

TODO support program change, I'll have to get ahold of patch_initialize.

Emit keyswitch msgs to adjust the channel to the new instrument.

TODO if the last note was a hold keyswitch, this will leave the keyswitch down. Technically I should clean that up, but it's a hassle because I'd need to keep the keyswitch down state in the PerformState so perform_notes can clean them all up, or let adjust_chan_state look into the future so it knows if there will be another note. But in practice, all notes get turned off after playing so the keyswitch should be cleaned up by that.

## perform note

Arguments

 :: RealTime.RealTime -> Maybe.Maybe RealTime.RealTime next note with the same addr -> T.Event -> Patch.Addr -> (MidiEvents, RealTime.RealTime) (msgs, note_off)

Emit MIDI for a single event.

Perform the note on and note off.

Perform control change messages.

Get pitch at the given point of the signal.

The pitch bend always tunes upwards from the tempered note. It would be slicker to use a negative offset if the note is eventually going above unity, but that's too much work.

Get the Midi.Key that will be used for the event, without pitch bend.

Return the (pos, msg) pairs, and whether the signal value went out of the allowed control range, 0--1.

Trim a signal to the proper time range and emit (X, Y) pairs. The proper time range is complicated since there are two levels of priority. Controls within the note's start to end+decay are always emitted. The end+decay is put into the NoteOffMap so the next note will yield control_lead_time if necessary. Samples after end+decay are also emitted, but trimmed so they won't overlap the next note's start - control_lead_time.

channelize respects control_lead_time, so I expect msgs to be scheduled on their own channels if possible.

If the signal has consecutive samples with the same value, this will emit unnecessary CCs, but they will be eliminated by postprocessing.

# post process

Keep a running state for each channel and drop duplicate msgs.

Some context free post-processing on the midi stream.

Having to deal with Log is ugly... can't I get that out with fmap?

Sort almost-sorted MidiEvents. Events may be out of order by as much as control_lead_time. This happens because perform_signal adds events between 0--control_lead_time before the note, which can violate the precondition of Seq.merge_asc_lists.

I tried to come up with a way for the events to come out sorted even with perform_signal, but creativity failed me, so I resorted to this hammer.

# event

The end of an event after taking decay into account. The note shouldn't be sounding past this time.

# util

Merge an unsorted list of sorted lists of midi messages.

Arguments

 :: [(T.Event, a)] -> ([(T.Event, a)] -> T.Event -> (a, [Log.Msg])) -> Events -> ([LEvent.LEvent (T.Event, a)], [(T.Event, a)]) (output for each event, final overlapping state)

Map the given function across the events, passing it previous events it overlaps with. The previous events passed to the function are paired with its previous return values on those events. The overlapping events are passed in reverse order, so the most recently overlapping is first.