hmidi-0.2.3.1: System/MIDI/Sync.hs
-- | MIDI sync to an external clock source.
--
-- To avoid confusion:
-- In our terminology, /beat/ means a quarter note (the same thing as the B in BPM).
-- In MIDI terminology however, a \"MIDI beat\" means a sixteenth note.
--
-- With our notion of beats, one bar is 4 beats (in 4/4 signature, that is)
--
module System.MIDI.Sync
( Beats, BPM, openSourceWithSync
)
where
--------------------------------------------------------------------------------
import Control.Monad
import Control.Concurrent
import Control.Concurrent.MVar
import System.MIDI
--------------------------------------------------------------------------------
-- | Song position measured in beats (that is, quarter notes), starting from zero.
-- So with 120 BPM, you will have song position 120 after one minute.
type Beats = Double
-- | Estimated BPM
type BPM = Double
oneTwentyFourth = 1/24 :: Double
lambda = 0.05 :: Double -- ad-hoc speed of bpm adjustement
-- | Opens a midi source with the possibility to sync to it.
--
-- The user callback gets the the song position in /beats/,
-- and also we return functions to query to song position and
-- the estimated BPM. You may want to round the BPM to the nearest
-- integer if that is appropriate. Song position is Nothing when
-- the playback is stopped.
--
-- Note that when first used, it may need some time to calibrate
-- the bpm correctly, so start your MIDI host, press play, and
-- wait a few second. Afterwards, it should be reasonably ok.
-- Also if you do fast realtime BPM changes,
-- it will be a tiny little bit behind.
--
-- Note that we forward all messages (including clock messages) to
-- the user, so you can implement your own handling of transport
-- (start/stop/continue) or send messages on clock if you want.
--
openSourceWithSync
:: Source -- ^ midi source
-> (BPM -> Maybe Beats -> MidiEvent -> IO ()) -- ^ user callback
-> IO (Connection, IO (Maybe Beats), IO BPM) -- ^ (connection, song_position, estimated_bpm)
openSourceWithSync src userCallback = do
theLastPos <- newMVar 0 :: IO (MVar Beats) -- last song position
theBpmEst <- newMVar 120 :: IO (MVar BPM) -- last bpm estimation
thePlayFlag <- newMVar False :: IO (MVar Bool) -- whether we are playing or stopped
theLastClock <- newMVar 0 :: IO (MVar TimeStamp) -- timestamp of last clock signal
theLastQuery <- newMVar 0 :: IO (MVar Beats) -- last queried pos
theLastClocks <- newMVar [0] :: IO (MVar [TimeStamp]) -- timestamps of last clock signal
let queryPos tstamp = do
b <- readMVar thePlayFlag
if b
then do
lastpos <- readMVar theLastPos -- song position at the last clock/start message
bpm <- readMVar theBpmEst -- estimated bpm
lastclock <- readMVar theLastClock -- time of the last clock/start message
let tdiff = fromIntegral (tstamp - lastclock) / 60000.0 :: Double -- in minutes
let newpos0 = lastpos + tdiff * bpm -- extrapolate since last clock signal
lastquery <- takeMVar theLastQuery
let newpos = max lastquery newpos0 -- make it monotone in time (!)
putMVar theLastQuery newpos
return (Just newpos)
else return Nothing
let queryBPM = readMVar theBpmEst
let handle (MidiEvent tstamp msg) = case msg of
SongPosition midibeats -> do
let pos = fromIntegral midibeats / 6
replaceMVar theLastPos pos
replaceMVar theLastQuery pos
SRTStart -> do
replaceMVar theLastPos 0
replaceMVar theLastQuery 0
replaceMVar theLastClock tstamp
replaceMVar theLastClocks [tstamp]
replaceMVar thePlayFlag True
SRTStop -> replaceMVar thePlayFlag False
SRTContinue -> do
replaceMVar theLastClock tstamp
replaceMVar thePlayFlag True
Reset -> do
replaceMVar theLastPos 0
replaceMVar theLastQuery 0
replaceMVar thePlayFlag False
replaceMVar theBpmEst 120
SRTClock -> do
lastclock <- takeMVar theLastClock
lastclocks <- takeMVar theLastClocks
bpm <- takeMVar theBpmEst
lastpos <- takeMVar theLastPos
let lastpos' = lastpos + oneTwentyFourth
let tdiff = fromIntegral (tstamp - lastclock ) / 60000.0 :: Double
let tdiff12 = if length lastclocks < 12
then 0
else fromIntegral (tstamp - lastclocks !! 11) / 60000.0 :: Double
let bpm' = if length lastclocks < 12
then (1-lambda)*(max 30 $ min 480 bpm) + lambda*(max 30 $ min 480 $ oneTwentyFourth/tdiff)
else (1-lambda)*(max 30 $ min 480 bpm) + lambda*(max 30 $ min 480 $ 0.5/tdiff12)
putMVar theLastClock tstamp
putMVar theLastClocks $ take 24 (tstamp:lastclocks)
putMVar theLastPos lastpos'
putMVar theBpmEst bpm'
-- print (bpm',(tdiff,1/24/tdiff),(tdiff12,0.5/tdiff12))
_ -> return ()
let syncCallback event@(MidiEvent tstamp _) = do
handle event
mbpos <- queryPos tstamp
bpm <- queryBPM
userCallback bpm mbpos event
conn <- openSource src (Just syncCallback)
return (conn, currentTime conn >>= queryPos, queryBPM)
--------------------------------------------------------------------------------
replaceMVar :: MVar a -> a -> IO ()
replaceMVar mv x = do
_ <- tryTakeMVar mv
putMVar mv x
--------------------------------------------------------------------------------