diff options
author | Gregor Kleen <gkleen@yggdrasil.li> | 2015-06-21 19:40:40 +0200 |
---|---|---|
committer | Gregor Kleen <gkleen@yggdrasil.li> | 2015-06-21 19:40:40 +0200 |
commit | 4658cc95745dbdffd7bc1be2e61fa463b28b4a16 (patch) | |
tree | 0656da1577123f9f4eb05b72d66ad6c4682c5661 /trivmix | |
parent | 5aeef88338cd761066ba196472e22f2c55fc846a (diff) | |
download | trivmix-4658cc95745dbdffd7bc1be2e61fa463b28b4a16.tar trivmix-4658cc95745dbdffd7bc1be2e61fa463b28b4a16.tar.gz trivmix-4658cc95745dbdffd7bc1be2e61fa463b28b4a16.tar.bz2 trivmix-4658cc95745dbdffd7bc1be2e61fa463b28b4a16.tar.xz trivmix-4658cc95745dbdffd7bc1be2e61fa463b28b4a16.zip |
Added adjmix
Diffstat (limited to 'trivmix')
-rw-r--r-- | trivmix/Trivmix.hs | 215 |
1 files changed, 215 insertions, 0 deletions
diff --git a/trivmix/Trivmix.hs b/trivmix/Trivmix.hs new file mode 100644 index 0000000..9f0cf22 --- /dev/null +++ b/trivmix/Trivmix.hs | |||
@@ -0,0 +1,215 @@ | |||
1 | {-# LANGUAGE RecordWildCards #-} | ||
2 | |||
3 | import Foreign.C.Types (CFloat(..)) | ||
4 | import qualified Sound.JACK as Jack | ||
5 | import qualified Sound.JACK.Audio as Audio | ||
6 | |||
7 | import Options.Applicative | ||
8 | |||
9 | import Data.Maybe | ||
10 | |||
11 | import System.Directory | ||
12 | import System.FilePath | ||
13 | import System.Posix.Files | ||
14 | import System.Posix.IO | ||
15 | import System.Posix.Types | ||
16 | import System.Environment | ||
17 | import System.Process | ||
18 | |||
19 | import Control.Concurrent | ||
20 | import Control.Concurrent.MVar | ||
21 | import Control.Concurrent.Chan | ||
22 | |||
23 | import qualified Control.Monad.Trans.Class as Trans | ||
24 | import qualified Control.Monad.Exception.Synchronous as Sync | ||
25 | |||
26 | import Control.Exception | ||
27 | import System.IO.Error | ||
28 | import System.IO | ||
29 | |||
30 | import System.FileLock | ||
31 | import System.INotify | ||
32 | |||
33 | import Data.Char | ||
34 | import Data.Function | ||
35 | |||
36 | import Control.Monad | ||
37 | |||
38 | import Trivmix.Types | ||
39 | |||
40 | data Options = Options | ||
41 | { input :: String | ||
42 | , output :: String | ||
43 | , client :: String | ||
44 | , run :: Maybe String | ||
45 | , levelFiles :: [FilePath] | ||
46 | } | ||
47 | |||
48 | optionParser :: Parser Options | ||
49 | optionParser = Options <$> | ||
50 | (fromMaybe "in" <$> optional (strOption ( long "input" | ||
51 | <> metavar "STRING" | ||
52 | ) | ||
53 | ) | ||
54 | ) | ||
55 | <*> (fromMaybe "out" <$> optional (strOption ( long "output" | ||
56 | <> metavar "STRING" | ||
57 | ) | ||
58 | ) | ||
59 | ) | ||
60 | <*> strOption ( long "client" | ||
61 | <> metavar "STRING" | ||
62 | ) | ||
63 | <*> optional ( strOption ( long "run" | ||
64 | <> metavar "FILE" | ||
65 | ) | ||
66 | ) | ||
67 | <*> some (strArgument ( metavar "FILE..." | ||
68 | <> help "Files that contain levels to assume and synchronize" | ||
69 | ) | ||
70 | ) | ||
71 | |||
72 | watchedAttrs :: [EventVariety] | ||
73 | watchedAttrs = [ Modify | ||
74 | , Move | ||
75 | , MoveIn | ||
76 | , MoveOut | ||
77 | , MoveSelf | ||
78 | , Create | ||
79 | , Delete | ||
80 | , DeleteSelf | ||
81 | ] | ||
82 | |||
83 | initialLevel :: Level | ||
84 | initialLevel = def | ||
85 | |||
86 | defFileMode :: FileMode | ||
87 | defFileMode = foldl unionFileModes nullFileMode [ ownerReadMode | ||
88 | , ownerWriteMode | ||
89 | , groupReadMode | ||
90 | , groupWriteMode | ||
91 | , otherReadMode | ||
92 | ] | ||
93 | |||
94 | defDirectoryMode :: FileMode | ||
95 | defDirectoryMode = foldl unionFileModes nullFileMode [ ownerModes | ||
96 | , groupModes | ||
97 | , otherReadMode | ||
98 | , otherExecuteMode | ||
99 | ] | ||
100 | main :: IO () | ||
101 | main = execParser opts >>= trivmix | ||
102 | where | ||
103 | opts = info (helper <*> optionParser) | ||
104 | ( fullDesc | ||
105 | <> progDesc "Setup a JACK mixing input/output pair controlled by files" | ||
106 | <> header "Trivmix - A trivial mixer" | ||
107 | ) | ||
108 | |||
109 | trivmix :: Options -> IO () | ||
110 | trivmix Options{..} = do | ||
111 | level <- newMVar initialLevel | ||
112 | let withFiles = foldl (.) id $ map (\f -> onStateFile f (show initialLevel ++ "\n")) levelFiles | ||
113 | withFiles $ withINotify $ \inotify -> do | ||
114 | handleFiles inotify level levelFiles | ||
115 | Jack.handleExceptions $ | ||
116 | Jack.withClientDefault client $ \client' -> | ||
117 | Jack.withPort client' input $ \input' -> | ||
118 | Jack.withPort client' output $ \output' -> do | ||
119 | Trans.lift $ do | ||
120 | case run of | ||
121 | Nothing -> return () | ||
122 | Just run' -> do | ||
123 | (_, _, _, ph) <- createProcess $ (proc run' [client ++ ":" ++ input, client ++ ":" ++ output]) { delegate_ctlc = True } | ||
124 | return () | ||
125 | Audio.withProcessMono client' input' (mix level) output' $ | ||
126 | Jack.withActivation client' $ Trans.lift Jack.waitForBreak | ||
127 | |||
128 | mix :: MVar Level -> CFloat -> IO CFloat | ||
129 | mix level input = do | ||
130 | level' <- readMVar level | ||
131 | return $ (CFloat $ toFloat level') * input | ||
132 | |||
133 | handleFiles :: INotify -> MVar Level -> [FilePath] -> IO () | ||
134 | handleFiles inotify level files = do | ||
135 | initLevel <- readMVar level | ||
136 | levelChanges <- (newChan :: IO (Chan Level)) | ||
137 | stderrLock <- newEmptyMVar | ||
138 | let | ||
139 | handleFile file = addWatch inotify watchedAttrs file (const $ readLevel levelChanges level file stderrLock) | ||
140 | mapM_ handleFile files | ||
141 | forkIO $ forever $ do -- Broadcast level changes and update all files | ||
142 | levelState <- readChan levelChanges | ||
143 | swapMVar level levelState | ||
144 | mapM_ (\f -> writeLevel f stderrLock levelState) files | ||
145 | return () | ||
146 | return () | ||
147 | |||
148 | onStateFile :: FilePath -> String -> IO a -> IO a | ||
149 | onStateFile file initial action = do | ||
150 | let directory = takeDirectory file | ||
151 | directories = iterate takeDirectory directory | ||
152 | createDirs <- takeWhileM (\d -> not <$> doesDirectoryExist d) directories | ||
153 | exists <- doesFileExist file | ||
154 | let acquireFile = case exists of | ||
155 | True -> return () | ||
156 | False -> do | ||
157 | hPutStrLn stderr $ "Creating ‘" ++ file ++ "’ (file)" | ||
158 | createFile file defFileMode >>= closeFd >> writeFile file initial | ||
159 | releaseFile = case exists of | ||
160 | True -> return () | ||
161 | False -> do | ||
162 | hPutStrLn stderr $ "Removing ‘" ++ file ++ "’ (file)" | ||
163 | removeFile file | ||
164 | acquireDir = (flip mapM) (reverse createDirs) $ \directory -> do | ||
165 | hPutStrLn stderr $ "Creating ‘" ++ directory ++ "’ (dir)" | ||
166 | createDirectory directory | ||
167 | setFileMode directory defDirectoryMode | ||
168 | releaseDir = (flip mapM) createDirs $ \directory -> do | ||
169 | hPutStrLn stderr $ "Removing ‘" ++ directory ++ "’ (dir)" | ||
170 | removeDirectory directory | ||
171 | acquire = acquireDir >> acquireFile | ||
172 | release = releaseFile >> releaseDir | ||
173 | bracket_ acquire release action | ||
174 | |||
175 | takeWhileM :: Monad m => (a -> m Bool) -> [a] -> m [a] | ||
176 | takeWhileM _ [] = return [] | ||
177 | takeWhileM pred (x:xs) = do | ||
178 | take <- pred x | ||
179 | case take of | ||
180 | True -> do | ||
181 | rest <- takeWhileM pred xs | ||
182 | return $ x:rest | ||
183 | False -> do | ||
184 | return [] | ||
185 | |||
186 | readLevel :: Chan Level -> MVar Level -> FilePath -> MVar () -> IO () | ||
187 | readLevel levelChan current file stderrLock = catch action handler | ||
188 | where | ||
189 | action = do | ||
190 | level <- withFileLock file Shared $ const $ readFile file >>= readIO . stripSpace | ||
191 | oldLevel <- readMVar current | ||
192 | when (oldLevel /= level) $ do | ||
193 | writeChan levelChan level | ||
194 | withMVarLock stderrLock $ | ||
195 | hPutStrLn stderr $ "Detected new level: " ++ (show level) | ||
196 | handler e = if isUserError e | ||
197 | then do | ||
198 | withMVarLock stderrLock $ | ||
199 | hPutStrLn stderr $ "Could not parse new level from ‘" ++ file ++ "’ - overwriting." | ||
200 | readMVar current >>= writeLevel file stderrLock | ||
201 | else throw e | ||
202 | stripSpace = reverse . stripSpace' . reverse . stripSpace' | ||
203 | stripSpace' [] = [] | ||
204 | stripSpace' l@(x:xs) = if isSpace x | ||
205 | then stripSpace' xs | ||
206 | else l | ||
207 | |||
208 | writeLevel :: FilePath -> MVar () -> Level -> IO () | ||
209 | writeLevel file stderrLock level = withFileLock file Exclusive $ const $ do | ||
210 | withMVarLock stderrLock $ | ||
211 | hPutStrLn stderr $ "Writing out level ‘" ++ (show level) ++ "’ to ‘" ++ file ++ "’" | ||
212 | writeFile file (show level ++ "\n") | ||
213 | |||
214 | withMVarLock :: MVar () -> IO a -> IO a | ||
215 | withMVarLock lock = bracket_ (putMVar lock ()) (takeMVar lock) | ||