Euterpea / Euterpea2

Euterpea version 2

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Simultaneous MIDI events can arrive to device out of order with lazy playback

donya opened this issue · comments

MIDI events can sometimes arrive out of order when sent back-to-back. I do not currently know the cause. This will reliably do it on both Windows and Mac with GHC 8.4.3 and 8.6.3 and with both Euterpea 2.0.7 and Euterpea 2.0.6 (so it is not a PortMidi version issue. Likely it was still present in older versions of both GHC and Euterpea since the MIDI backend in question has not changed for a long time.

x = tempo 1.3 $ forever $ c 4 en
play x

Notes will typically be dropped near the start of playback, but it is different each time and often resolves after some time to not dropping any notes. Not all tempo modifiers and durations produce this effect; many are completely safe.

Examining the outgoing MIDI messages reveals that this is sometimes happening with values like x:

[(t1, On), (t2, Off), (t2, On), (t3, Off), ...] -> [(t1, On), (t2, On), (t2, Off), (t3, Off), ...] (pseudocode)

resulting in a infinitesimally small note at time t2 and then silence until t3.

Only "play" and "playDev" are affected, not "playS" on finite portions of the music, indicating that this is something to do with lazy playback.

An immediate fix for anyone suffering from this problem is to substitute use of one of the following playback functions for play and playDev respectively. The strategy taken is to trim an imperceptible amount of duration from the notes to try to avoid having exactly simultaneous note on/off messages whose orders must be interpreted somehow.

import Euterpea
import Euterpea.IO.MIDI.MidiIO
import Control.DeepSeq

play' :: (ToMusic1 a, NFData a) => Music a -> IO ()
play' = playC defParams{perfAlg=fixPerf} where
    fixPerf = map (\e -> e{eDur = max 0 (eDur e - 0.000001)}) . perform

playDev' :: (ToMusic1 a, NFData a) => Int -> Music a -> IO ()
playDev' dev = playC defParams{devID = Just (unsafeOutputID dev), perfAlg=fixPerf} where
    fixPerf = map (\e -> e{eDur = max 0 (eDur e - 0.000001)}) . perform

This is not an unreasonable solution; most music software I'm aware of does not actually permit simultaneous off-on patterns like Euterpea is attempting to do.

Of course, this will not fix timing issues caused by sever computation bubbles encountered during lazy playback (nor is there any reasonable fix for that scenario).

After some thought, I am simply going to make this the default behavior for play and playDev until a better solution arises. I will NOT be pushing this change to Hackage until I have a chance to look into the issue further. This will mean:

  • Hackage will continue have Euterpea 2.0.7 with the regular versions of play and playDev.

  • Github will have 2.0.8 with fixed versions while it undergoes testing. Users of 2.0.7 experiencing the problem can either install 2.0.8 from GitHub or manually use the fix shown above. 2.0.8 may eventually receive a different solution to this problem if one exists.

I will be making a push later today with this change.