1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
| -- Copyright 2013 Evan Laforge
-- This program is distributed under the terms of the GNU General Public
-- License 3.0, see COPYING or http://www.gnu.org/licenses/gpl-3.0.txt
{-# LANGUAGE CPP #-}
{- | Implement midi thru by mapping InputNotes to MIDI messages.
This is effectively a recreation of the deriver and MIDI performer, but
geared to producing a single note immediately rather than deriving and
performing an entire score. But since derivation and performance are both
very complicated, it's doomed to be complicated and inaccurate.
The rationale is that the performer is oriented around a stream of events
when their durations are known, while this must derive a single key, and in
real time. However, it's also due to history (derivation used to be much
simpler), and concerns about efficiency, so in the future I'll probably
move towards reusing as much of the deriver and performer as possible.
Note that actually much of the deriver is already reused, courtesy of
'Perf.derive_at'. Also, 'Scale.scale_input_to_nn' may have a shortcut
implementation, but for complicated scales falls back on derivation.
An implementation that fully reuses deriver and performer is in
"Cmd.Instrument.CUtil".insert_expr.
This is a very complicated thru and might be too slow. It has to deal
with:
- Remap input pitch according to scale and control pitch bend range
(done by NoteEntry) and instrument pb range. This means keeping track of
previous note id and pb val.
- Remap addr based on addrs assign to instrument, assigning round-robin.
This means keeping track of note ids assigned to addrs and serial numbers
for each addr.
It's different from the usual simple thru in that it attempts to assign
control messages to a single note. So if the instrument is multiplexed,
control changes (including pitch bend) will go only to the last sounding
key. This also means that controls will not go through at all unless
there is a note sounding.
It should be possible to reduce latency by bypassing the responder loop and
running this in its own thread. It does mean the InputNote work is
duplicated and synchronization of state, such as current instrument info,
gets more complicated because it has to go through an mvar or something.
I should find out what makes the responder so slow. Profile it!
- The sync afterwards: Some mechanism to find out if no Ui.State changes
have happened and skip sync.
- Marshalling the cmd list: cache the expensive parts. The only changing
bit is the focus cmds, so keep those until focus changes.
- Duplicate NoteInput conversions.
- Instrument is looked up on every msg just for pb_range, so cache that.
Effectively, the short-circuit thread is another way to cache this.
-}
module Cmd.MidiThru (
cmd_midi_thru, midi_thru_instrument
-- * util
, channel_messages
#ifdef TESTING
, module Cmd.MidiThru
#endif
) where
import qualified Data.Map as Map
import qualified Data.Set as Set
import qualified Util.Log as Log
import qualified Util.Seq as Seq
import qualified Midi.Midi as Midi
import qualified Ui.Ui as Ui
import qualified Ui.UiConfig as UiConfig
import qualified Cmd.Cmd as Cmd
import qualified Cmd.EditUtil as EditUtil
import qualified Cmd.InputNote as InputNote
import Cmd.InputNote (NoteId)
import qualified Cmd.Msg as Msg
import qualified Cmd.Perf as Perf
import qualified Cmd.Selection as Selection
import qualified Derive.Attrs as Attrs
import qualified Derive.BaseTypes as BaseTypes
import qualified Derive.Controls as Controls
import qualified Derive.Derive as Derive
import qualified Derive.Scale as Scale
import qualified Derive.Score as Score
import qualified Perform.Midi.Control as Control
import qualified Perform.Midi.Patch as Patch
import Perform.Midi.Patch (Addr)
import qualified Perform.Pitch as Pitch
import qualified Instrument.Common as Common
import qualified Instrument.Inst as Inst
import Global
-- | Send midi thru, addressing it to the given Instrument.
cmd_midi_thru :: Msg.Msg -> Cmd.CmdId Cmd.Status
cmd_midi_thru msg = do
input <- case msg of
Msg.InputNote input -> return input
_ -> Cmd.abort
score_inst <- Cmd.abort_unless =<< EditUtil.lookup_instrument
attrs <- Cmd.get_instrument_attributes score_inst
midi_thru_instrument score_inst attrs input
return Cmd.Continue
midi_thru_instrument :: Score.Instrument -> Attrs.Attributes
-> InputNote.Input -> Cmd.CmdId ()
midi_thru_instrument score_inst attrs input = do
resolved <- Cmd.get_instrument score_inst
let code_of = Common.common_code . Inst.inst_common . Cmd.inst_instrument
case Cmd.inst_thru (code_of resolved) of
Nothing -> default_thru resolved score_inst attrs input
Just thru -> thru attrs input
-- | I used to keep track of the previous PitchBend to avoid sending extra ones.
-- But it turns out I don't actually know the state of the MIDI channel, so
-- now I always send PitchBend. I'm not sure why I ever thought it could work.
-- I could still do this by tracking channel state at the Midi.Interface level.
-- I actually already do that a bit tracking with note_tracker, but it's simpler
-- to just always send PitchBend, unless it becomes a problem.
default_thru :: Cmd.ResolvedInstrument -> Score.Instrument -> Cmd.ThruFunction
default_thru resolved score_inst attrs input = do
(patch, config) <- Cmd.abort_unless $ Cmd.midi_instrument resolved
let addrs = Patch.config_addrs config
unless (null addrs) $ do
scale <- Perf.get_scale =<< Selection.track
(input_nn, ks) <- Cmd.require
(pretty (Scale.scale_id scale) <> " doesn't have " <> pretty input)
=<< input_to_nn score_inst (Patch.patch_attribute_map patch)
(Patch.settings#Patch.scale #$ config) scale attrs input
wdev_state <- Cmd.get_wdev_state
let result = input_to_midi
(Patch.settings#Patch.pitch_bend_range #$ config)
wdev_state addrs input_nn
whenJust result $ \(thru_msgs, wdev_state) -> do
Cmd.modify_wdev_state (const wdev_state)
let ks_msgs = concatMap (keyswitch_to_midi thru_msgs) ks
mapM_ (uncurry Cmd.midi) (ks_msgs ++ thru_msgs)
-- | The keyswitch winds up being simultaneous with the note on. Especially
-- stupid VSTs like kontakt will sometimes miss a keyswitch if it doesn't have
-- enough lead time. There's not much I can do about that, but to avoid making
-- the keyswitch too short I hold it down along with the note.
keyswitch_to_midi :: [(Midi.WriteDevice, Midi.Message)] -> Patch.Keyswitch
-> [(Midi.WriteDevice, Midi.Message)]
keyswitch_to_midi msgs ks = case msum (map note_msg msgs) of
Nothing -> []
Just (addr, key, is_note_on) -> map (with_addr addr) $
if is_note_on then [Patch.keyswitch_on key ks]
else maybe [] (:[]) (Patch.keyswitch_off ks)
where
note_msg (dev, Midi.ChannelMessage chan msg) = case msg of
Midi.NoteOn key _ -> Just ((dev, chan), key, True)
Midi.NoteOff key _ -> Just ((dev, chan), key, False)
_ -> Nothing
note_msg _ = Nothing
-- | Realize the Input as a pitch in the given scale.
input_to_nn :: Cmd.M m => Score.Instrument -> Patch.AttributeMap
-> Maybe Patch.Scale -> Scale.Scale -> Attrs.Attributes -> InputNote.Input
-> m (Maybe (InputNote.InputNn, [Patch.Keyswitch]))
input_to_nn inst attr_map patch_scale scale attrs inote = case inote of
InputNote.NoteOn note_id input vel ->
justm (convert input) $ \(nn, ks) ->
return $ Just (InputNote.NoteOn note_id nn vel, ks)
InputNote.PitchChange note_id input ->
justm (convert input) $ \(nn, _) ->
return $ Just (InputNote.PitchChange note_id nn, [])
InputNote.NoteOff note_id vel ->
return $ Just (InputNote.NoteOff note_id vel, ks)
where
ks = maybe [] (fst . snd) $ Common.lookup_attributes attrs attr_map
InputNote.Control note_id control val ->
return $ Just (InputNote.Control note_id control val, [])
where
convert input = do
(block_id, _, track_id, pos) <- Selection.get_insert
-- I ignore _logs, any interesting errors should be in 'result'.
(result, _logs) <- Perf.derive_at block_id track_id $
Derive.with_instrument inst $
filter_transposers scale $
Scale.scale_input_to_nn scale pos input
case result of
Left err -> throw $ "derive_at: " <> err
-- This just means the key isn't in the scale, it happens a lot so
-- no need to shout about it.
Right (Left BaseTypes.InvalidInput) -> Cmd.abort
Right (Left err) -> throw $ pretty err
Right (Right nn) -> do
let (result, not_found) =
convert_pitch attr_map patch_scale attrs nn
when not_found $
Log.warn $ "inst " <> pretty inst <> " doesn't have attrs "
<> pretty attrs <> ", understood attrs are: "
<> pretty (Common.mapped_attributes attr_map)
return result
where throw = Cmd.throw . ("error deriving input key's nn: " <>)
-- | Remove transposers because otherwise the thru pitch doesn't match the
-- entered pitch and it's very confusing. However, I retain 'Controls.octave'
-- and 'Controls.hz' because those are used to configure a scale, e.g. via
-- 'Patch.config_controls', and the pitch is nominally the same.
filter_transposers :: Scale.Scale -> Derive.Deriver a
-> Derive.Deriver a
filter_transposers scale = Derive.with_controls transposers
where
transposers = zip
(filter (`notElem` [Controls.octave, Controls.hz])
(Set.toList (Scale.scale_transposers scale)))
(repeat (Score.untyped mempty))
-- | This is a midi thru version of 'Perform.Midi.Convert.convert_midi_pitch'.
-- It's different because it works with a scalar NoteNumber instead of
-- a Score.Event with a pitch signal, which makes it hard to share code.
convert_pitch :: Patch.AttributeMap -> Maybe Patch.Scale -> Attrs.Attributes
-> Pitch.NoteNumber
-> (Maybe (Pitch.NoteNumber, [Patch.Keyswitch]), Bool)
-- ^ The Bool is True if the attrs were non-empty but not found.
convert_pitch attr_map patch_scale attrs nn =
case Common.lookup_attributes attrs attr_map of
Nothing -> ((, []) <$> maybe_pitch, attrs /= mempty)
Just (_, (keyswitches, maybe_keymap)) ->
( (, keyswitches) <$> maybe maybe_pitch set_keymap maybe_keymap
, False
)
where
maybe_pitch = apply_patch_scale nn
apply_patch_scale = maybe Just Patch.convert_scale patch_scale
set_keymap (Patch.UnpitchedKeymap key) = Just $ Midi.from_key key
set_keymap (Patch.PitchedKeymap low _ low_pitch) =
(+ Midi.from_key (low - low_pitch)) <$> maybe_pitch
input_to_midi :: Control.PbRange -> Cmd.WriteDeviceState
-> [Addr] -> InputNote.InputNn
-> Maybe ([(Midi.WriteDevice, Midi.Message)], Cmd.WriteDeviceState)
input_to_midi pb_range wdev_state addrs input_nn = case alloc addrs input_nn of
(Nothing, _) -> Nothing
(Just addr, new_state) -> Just (map (with_addr addr) msgs, state)
where
(msgs, note_key) = InputNote.to_midi pb_range
(Cmd.wdev_note_key wdev_state) input_nn
state = merge_state new_state
(wdev_state { Cmd.wdev_note_key = note_key })
where
alloc = alloc_addr (Cmd.wdev_note_addr wdev_state)
(Cmd.wdev_addr_serial wdev_state) (Cmd.wdev_serial wdev_state)
merge_state :: Maybe (Map NoteId Addr, Map Addr Cmd.Serial)
-> Cmd.WriteDeviceState -> Cmd.WriteDeviceState
merge_state new_state old = case new_state of
Nothing -> old
Just (note_addr, addr_serial) -> old
{ Cmd.wdev_note_addr = note_addr
, Cmd.wdev_addr_serial = addr_serial
, Cmd.wdev_serial = Cmd.wdev_serial old + 1
}
-- | If the note_id is already playing in an addr, return that one. Otherwise,
-- if it's not NoteOn or NoteOff, abort. If it is, pick a free addr, and if
-- there is no free one, pick the oldest one. Update the wdev state and assign
-- the note id to the addr.
alloc_addr :: Map NoteId Addr -> Map Addr Cmd.Serial -> Cmd.Serial
-> [Addr] -- ^ Addrs allocated to this instrument.
-> InputNote.InputNn
-> (Maybe Addr, Maybe (Map NoteId Addr, Map Addr Cmd.Serial))
alloc_addr note_addr addr_serial serial addrs input
| Just addr <- Map.lookup note_id note_addr, addr `elem` addrs =
case input of
InputNote.NoteOff _ _ -> (Just addr, unassign addr)
_ -> (Just addr, Nothing)
| not (new_note input) = (Nothing, Nothing)
| Just addr <- oldest = (Just addr, assign addr)
| otherwise = (Nothing, Nothing) -- addrs must be null
where
note_id = InputNote.input_id input
new_note (InputNote.NoteOn {}) = True
new_note (InputNote.NoteOff {}) = True
new_note _ = False
assign addr = Just
(Map.insert note_id addr note_addr, Map.insert addr serial addr_serial)
unassign addr = Just
(Map.delete note_id note_addr, Map.insert addr serial addr_serial)
-- Always pick the channel with the oldest note, whether or not it's
-- allocated. Previously I would try to pick a free one, but reusing
-- a free channel led to audible artifacts with long-ringing instruments.
oldest = Seq.minimum_on (flip Map.lookup addr_serial) addrs
with_addr :: Addr -> Midi.ChannelMessage -> (Midi.WriteDevice, Midi.Message)
with_addr (wdev, chan) msg = (wdev, Midi.ChannelMessage chan msg)
-- * util
-- | Send ChannelMessages to the addrs (or just the lowest addr) of the current
-- instrument. This bypasses all of the WriteDeviceState stuff so it won't
-- cooperate with addr allocation, but hopefully this won't cause problems for
-- simple uses like keymapped instruments.
channel_messages :: Cmd.M m => Maybe Score.Instrument -- ^ use this inst, or
-- the one on the selected track if Nothing.
-> Bool -> [Midi.ChannelMessage] -> m ()
channel_messages maybe_inst first_addr msgs = do
addrs <- get_addrs maybe_inst
let addrs2 = if first_addr then take 1 addrs else addrs
sequence_ [Cmd.midi wdev (Midi.ChannelMessage chan msg)
| (wdev, chan) <- addrs2, msg <- msgs]
get_addrs :: Cmd.M m => Maybe Score.Instrument -> m [Addr]
get_addrs maybe_inst = do
inst <- maybe (Cmd.abort_unless =<< EditUtil.lookup_instrument)
return maybe_inst
alloc <- Ui.allocation inst <#> Ui.get
return $ case UiConfig.alloc_backend <$> alloc of
Just (UiConfig.Midi config) -> Patch.config_addrs config
_ -> []
|