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 /src | |
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 'src')
-rw-r--r-- | src/Trivmix.hs | 257 | ||||
-rw-r--r-- | src/Trivmix/Types.hs | 90 |
2 files changed, 90 insertions, 257 deletions
diff --git a/src/Trivmix.hs b/src/Trivmix.hs deleted file mode 100644 index 37ecec6..0000000 --- a/src/Trivmix.hs +++ /dev/null | |||
@@ -1,257 +0,0 @@ | |||
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 Data.Fixed | ||
39 | |||
40 | import Data.CaseInsensitive ( CI ) | ||
41 | import qualified Data.CaseInsensitive as CI | ||
42 | |||
43 | data Options = Options | ||
44 | { input :: String | ||
45 | , output :: String | ||
46 | , client :: String | ||
47 | , run :: Maybe String | ||
48 | , levelFiles :: [FilePath] | ||
49 | } | ||
50 | |||
51 | data Level = Lin Float | DB Float | ||
52 | |||
53 | withPrec :: (Num a, RealFrac a, HasResolution b) => a -> Fixed b | ||
54 | withPrec x = withResolution $ \p' -> MkFixed $ toInteger $ round $ x * (fromInteger p') | ||
55 | |||
56 | withType :: (p a -> f a) -> f a | ||
57 | withType f = f undefined | ||
58 | |||
59 | withResolution :: (HasResolution a) => (Integer -> f a) -> f a | ||
60 | withResolution f = withType (f . resolution) | ||
61 | |||
62 | instance Show Level where | ||
63 | show (Lin x) = show x | ||
64 | show (DB x) = (show $ (withPrec x' :: Milli)) ++ "dB" | ||
65 | where | ||
66 | x' = 20 * (logBase 10 x) | ||
67 | |||
68 | instance Read Level where | ||
69 | readsPrec i = map toL . readsPrec i | ||
70 | where | ||
71 | toL :: (Float, String) -> (Level, String) | ||
72 | toL (f, str) | ||
73 | | ((==) `on` CI.mk) prec unit = (DB $ 10 ** (0.05 * f), rest) | ||
74 | | otherwise = (Lin f, str) | ||
75 | where | ||
76 | prec = take lU str | ||
77 | rest = drop lU str | ||
78 | unit = "dB" | ||
79 | lU = length unit | ||
80 | |||
81 | instance Eq Level where | ||
82 | (Lin a) == (Lin b) = a == b | ||
83 | (Lin a) == (DB b) = a == b | ||
84 | (DB a) == (Lin b) = a == b | ||
85 | (DB a) == (DB b) = a == b | ||
86 | |||
87 | optionParser :: Parser Options | ||
88 | optionParser = Options <$> | ||
89 | (fromMaybe "in" <$> optional (strOption ( long "input" | ||
90 | <> metavar "STRING" | ||
91 | ) | ||
92 | ) | ||
93 | ) | ||
94 | <*> (fromMaybe "out" <$> optional (strOption ( long "output" | ||
95 | <> metavar "STRING" | ||
96 | ) | ||
97 | ) | ||
98 | ) | ||
99 | <*> strOption ( long "client" | ||
100 | <> metavar "STRING" | ||
101 | ) | ||
102 | <*> optional ( strOption ( long "run" | ||
103 | <> metavar "FILE" | ||
104 | ) | ||
105 | ) | ||
106 | <*> some (strArgument ( metavar "FILE..." | ||
107 | <> help "Files that contain levels to assume and synchronize" | ||
108 | ) | ||
109 | ) | ||
110 | |||
111 | watchedAttrs :: [EventVariety] | ||
112 | watchedAttrs = [ Modify | ||
113 | , Move | ||
114 | , MoveIn | ||
115 | , MoveOut | ||
116 | , MoveSelf | ||
117 | , Create | ||
118 | , Delete | ||
119 | , DeleteSelf | ||
120 | ] | ||
121 | |||
122 | initialLevel :: Level | ||
123 | initialLevel = Lin 0 | ||
124 | |||
125 | defFileMode :: FileMode | ||
126 | defFileMode = foldl unionFileModes nullFileMode [ ownerReadMode | ||
127 | , ownerWriteMode | ||
128 | , groupReadMode | ||
129 | , groupWriteMode | ||
130 | , otherReadMode | ||
131 | ] | ||
132 | |||
133 | defDirectoryMode :: FileMode | ||
134 | defDirectoryMode = foldl unionFileModes nullFileMode [ ownerModes | ||
135 | , groupModes | ||
136 | , otherReadMode | ||
137 | , otherExecuteMode | ||
138 | ] | ||
139 | main :: IO () | ||
140 | main = execParser opts >>= trivmix | ||
141 | where | ||
142 | opts = info (helper <*> optionParser) | ||
143 | ( fullDesc | ||
144 | <> progDesc "Setup a JACK mixing input/output pair controlled by files" | ||
145 | <> header "Trivmix - A trivial mixer" | ||
146 | ) | ||
147 | |||
148 | trivmix :: Options -> IO () | ||
149 | trivmix Options{..} = do | ||
150 | level <- newMVar initialLevel | ||
151 | let withFiles = foldl (.) id $ map (\f -> onStateFile f (show initialLevel ++ "\n")) levelFiles | ||
152 | withFiles $ withINotify $ \inotify -> do | ||
153 | handleFiles inotify level levelFiles | ||
154 | Jack.handleExceptions $ | ||
155 | Jack.withClientDefault client $ \client' -> | ||
156 | Jack.withPort client' input $ \input' -> | ||
157 | Jack.withPort client' output $ \output' -> do | ||
158 | Trans.lift $ do | ||
159 | case run of | ||
160 | Nothing -> return () | ||
161 | Just run' -> do | ||
162 | (_, _, _, ph) <- createProcess $ (proc run' [client ++ ":" ++ input, client ++ ":" ++ output]) { delegate_ctlc = True } | ||
163 | return () | ||
164 | Audio.withProcessMono client' input' (mix level) output' $ | ||
165 | Jack.withActivation client' $ Trans.lift Jack.waitForBreak | ||
166 | |||
167 | mix :: MVar Level -> CFloat -> IO CFloat | ||
168 | mix level input = do | ||
169 | level' <- readMVar level | ||
170 | return $ (CFloat $ toFloat level') * input | ||
171 | where | ||
172 | toFloat (Lin x) = x | ||
173 | toFloat (DB x) = x | ||
174 | |||
175 | handleFiles :: INotify -> MVar Level -> [FilePath] -> IO () | ||
176 | handleFiles inotify level files = do | ||
177 | initLevel <- readMVar level | ||
178 | levelChanges <- (newChan :: IO (Chan Level)) | ||
179 | stderrLock <- newEmptyMVar | ||
180 | let | ||
181 | handleFile file = addWatch inotify watchedAttrs file (const $ readLevel levelChanges level file stderrLock) | ||
182 | mapM_ handleFile files | ||
183 | forkIO $ forever $ do -- Broadcast level changes and update all files | ||
184 | levelState <- readChan levelChanges | ||
185 | swapMVar level levelState | ||
186 | mapM_ (\f -> writeLevel f stderrLock levelState) files | ||
187 | return () | ||
188 | return () | ||
189 | |||
190 | onStateFile :: FilePath -> String -> IO a -> IO a | ||
191 | onStateFile file initial action = do | ||
192 | let directory = takeDirectory file | ||
193 | directories = iterate takeDirectory directory | ||
194 | createDirs <- takeWhileM (\d -> not <$> doesDirectoryExist d) directories | ||
195 | exists <- doesFileExist file | ||
196 | let acquireFile = case exists of | ||
197 | True -> return () | ||
198 | False -> do | ||
199 | hPutStrLn stderr $ "Creating ‘" ++ file ++ "’ (file)" | ||
200 | createFile file defFileMode >>= closeFd >> writeFile file initial | ||
201 | releaseFile = case exists of | ||
202 | True -> return () | ||
203 | False -> do | ||
204 | hPutStrLn stderr $ "Removing ‘" ++ file ++ "’ (file)" | ||
205 | removeFile file | ||
206 | acquireDir = (flip mapM) (reverse createDirs) $ \directory -> do | ||
207 | hPutStrLn stderr $ "Creating ‘" ++ directory ++ "’ (dir)" | ||
208 | createDirectory directory | ||
209 | setFileMode directory defDirectoryMode | ||
210 | releaseDir = (flip mapM) createDirs $ \directory -> do | ||
211 | hPutStrLn stderr $ "Removing ‘" ++ directory ++ "’ (dir)" | ||
212 | removeDirectory directory | ||
213 | acquire = acquireDir >> acquireFile | ||
214 | release = releaseFile >> releaseDir | ||
215 | bracket_ acquire release action | ||
216 | |||
217 | takeWhileM :: Monad m => (a -> m Bool) -> [a] -> m [a] | ||
218 | takeWhileM _ [] = return [] | ||
219 | takeWhileM pred (x:xs) = do | ||
220 | take <- pred x | ||
221 | case take of | ||
222 | True -> do | ||
223 | rest <- takeWhileM pred xs | ||
224 | return $ x:rest | ||
225 | False -> do | ||
226 | return [] | ||
227 | |||
228 | readLevel :: Chan Level -> MVar Level -> FilePath -> MVar () -> IO () | ||
229 | readLevel levelChan current file stderrLock = catch action handler | ||
230 | where | ||
231 | action = do | ||
232 | level <- withFileLock file Shared $ const $ readFile file >>= readIO . stripSpace | ||
233 | oldLevel <- readMVar current | ||
234 | when (oldLevel /= level) $ do | ||
235 | writeChan levelChan level | ||
236 | withMVarLock stderrLock $ | ||
237 | hPutStrLn stderr $ "Detected new level: " ++ (show level) | ||
238 | handler e = if isUserError e | ||
239 | then do | ||
240 | withMVarLock stderrLock $ | ||
241 | hPutStrLn stderr $ "Could not parse new level from ‘" ++ file ++ "’ - overwriting." | ||
242 | readMVar current >>= writeLevel file stderrLock | ||
243 | else throw e | ||
244 | stripSpace = reverse . stripSpace' . reverse . stripSpace' | ||
245 | stripSpace' [] = [] | ||
246 | stripSpace' l@(x:xs) = if isSpace x | ||
247 | then stripSpace' xs | ||
248 | else l | ||
249 | |||
250 | writeLevel :: FilePath -> MVar () -> Level -> IO () | ||
251 | writeLevel file stderrLock level = withFileLock file Exclusive $ const $ do | ||
252 | withMVarLock stderrLock $ | ||
253 | hPutStrLn stderr $ "Writing out level ‘" ++ (show level) ++ "’ to ‘" ++ file ++ "’" | ||
254 | writeFile file (show level ++ "\n") | ||
255 | |||
256 | withMVarLock :: MVar () -> IO a -> IO a | ||
257 | withMVarLock lock = bracket_ (putMVar lock ()) (takeMVar lock) | ||
diff --git a/src/Trivmix/Types.hs b/src/Trivmix/Types.hs new file mode 100644 index 0000000..66accdf --- /dev/null +++ b/src/Trivmix/Types.hs | |||
@@ -0,0 +1,90 @@ | |||
1 | {-# LANGUAGE FlexibleInstances, UndecidableInstances #-} | ||
2 | |||
3 | module Trivmix.Types | ||
4 | ( Level | ||
5 | , toFloat | ||
6 | , Adjustment(..) | ||
7 | , doAdjustment | ||
8 | , module Data.Default | ||
9 | ) where | ||
10 | |||
11 | import Data.Fixed | ||
12 | import Data.CaseInsensitive ( CI ) | ||
13 | import qualified Data.CaseInsensitive as CI | ||
14 | |||
15 | import Data.Default | ||
16 | |||
17 | import Data.Function (on) | ||
18 | |||
19 | data Level = Lin Float | DB Float | ||
20 | |||
21 | instance Num Level where | ||
22 | (+) = asFloat (+) | ||
23 | (-) = asFloat (-) | ||
24 | (*) = asFloat (*) | ||
25 | abs = Lin . abs . toFloat | ||
26 | signum = Lin . signum . toFloat | ||
27 | fromInteger = Lin . fromInteger | ||
28 | |||
29 | asFloat :: (Float -> Float -> Float) -> Level -> Level -> Level | ||
30 | asFloat f (Lin x) (Lin y) = Lin $ f x y | ||
31 | asFloat f x y = DB $ (f `on` toFloat) x y | ||
32 | |||
33 | toFloat :: Level -> Float | ||
34 | toFloat (Lin x) = x | ||
35 | toFloat (DB x) = x | ||
36 | |||
37 | withPrec :: (Num a, RealFrac a, HasResolution b) => a -> Fixed b | ||
38 | withPrec x = withResolution $ \p' -> MkFixed $ toInteger $ round $ x * (fromInteger p') | ||
39 | |||
40 | withType :: (p a -> f a) -> f a | ||
41 | withType f = f undefined | ||
42 | |||
43 | withResolution :: (HasResolution a) => (Integer -> f a) -> f a | ||
44 | withResolution f = withType (f . resolution) | ||
45 | |||
46 | instance Show Level where | ||
47 | show (Lin x) = show x | ||
48 | show (DB x) = (show $ (withPrec x' :: Milli)) ++ "dB" | ||
49 | where | ||
50 | x' = 20 * (logBase 10 x) | ||
51 | |||
52 | instance Read Level where | ||
53 | readsPrec i = map toL . readsPrec i | ||
54 | where | ||
55 | toL :: (Float, String) -> (Level, String) | ||
56 | toL (f, str) | ||
57 | | ((==) `on` CI.mk) prec unit = (DB $ 10 ** (0.05 * f), rest) | ||
58 | | otherwise = (Lin f, str) | ||
59 | where | ||
60 | prec = take lU str | ||
61 | rest = drop lU str | ||
62 | unit = "dB" | ||
63 | lU = length unit | ||
64 | |||
65 | instance Eq Level where | ||
66 | (Lin a) == (Lin b) = a == b | ||
67 | (Lin a) == (DB b) = a == b | ||
68 | (DB a) == (Lin b) = a == b | ||
69 | (DB a) == (DB b) = a == b | ||
70 | |||
71 | instance Default Level where | ||
72 | def = Lin 0 | ||
73 | |||
74 | data Adjustment a = Set a | ||
75 | | Add a | ||
76 | | Sub a | ||
77 | deriving (Show, Eq) | ||
78 | |||
79 | class Adjustable a where | ||
80 | add :: a -> a -> a | ||
81 | sub :: a -> a -> a | ||
82 | |||
83 | instance Num a => Adjustable a where | ||
84 | add = (+) | ||
85 | sub = (-) | ||
86 | |||
87 | doAdjustment :: Adjustable a => a -> Adjustment a -> a | ||
88 | doAdjustment _ (Set y) = y | ||
89 | doAdjustment x (Add y) = add x y | ||
90 | doAdjustment x (Sub y) = sub x y | ||