summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndrew Cady <d@jerkface.net>2015-12-10 17:18:30 -0500
committerAndrew Cady <d@jerkface.net>2015-12-10 17:18:30 -0500
commit66f65478a22fcc3ff024f0c1456ece372aae554b (patch)
treeb461ea2b145012b9e62609e84e8d08e12121a674
parentcd9e7854db78041b7453a3d1bcfa45a95fe53604 (diff)
Implement saving of .mid files
(Currently accessible only under testing command "save".)
-rw-r--r--axis-of-eval.cabal2
-rw-r--r--midi-dump.hs59
2 files changed, 55 insertions, 6 deletions
diff --git a/axis-of-eval.cabal b/axis-of-eval.cabal
index dac8e24..d1aee15 100644
--- a/axis-of-eval.cabal
+++ b/axis-of-eval.cabal
@@ -37,7 +37,7 @@ executable midi-dump
37 build-depends: 37 build-depends:
38 base, time, containers, haskore, alsa-seq, alsa-core, mtl, clock, 38 base, time, containers, haskore, alsa-seq, alsa-core, mtl, clock,
39 sqlite-simple, bytestring, base-prelude, midi-alsa, midi, psqueues, 39 sqlite-simple, bytestring, base-prelude, midi-alsa, midi, psqueues,
40 transformers, semigroups, HCodecs 40 transformers, semigroups, HCodecs, threads
41 main-is: midi-dump.hs 41 main-is: midi-dump.hs
42 other-modules: AlsaSeq, Midi, RealTimeQueue 42 other-modules: AlsaSeq, Midi, RealTimeQueue
43 ghc-options: -threaded -W -Wall -O2 43 ghc-options: -threaded -W -Wall -O2
diff --git a/midi-dump.hs b/midi-dump.hs
index d2b6c15..eacaa03 100644
--- a/midi-dump.hs
+++ b/midi-dump.hs
@@ -31,6 +31,9 @@ import qualified Sound.ALSA.Sequencer.RealTime as RealTime
31import Midi 31import Midi
32import RealTimeQueue as Q hiding (null) 32import RealTimeQueue as Q hiding (null)
33import qualified Codec.Midi 33import qualified Codec.Midi
34import Codec.Midi (Midi(..), FileType(..), TimeDiv(..))
35
36import qualified Control.Concurrent.Thread as Thread
34 37
35verbose :: Bool 38verbose :: Bool
36verbose = False 39verbose = False
@@ -45,6 +48,7 @@ data Triad = Triad TriadType Event.Pitch Event.Velocity deriving (Show, Eq)
45 48
46data LoopState = LoopState { 49data LoopState = LoopState {
47 _wantExit :: Bool, 50 _wantExit :: Bool,
51 _waitThreads :: [IO (Thread.Result ())],
48 _keysDown :: MidiPitchMap, 52 _keysDown :: MidiPitchMap,
49 _triad :: Maybe Triad, 53 _triad :: Maybe Triad,
50 _scheduled :: Q.Queue Event.Data, 54 _scheduled :: Q.Queue Event.Data,
@@ -54,7 +58,7 @@ data LoopState = LoopState {
54} 58}
55 59
56initializeState :: TimeSpec -> LoopState 60initializeState :: TimeSpec -> LoopState
57initializeState now = LoopState False Map.empty Nothing createQueue (StartRecording now) (StartRecording now) now 61initializeState now = LoopState False [] Map.empty Nothing createQueue (StartRecording now) (StartRecording now) now
58 62
59data LoopEnv = LoopEnv { 63data LoopEnv = LoopEnv {
60 _saver :: Chan CompleteRecording, 64 _saver :: Chan CompleteRecording,
@@ -109,8 +113,27 @@ sqlInsert = fromString . concat $
109 , "VALUES (?,?,?,?, ?,?,?,?, ?)" 113 , "VALUES (?,?,?,?, ?,?,?,?, ?)"
110 ] 114 ]
111 115
112sqlSelectEVERYTHING :: MidiController [CompleteRecording] 116sqlSelectRECENT :: MidiController [CompleteRecording]
113sqlSelectEVERYTHING = do 117sqlSelectRECENT = do
118 conn <- asks _sqlite
119 fmap reverse $ liftIO $ query_ conn $ fromString . concat $
120 [ "SELECT "
121 , "start_sec,"
122 , "start_nsec,"
123 , "end_sec,"
124 , "end_nsec,"
125 , "first_sec,"
126 , "first_nsec,"
127 , "last_sec,"
128 , "last_nsec,"
129 , "midi"
130 , " FROM axis_input",
131 " ORDER BY start_sec DESC, start_nsec DESC ",
132 " LIMIT 10"
133 ]
134
135_sqlSelectEVERYTHING :: MidiController [CompleteRecording]
136_sqlSelectEVERYTHING = do
114 conn <- asks _sqlite 137 conn <- asks _sqlite
115 liftIO $ query_ conn $ fromString . concat $ 138 liftIO $ query_ conn $ fromString . concat $
116 [ "SELECT " 139 [ "SELECT "
@@ -151,7 +174,12 @@ mainLoop = do
151 maybeReadLine >>= maybe processMidi processCommand 174 maybeReadLine >>= maybe processMidi processCommand
152 wantExit <- gets _wantExit 175 wantExit <- gets _wantExit
153 playScheduled 176 playScheduled
154 unless wantExit mainLoop 177 if wantExit
178 then waitThreads
179 else mainLoop
180
181waitThreads :: MidiController ()
182waitThreads = gets _waitThreads >>= mapM_ liftIO
155 183
156playScheduled :: MidiController () 184playScheduled :: MidiController ()
157playScheduled = queueAction $ runScheduledIO (TimeSpec 1 0) $ uncurry alsaDelayNoteEv 185playScheduled = queueAction $ runScheduledIO (TimeSpec 1 0) $ uncurry alsaDelayNoteEv
@@ -195,7 +223,8 @@ processCommand :: String -> MidiController ()
195processCommand "exit" = modify $ \s -> s { _wantExit = True } 223processCommand "exit" = modify $ \s -> s { _wantExit = True }
196-- processCommand "" = return () 224-- processCommand "" = return ()
197processCommand "" = gets _replay >>= playRecording 225processCommand "" = gets _replay >>= playRecording
198processCommand "dump" = sqlSelectEVERYTHING >>= playRecording . mconcat 226processCommand "dump" = sqlSelectRECENT >>= playRecording . mconcat
227processCommand "save" = sqlSelectRECENT >>= saveRecording "saved.mid" . mconcat
199processCommand "C" = do 228processCommand "C" = do
200 let notes = (Event.NoteEv Event.NoteOn . mkNote) <$> [60, 64, 67] 229 let notes = (Event.NoteEv Event.NoteOn . mkNote) <$> [60, 64, 67]
201 forM_ notes (delayNoteEv (TimeSpec 0 0)) 230 forM_ notes (delayNoteEv (TimeSpec 0 0))
@@ -218,6 +247,9 @@ type MidiController = MidiControllerT IO
218playRecording :: Playable p => p -> MidiController () 247playRecording :: Playable p => p -> MidiController ()
219playRecording = playEvents . playableEvents 248playRecording = playEvents . playableEvents
220 249
250saveRecording :: Playable p => FilePath -> p -> MidiController ()
251saveRecording file = saveEvents file . playableEvents
252
221fixedOutputChannel :: Maybe Codec.Midi.Channel 253fixedOutputChannel :: Maybe Codec.Midi.Channel
222fixedOutputChannel = Just 0 254fixedOutputChannel = Just 0
223 255
@@ -225,6 +257,23 @@ setOutputChannel :: Codec.Midi.Message -> Codec.Midi.Message
225setOutputChannel = case fixedOutputChannel of Just n -> setChannel n 257setOutputChannel = case fixedOutputChannel of Just n -> setChannel n
226 Nothing -> id 258 Nothing -> id
227 259
260saveEvents :: FilePath -> [RecordedEvent] -> MidiController ()
261saveEvents file evts@(_:_) = do
262 (_, wait) <- liftIO $ Thread.forkIO $ Codec.Midi.exportFile file midi
263 modify $ \s -> s { _waitThreads = wait:_waitThreads s }
264 where
265 midi = Midi MultiTrack (TicksPerBeat $ fromIntegral ticksPerBeat) [track ++ [(0, Codec.Midi.TrackEnd)]]
266 track = zip (toDeltas (conv . subtract (head delays) <$> delays)) events
267 (delays, events) = unzip $ reverse $ (fmap.fmap) setOutputChannel evts -- TODO: do not use fixed channel
268 conv :: TimeSpec -> Int
269 conv = fromIntegral . (`div` (10^(9::Int) `div` ticksPerSecond)) . timeSpecAsNanoSecs
270 ticksPerSecond = ticksPerBeat * beatsPerSecond
271 beatsPerSecond = 120 `div` 60
272 ticksPerBeat :: Integer
273 -- ticksPerBeat = 2^(15::Int) - 1
274 ticksPerBeat = 2400
275saveEvents _ _ = return ()
276
228playEvents :: [RecordedEvent] -> RWST LoopEnv () LoopState IO () 277playEvents :: [RecordedEvent] -> RWST LoopEnv () LoopState IO ()
229playEvents evts@(_:_) = 278playEvents evts@(_:_) =
230 mapM_ (uncurry delayNoteEv) (zip (subtract (head delays) <$> delays) events) 279 mapM_ (uncurry delayNoteEv) (zip (subtract (head delays) <$> delays) events)