summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndrew Cady <d@jerkface.net>2015-12-12 11:43:00 -0500
committerAndrew Cady <d@jerkface.net>2015-12-12 11:43:00 -0500
commit7f964d297995103eabe758516296f805f2c8b0a3 (patch)
treea18bbf5eeb1c69141c79a6bc8da38e3161d0f2f7
parentf06340344ceb88bc292db912220b4f8de638c78e (diff)
Implement a metronome.
You provide a tempo by playing the metronome on the MIDI keyboard. Then press M-m to have the sequencer continue the same tempo on the MIDI drum channel.
-rw-r--r--midi-dump.hs85
1 files changed, 76 insertions, 9 deletions
diff --git a/midi-dump.hs b/midi-dump.hs
index 24d0ef1..5e0859b 100644
--- a/midi-dump.hs
+++ b/midi-dump.hs
@@ -42,6 +42,8 @@ import Data.Time.Clock (picosecondsToDiffTime, UTCTime)
42import qualified Data.Set as Set 42import qualified Data.Set as Set
43import Data.Set (Set) 43import Data.Set (Set)
44 44
45import System.IO
46
45verbose :: Bool 47verbose :: Bool
46verbose = False 48verbose = False
47 49
@@ -54,18 +56,25 @@ data TriadType = Major | Minor deriving (Show, Eq, Ord)
54data Triad = Triad TriadType Event.Pitch Event.Velocity deriving (Show, Eq, Ord) 56data Triad = Triad TriadType Event.Pitch Event.Velocity deriving (Show, Eq, Ord)
55 57
56data LoopState = LoopState { 58data LoopState = LoopState {
57 _wantExit :: Bool, 59 _wantExit :: Bool,
58 _waitThreads :: [IO (Thread.Result ())], 60 _waitThreads :: [IO (Thread.Result ())],
59 _keysDown :: MidiPitchMap, 61 _keysDown :: MidiPitchMap,
60 _triad :: Set Triad, 62 _triad :: Set Triad,
61 _scheduled :: Q.Queue Event.Data, 63 _scheduled :: Q.Queue Event.Data,
62 _recording :: Recording, 64 _metronome :: Maybe Metronome,
63 _replay :: Recording, 65 _recording :: Recording,
64 _lastTick :: TimeSpec 66 _replay :: Recording,
67 _lastTick :: TimeSpec
68}
69
70data Metronome = Metronome {
71 _metronomeStart :: TimeSpec,
72 _metronomeInterval :: TimeSpec,
73 _metronomeTicked :: TimeSpec
65} 74}
66 75
67initializeState :: TimeSpec -> LoopState 76initializeState :: TimeSpec -> LoopState
68initializeState now = LoopState False [] Map.empty Set.empty createQueue (StartRecording now) (StartRecording now) now 77initializeState now = LoopState False [] Map.empty Set.empty createQueue Nothing (StartRecording now) (StartRecording now) now
69 78
70data LoopEnv = LoopEnv { 79data LoopEnv = LoopEnv {
71 _saver :: Chan CompleteRecording, 80 _saver :: Chan CompleteRecording,
@@ -180,6 +189,7 @@ mainLoop :: MidiController ()
180mainLoop = do 189mainLoop = do
181 maybeReadLine >>= maybe processMidi processCommand 190 maybeReadLine >>= maybe processMidi processCommand
182 wantExit <- gets _wantExit 191 wantExit <- gets _wantExit
192 metronome
183 playScheduled 193 playScheduled
184 if wantExit 194 if wantExit
185 then waitThreads 195 then waitThreads
@@ -188,6 +198,27 @@ mainLoop = do
188waitThreads :: MidiController () 198waitThreads :: MidiController ()
189waitThreads = gets _waitThreads >>= mapM_ liftIO 199waitThreads = gets _waitThreads >>= mapM_ liftIO
190 200
201metronome :: MidiController ()
202metronome = gets _metronome >>= mapM_ f
203 where
204 f (Metronome start interval ticked) = do
205 now <- getAbsTime
206 let next = now + interval - (now `tsMod` interval)
207 when (next > ticked) $ do
208 let delay = next - now
209 delayNoteEv delay $ metronote 127
210 -- delayNoteEv (metroDuration + delay) $ metronote 0
211 modify $ \s -> s { _metronome = Just $ Metronome start interval next }
212
213 metronote vel = Event.NoteEv Event.NoteOn $ Event.simpleNote (Event.Channel 9) (Event.Pitch 37) (Event.Velocity vel)
214 -- metroDuration = TimeSpec 0 (1*10^(6::Integer))
215
216tsMod :: forall a. Num a => TimeSpec -> TimeSpec -> a
217tsMod x y = fromInteger (timeSpecAsNanoSecs x `mod` timeSpecAsNanoSecs y)
218
219tsDiv :: forall a. Num a => TimeSpec -> TimeSpec -> a
220tsDiv x y = fromInteger (timeSpecAsNanoSecs x `div` timeSpecAsNanoSecs y)
221
191playScheduled :: MidiController () 222playScheduled :: MidiController ()
192playScheduled = queueAction $ runScheduledIO (TimeSpec 1 0) $ uncurry alsaDelayNoteEv 223playScheduled = queueAction $ runScheduledIO (TimeSpec 1 0) $ uncurry alsaDelayNoteEv
193-- TODO: flush ALSA output here (and remove flush from playNoteEv) 224-- TODO: flush ALSA output here (and remove flush from playNoteEv)
@@ -247,6 +278,22 @@ processCommand "dump" = sqlSelectEVERYTHING >>= saveRecording "dumped.mid" . mco
247processCommand "C" = do 278processCommand "C" = do
248 let notes = (Event.NoteEv Event.NoteOn . mkNote) <$> [60, 64, 67] 279 let notes = (Event.NoteEv Event.NoteOn . mkNote) <$> [60, 64, 67]
249 forM_ notes (delayNoteEv (TimeSpec 0 0)) 280 forM_ notes (delayNoteEv (TimeSpec 0 0))
281processCommand "M-m" = do
282 now <- getAbsTime
283 m <- gets _metronome
284 times <- takeNoteTimes 8 <$> gets _replay
285 let deltas = drop 1 $ toDeltas times
286 deltas' = dropOutliers deltas
287 len = length deltas'
288 dropOutliers xs@(x:_) = takeWhile ((2 * x) >) xs
289 dropOutliers [] = []
290 if isNothing m && (len >= 3) then do
291 let interval = sum deltas' `tsDiv` fromIntegral len
292 nextTick = now + interval - (now `tsMod` interval)
293 lastTick = nextTick - interval
294 modify $ \s -> s { _metronome = Just $ Metronome lastTick interval nextTick }
295 else
296 modify $ \s -> s { _metronome = Nothing }
250processCommand "C'" = do 297processCommand "C'" = do
251 let notes = (Event.NoteEv Event.NoteOn . mkNote) <$> [60, 64, 67] 298 let notes = (Event.NoteEv Event.NoteOn . mkNote) <$> [60, 64, 67]
252 forM_ notes (delayNoteEv (TimeSpec 2 0)) 299 forM_ notes (delayNoteEv (TimeSpec 2 0))
@@ -263,6 +310,13 @@ processCommand str = liftIO $ putStrLn $ "Unknown command: " ++ str
263type MidiControllerT m = RWST LoopEnv () LoopState m 310type MidiControllerT m = RWST LoopEnv () LoopState m
264type MidiController = MidiControllerT IO 311type MidiController = MidiControllerT IO
265 312
313takeNoteTimes :: Playable p => Int -> p -> [TimeSpec]
314takeNoteTimes n p = fst <$> take n (filter (isNoteReallyOn . snd) (playableEvents p))
315
316isNoteReallyOn :: Codec.Midi.Message -> Bool
317isNoteReallyOn (Codec.Midi.NoteOn _ _ v) | v > 0 = True
318isNoteReallyOn _ = False
319
266playRecording :: Playable p => p -> MidiController () 320playRecording :: Playable p => p -> MidiController ()
267playRecording = playEvents . playableEvents 321playRecording = playEvents . playableEvents
268 322
@@ -417,9 +471,22 @@ maybeReadLine = asks _lineReader >>= liftIO . tryTakeMVar
417startLineReader :: IO (MVar String) 471startLineReader :: IO (MVar String)
418startLineReader = do 472startLineReader = do
419 mv <- liftIO newEmptyMVar 473 mv <- liftIO newEmptyMVar
420 _thread <- liftIO $ forkIO (forever $ tryIOError getLine >>= either (const $ putMVar mv "exit") (putMVar mv)) 474 hSetBuffering stdin NoBuffering
475 _thread <- liftIO $ forkIO (forever $ tryIOError getChLine >>= either (const $ putMVar mv "exit") (putMVar mv))
421 return mv 476 return mv
422 477
478getChLine :: IO String
479getChLine = getChar >>= f ""
480 where
481 f acc '\n' = return $ reverse acc
482 f _ '\ESC' = (("M-" ++) . unControlCheck) <$> getChar
483 f "" '\^D' = return "exit"
484 f _ c | isControl c = return $ unControl c
485 f acc c = getChar >>= f (c:acc)
486 unControl = ("C-" ++) . return . chr . (+ (ord 'a' - 1)) . ord
487 unControlCheck c | isControl c = unControl c
488 | otherwise = return c
489
423saveMidi :: forall (m :: * -> *). (MonadIO m, MonadState LoopState m, MonadReader LoopEnv m) => Recording -> m () 490saveMidi :: forall (m :: * -> *). (MonadIO m, MonadState LoopState m, MonadReader LoopEnv m) => Recording -> m ()
424saveMidi recording = do 491saveMidi recording = do
425 saver <- asks _saver 492 saver <- asks _saver