diff options
author | Andrew Cady <d@jerkface.net> | 2015-12-12 11:43:00 -0500 |
---|---|---|
committer | Andrew Cady <d@jerkface.net> | 2015-12-12 11:43:00 -0500 |
commit | 7f964d297995103eabe758516296f805f2c8b0a3 (patch) | |
tree | a18bbf5eeb1c69141c79a6bc8da38e3161d0f2f7 | |
parent | f06340344ceb88bc292db912220b4f8de638c78e (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.hs | 85 |
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) | |||
42 | import qualified Data.Set as Set | 42 | import qualified Data.Set as Set |
43 | import Data.Set (Set) | 43 | import Data.Set (Set) |
44 | 44 | ||
45 | import System.IO | ||
46 | |||
45 | verbose :: Bool | 47 | verbose :: Bool |
46 | verbose = False | 48 | verbose = False |
47 | 49 | ||
@@ -54,18 +56,25 @@ data TriadType = Major | Minor deriving (Show, Eq, Ord) | |||
54 | data Triad = Triad TriadType Event.Pitch Event.Velocity deriving (Show, Eq, Ord) | 56 | data Triad = Triad TriadType Event.Pitch Event.Velocity deriving (Show, Eq, Ord) |
55 | 57 | ||
56 | data LoopState = LoopState { | 58 | data 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 | |||
70 | data Metronome = Metronome { | ||
71 | _metronomeStart :: TimeSpec, | ||
72 | _metronomeInterval :: TimeSpec, | ||
73 | _metronomeTicked :: TimeSpec | ||
65 | } | 74 | } |
66 | 75 | ||
67 | initializeState :: TimeSpec -> LoopState | 76 | initializeState :: TimeSpec -> LoopState |
68 | initializeState now = LoopState False [] Map.empty Set.empty createQueue (StartRecording now) (StartRecording now) now | 77 | initializeState now = LoopState False [] Map.empty Set.empty createQueue Nothing (StartRecording now) (StartRecording now) now |
69 | 78 | ||
70 | data LoopEnv = LoopEnv { | 79 | data LoopEnv = LoopEnv { |
71 | _saver :: Chan CompleteRecording, | 80 | _saver :: Chan CompleteRecording, |
@@ -180,6 +189,7 @@ mainLoop :: MidiController () | |||
180 | mainLoop = do | 189 | mainLoop = 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 | |||
188 | waitThreads :: MidiController () | 198 | waitThreads :: MidiController () |
189 | waitThreads = gets _waitThreads >>= mapM_ liftIO | 199 | waitThreads = gets _waitThreads >>= mapM_ liftIO |
190 | 200 | ||
201 | metronome :: MidiController () | ||
202 | metronome = 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 | |||
216 | tsMod :: forall a. Num a => TimeSpec -> TimeSpec -> a | ||
217 | tsMod x y = fromInteger (timeSpecAsNanoSecs x `mod` timeSpecAsNanoSecs y) | ||
218 | |||
219 | tsDiv :: forall a. Num a => TimeSpec -> TimeSpec -> a | ||
220 | tsDiv x y = fromInteger (timeSpecAsNanoSecs x `div` timeSpecAsNanoSecs y) | ||
221 | |||
191 | playScheduled :: MidiController () | 222 | playScheduled :: MidiController () |
192 | playScheduled = queueAction $ runScheduledIO (TimeSpec 1 0) $ uncurry alsaDelayNoteEv | 223 | playScheduled = 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 | |||
247 | processCommand "C" = do | 278 | processCommand "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)) |
281 | processCommand "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 } | ||
250 | processCommand "C'" = do | 297 | processCommand "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 | |||
263 | type MidiControllerT m = RWST LoopEnv () LoopState m | 310 | type MidiControllerT m = RWST LoopEnv () LoopState m |
264 | type MidiController = MidiControllerT IO | 311 | type MidiController = MidiControllerT IO |
265 | 312 | ||
313 | takeNoteTimes :: Playable p => Int -> p -> [TimeSpec] | ||
314 | takeNoteTimes n p = fst <$> take n (filter (isNoteReallyOn . snd) (playableEvents p)) | ||
315 | |||
316 | isNoteReallyOn :: Codec.Midi.Message -> Bool | ||
317 | isNoteReallyOn (Codec.Midi.NoteOn _ _ v) | v > 0 = True | ||
318 | isNoteReallyOn _ = False | ||
319 | |||
266 | playRecording :: Playable p => p -> MidiController () | 320 | playRecording :: Playable p => p -> MidiController () |
267 | playRecording = playEvents . playableEvents | 321 | playRecording = playEvents . playableEvents |
268 | 322 | ||
@@ -417,9 +471,22 @@ maybeReadLine = asks _lineReader >>= liftIO . tryTakeMVar | |||
417 | startLineReader :: IO (MVar String) | 471 | startLineReader :: IO (MVar String) |
418 | startLineReader = do | 472 | startLineReader = 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 | ||
478 | getChLine :: IO String | ||
479 | getChLine = 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 | |||
423 | saveMidi :: forall (m :: * -> *). (MonadIO m, MonadState LoopState m, MonadReader LoopEnv m) => Recording -> m () | 490 | saveMidi :: forall (m :: * -> *). (MonadIO m, MonadState LoopState m, MonadReader LoopEnv m) => Recording -> m () |
424 | saveMidi recording = do | 491 | saveMidi recording = do |
425 | saver <- asks _saver | 492 | saver <- asks _saver |