diff options
author | Gregor Kleen <gkleen@yggdrasil.li> | 2018-05-15 12:50:42 +0200 |
---|---|---|
committer | Gregor Kleen <gkleen@yggdrasil.li> | 2018-05-15 12:50:42 +0200 |
commit | 86ce943d5a49982246ab83e4acc72ffb7c22567c (patch) | |
tree | 9a73ed3f112c302e5a1ed980c6505fa31fcf9287 | |
parent | 31a88f4dd0800caeeb56d785b1876a9c2b88fb93 (diff) | |
download | trivmix-86ce943d5a49982246ab83e4acc72ffb7c22567c.tar trivmix-86ce943d5a49982246ab83e4acc72ffb7c22567c.tar.gz trivmix-86ce943d5a49982246ab83e4acc72ffb7c22567c.tar.bz2 trivmix-86ce943d5a49982246ab83e4acc72ffb7c22567c.tar.xz trivmix-86ce943d5a49982246ab83e4acc72ffb7c22567c.zip |
Implement balance & refine types
-rw-r--r-- | src/Trivmix/Types.hs | 79 | ||||
-rw-r--r-- | trivmix.cabal | 5 | ||||
-rw-r--r-- | trivmix.nix | 17 | ||||
-rw-r--r-- | trivmix/Trivmix.hs | 54 |
4 files changed, 100 insertions, 55 deletions
diff --git a/src/Trivmix/Types.hs b/src/Trivmix/Types.hs index a6a41b9..f01e023 100644 --- a/src/Trivmix/Types.hs +++ b/src/Trivmix/Types.hs | |||
@@ -1,7 +1,8 @@ | |||
1 | {-# LANGUAGE FlexibleInstances, UndecidableInstances #-} | 1 | {-# LANGUAGE FlexibleInstances, UndecidableInstances, ViewPatterns, TemplateHaskell, PatternGuards #-} |
2 | 2 | ||
3 | module Trivmix.Types | 3 | module Trivmix.Types |
4 | ( Level | 4 | ( Level' |
5 | , Level(Lin), toLin | ||
5 | , toFloat | 6 | , toFloat |
6 | , asFloat | 7 | , asFloat |
7 | , Adjustment(..) | 8 | , Adjustment(..) |
@@ -13,27 +14,37 @@ import Data.Fixed | |||
13 | import Data.CaseInsensitive ( CI ) | 14 | import Data.CaseInsensitive ( CI ) |
14 | import qualified Data.CaseInsensitive as CI | 15 | import qualified Data.CaseInsensitive as CI |
15 | 16 | ||
17 | import Text.ParserCombinators.ReadPrec | ||
18 | import Control.Applicative | ||
19 | import Control.Monad | ||
20 | |||
16 | import Data.Default | 21 | import Data.Default |
17 | 22 | ||
18 | import Data.Function (on) | 23 | import Data.Function (on) |
19 | 24 | ||
20 | data Level = Lin Float | DB Float | 25 | import Refined |
21 | 26 | ||
22 | instance Num Level where | 27 | type Level' = Refined NonNegative Float |
23 | (+) = asFloat (+) | 28 | data Level = Lin { toLin :: Level' } | DB { toLin :: Level' } |
24 | (-) = asFloat (-) | ||
25 | (*) = asFloat (*) | ||
26 | abs = Lin . abs . toFloat | ||
27 | signum = Lin . signum . toFloat | ||
28 | fromInteger = Lin . fromInteger | ||
29 | 29 | ||
30 | asFloat :: (Float -> Float -> Float) -> Level -> Level -> Level | 30 | instance Num Level where |
31 | asFloat f (Lin x) (Lin y) = Lin $ f x y | 31 | (+) = fmap (either error id) . asFloat (+) |
32 | asFloat f x y = DB $ (f `on` toFloat) x y | 32 | (-) = fmap (either error id) . asFloat (-) |
33 | (*) = fmap (either error id) . asFloat (*) | ||
34 | abs = Lin . toLin | ||
35 | signum = Lin . either error id . refine . signum . toFloat | ||
36 | fromInteger = Lin . either error id . refine . fromInteger | ||
37 | |||
38 | asFloat :: (Float -> Float -> Float) -> Level -> Level -> Either String Level | ||
39 | asFloat ((`on` toFloat) -> f) x y = toLvl <$> refine (f x y) | ||
40 | where | ||
41 | toLvl | ||
42 | | DB _ <- x = DB | ||
43 | | DB _ <- y = DB | ||
44 | | otherwise = Lin | ||
33 | 45 | ||
34 | toFloat :: Level -> Float | 46 | toFloat :: Level -> Float |
35 | toFloat (Lin x) = x | 47 | toFloat = unrefine . toLin |
36 | toFloat (DB x) = x | ||
37 | 48 | ||
38 | withPrec :: (Num a, RealFrac a, HasResolution b) => a -> Fixed b | 49 | withPrec :: (Num a, RealFrac a, HasResolution b) => a -> Fixed b |
39 | withPrec x = withResolution $ \p' -> MkFixed $ toInteger $ round $ x * (fromInteger p') | 50 | withPrec x = withResolution $ \p' -> MkFixed $ toInteger $ round $ x * (fromInteger p') |
@@ -44,39 +55,33 @@ withType f = f undefined | |||
44 | withResolution :: (HasResolution a) => (Integer -> f a) -> f a | 55 | withResolution :: (HasResolution a) => (Integer -> f a) -> f a |
45 | withResolution f = withType (f . resolution) | 56 | withResolution f = withType (f . resolution) |
46 | 57 | ||
47 | linToDb :: Float -> Float | 58 | linToDb :: Level' -> Float |
48 | linToDb x = 20 * (logBase 10 x) | 59 | linToDb (unrefine -> x) = 20 * (logBase 10 x) |
49 | 60 | ||
50 | dBToLin :: Float -> Float | 61 | dBToLin :: Float -> Level' |
51 | dBToLin x = 10 ** (0.05 * x) | 62 | dBToLin x = either error id . refine $ 10 ** (0.05 * x) |
52 | 63 | ||
53 | instance Show Level where | 64 | instance Show Level where |
54 | show (Lin x) = show x | 65 | show (Lin x) = show x |
55 | show (DB x) = (show $ (withPrec x' :: Milli)) ++ "dB" | 66 | show (DB (linToDb -> x)) = (show $ (withPrec x :: Milli)) ++ "dB" |
56 | where | ||
57 | x' = linToDb x | ||
58 | 67 | ||
59 | instance Read Level where | 68 | instance Read Level where |
60 | readsPrec i = map toL . readsPrec i | 69 | readsPrec = readPrec_to_S $ parseDb <|> parseLin |
61 | where | 70 | where |
62 | toL :: (Float, String) -> (Level, String) | 71 | parseDb = do |
63 | toL (f, str) | 72 | db <- readS_to_Prec readsPrec |
64 | | ((==) `on` CI.mk) prec unit = (DB $ dBToLin f, rest) | 73 | let |
65 | | otherwise = (Lin f, str) | 74 | unit@(length -> lU) = "dB" |
66 | where | 75 | unit' <- forM [1..lU] $ const get |
67 | prec = take lU str | 76 | guard $ ((==) `on` CI.mk) unit unit' |
68 | rest = drop lU str | 77 | return . DB $ dBToLin db |
69 | unit = "dB" | 78 | parseLin = Lin <$> readS_to_Prec readsPrec |
70 | lU = length unit | ||
71 | 79 | ||
72 | instance Eq Level where | 80 | instance Eq Level where |
73 | (Lin a) == (Lin b) = a == b | 81 | (==) = (==) `on` toLin |
74 | (Lin a) == (DB b) = a == b | ||
75 | (DB a) == (Lin b) = a == b | ||
76 | (DB a) == (DB b) = a == b | ||
77 | 82 | ||
78 | instance Default Level where | 83 | instance Default Level where |
79 | def = Lin 0 | 84 | def = Lin $$(refineTH 0) |
80 | 85 | ||
81 | data Adjustment a = Set a | 86 | data Adjustment a = Set a |
82 | | Add a | 87 | | Add a |
diff --git a/trivmix.cabal b/trivmix.cabal index 1a2ef21..d075871 100644 --- a/trivmix.cabal +++ b/trivmix.cabal | |||
@@ -2,7 +2,7 @@ | |||
2 | -- documentation, see http://haskell.org/cabal/users-guide/ | 2 | -- documentation, see http://haskell.org/cabal/users-guide/ |
3 | 3 | ||
4 | name: trivmix | 4 | name: trivmix |
5 | version: 2.7.6 | 5 | version: 3.0.0 |
6 | -- synopsis: | 6 | -- synopsis: |
7 | -- description: | 7 | -- description: |
8 | license: PublicDomain | 8 | license: PublicDomain |
@@ -22,6 +22,7 @@ library | |||
22 | build-depends: base >=4.8 && <5 | 22 | build-depends: base >=4.8 && <5 |
23 | , data-default >=0.5 && <1 | 23 | , data-default >=0.5 && <1 |
24 | , case-insensitive >=1.2 && <2 | 24 | , case-insensitive >=1.2 && <2 |
25 | , refined >=0.1.2.1 && <1 | ||
25 | 26 | ||
26 | executable trivmix | 27 | executable trivmix |
27 | main-is: Trivmix.hs | 28 | main-is: Trivmix.hs |
@@ -39,6 +40,8 @@ executable trivmix | |||
39 | , process >=1.2 && <2 | 40 | , process >=1.2 && <2 |
40 | , filelock >=0.1 && <1 | 41 | , filelock >=0.1 && <1 |
41 | , systemd >=1.1.2 && <2 | 42 | , systemd >=1.1.2 && <2 |
43 | , heredoc >=0.2.0.0 && <1 | ||
44 | , refined >=0.1.2.1 && <1 | ||
42 | , trivmix | 45 | , trivmix |
43 | hs-source-dirs: trivmix | 46 | hs-source-dirs: trivmix |
44 | default-language: Haskell2010 | 47 | default-language: Haskell2010 |
diff --git a/trivmix.nix b/trivmix.nix index dcc06c0..a823793 100644 --- a/trivmix.nix +++ b/trivmix.nix | |||
@@ -1,18 +1,21 @@ | |||
1 | { mkDerivation, base, case-insensitive, data-default, directory | 1 | { mkDerivation, base, case-insensitive, data-default, directory |
2 | , explicit-exception, filelock, filepath, hinotify, jack | 2 | , explicit-exception, filelock, filepath, heredoc, hinotify, jack |
3 | , optparse-applicative, process, stdenv, systemd, transformers | 3 | , optparse-applicative, process, refined, stdenv, systemd |
4 | , unix | 4 | , transformers, unix |
5 | }: | 5 | }: |
6 | mkDerivation { | 6 | mkDerivation { |
7 | pname = "trivmix"; | 7 | pname = "trivmix"; |
8 | version = "2.7.6"; | 8 | version = "3.0.0"; |
9 | src = ./.; | 9 | src = ./.; |
10 | isLibrary = true; | 10 | isLibrary = true; |
11 | isExecutable = true; | 11 | isExecutable = true; |
12 | libraryHaskellDepends = [ base case-insensitive data-default ]; | 12 | libraryHaskellDepends = [ |
13 | base case-insensitive data-default refined | ||
14 | ]; | ||
13 | executableHaskellDepends = [ | 15 | executableHaskellDepends = [ |
14 | base directory explicit-exception filelock filepath hinotify jack | 16 | base directory explicit-exception filelock filepath heredoc |
15 | optparse-applicative process systemd transformers unix | 17 | hinotify jack optparse-applicative process refined systemd |
18 | transformers unix | ||
16 | ]; | 19 | ]; |
17 | license = stdenv.lib.licenses.publicDomain; | 20 | license = stdenv.lib.licenses.publicDomain; |
18 | } | 21 | } |
diff --git a/trivmix/Trivmix.hs b/trivmix/Trivmix.hs index 084da7f..4d7c6f7 100644 --- a/trivmix/Trivmix.hs +++ b/trivmix/Trivmix.hs | |||
@@ -1,10 +1,12 @@ | |||
1 | {-# LANGUAGE RecordWildCards #-} | 1 | {-# LANGUAGE RecordWildCards #-} |
2 | {-# LANGUAGE ViewPatterns #-} | ||
3 | {-# LANGUAGE TemplateHaskell, QuasiQuotes #-} | ||
2 | 4 | ||
3 | import Foreign.C.Types (CFloat(..)) | 5 | import Foreign.C.Types (CFloat(..)) |
4 | import qualified Sound.JACK as Jack | 6 | import qualified Sound.JACK as Jack |
5 | import qualified Sound.JACK.Audio as Audio | 7 | import qualified Sound.JACK.Audio as Audio |
6 | 8 | ||
7 | import Options.Applicative | 9 | import Options.Applicative hiding (str) |
8 | 10 | ||
9 | import Data.Maybe | 11 | import Data.Maybe |
10 | 12 | ||
@@ -16,7 +18,7 @@ import System.Posix.Types | |||
16 | import System.Environment | 18 | import System.Environment |
17 | import System.Process | 19 | import System.Process |
18 | 20 | ||
19 | import System.Systemd.Daemon (notifyReady) | 21 | import System.Systemd.Daemon (notifyReady, notifyWatchdog) |
20 | 22 | ||
21 | import Control.Concurrent | 23 | import Control.Concurrent |
22 | import Control.Concurrent.MVar | 24 | import Control.Concurrent.MVar |
@@ -38,15 +40,22 @@ import Data.Char | |||
38 | import Data.Function | 40 | import Data.Function |
39 | 41 | ||
40 | import Control.Monad | 42 | import Control.Monad |
43 | |||
44 | import Text.Heredoc (str) | ||
45 | import Refined | ||
41 | 46 | ||
42 | import Trivmix.Types | 47 | import Trivmix.Types |
43 | 48 | ||
49 | type Balance = Refined ZeroToOne Float | ||
50 | |||
44 | data Options = Options | 51 | data Options = Options |
45 | { input :: String | 52 | { input :: String |
46 | , output :: String | 53 | , output :: String |
47 | , client :: String | 54 | , client :: String |
48 | , initialLevel :: Level | 55 | , initialLevel :: Level |
56 | , initialBalance :: Balance | ||
49 | , run :: [FilePath] | 57 | , run :: [FilePath] |
58 | , balanceFiles :: [FilePath] | ||
50 | , levelFiles :: [FilePath] | 59 | , levelFiles :: [FilePath] |
51 | } | 60 | } |
52 | 61 | ||
@@ -74,13 +83,33 @@ optionParser = Options | |||
74 | <> value def | 83 | <> value def |
75 | <> showDefault | 84 | <> showDefault |
76 | ) | 85 | ) |
86 | <*> option auto ( long "balance" | ||
87 | <> metavar "BALANCE" | ||
88 | <> help "Initial value for balance" | ||
89 | <> value ($$(refineTH 1.0) :: Balance) | ||
90 | <> showDefault | ||
91 | ) | ||
77 | <*> many ( strOption ( long "run" | 92 | <*> many ( strOption ( long "run" |
78 | <> metavar "FILE" | 93 | <> metavar "FILE" |
79 | <> help "Execute a file once setup of jacks is done (use this to autoconnect)\nThe executable gets passed the input port (including client name) as its first argument and the output as its second." | 94 | <> help [str|Execute a file once setup of jacks is done (use this to autoconnect) |
95 | |The executable gets passed the input port (including client name) as its first argument and the output as its second. | ||
96 | |] | ||
80 | ) | 97 | ) |
81 | ) | 98 | ) |
99 | <*> many ( strOption ( long "balance" | ||
100 | <> metavar "FILE" | ||
101 | <> help [str|Files that contain factors in the interval [0,1] to multiply with each other and the current level. | ||
102 | |For deterministic behaviour use flock(2). | ||
103 | |The format used in these files is a float using ‘.’ as a decimal point. | ||
104 | |] | ||
105 | ) | ||
106 | ) | ||
82 | <*> many (strArgument ( metavar "FILE..." | 107 | <*> many (strArgument ( metavar "FILE..." |
83 | <> help "Files that contain levels to assume and synchronize\nFor deterministic behaviour use flock(2).\nThe format used in these files is either a signed float, using ‘.’ as a decimal point or a signed float postfixed with ‘dB’.\nCaveat: ‘-InfinitydB’ exists and works as expected (i.e.: it is equal to ‘0.0’)" | 108 | <> help [str|Files that contain levels to assume and synchronize |
109 | |For deterministic behaviour use flock(2). | ||
110 | |The format used in these files is either a signed float, using ‘.’ as a decimal point or a signed float postfixed with ‘dB’. | ||
111 | |Caveat: ‘-InfinitydB’ exists and works as expected (i.e.: it is equal to ‘0.0’) | ||
112 | |] | ||
84 | ) | 113 | ) |
85 | ) | 114 | ) |
86 | 115 | ||
@@ -122,6 +151,7 @@ main = execParser opts >>= trivmix | |||
122 | trivmix :: Options -> IO () | 151 | trivmix :: Options -> IO () |
123 | trivmix Options{..} = do | 152 | trivmix Options{..} = do |
124 | level <- newMVar initialLevel | 153 | level <- newMVar initialLevel |
154 | balance <- newMVar initialBalance | ||
125 | level' <- newMVar initialLevel | 155 | level' <- newMVar initialLevel |
126 | forkIO $ forever $ do -- Smooth out discontinuity | 156 | forkIO $ forever $ do -- Smooth out discontinuity |
127 | let | 157 | let |
@@ -130,12 +160,15 @@ trivmix Options{..} = do | |||
130 | frames = interval * fps | 160 | frames = interval * fps |
131 | delay = round $ recip fps * 1e6 | 161 | delay = round $ recip fps * 1e6 |
132 | linInt x a b = a * (1 - x) + b * x | 162 | linInt x a b = a * (1 - x) + b * x |
133 | newLevel <- readMVar level | 163 | linInt' x a b = either error id $ asFloat (linInt x) a b |
164 | mulBalance (unrefine -> b) x = either error id $ asFloat (*) (Lin . either error id $ refine b) x | ||
165 | newLevel <- mulBalance <$> readMVar balance <*> readMVar level | ||
134 | currentLevel <- readMVar level' | 166 | currentLevel <- readMVar level' |
135 | mapM_ (\x -> swapMVar level' (asFloat (linInt x) currentLevel newLevel) >> threadDelay delay) ([0,recip frames..1] :: [Float]) | 167 | mapM_ (\x -> swapMVar level' (linInt' x currentLevel newLevel) >> threadDelay delay) ([0,recip frames..1] :: [Float]) |
136 | let withFiles = foldl (.) id $ map (\f -> onStateFile f (show initialLevel ++ "\n")) levelFiles | 168 | let withFiles = foldl (.) id $ map (\f -> onStateFile f (show initialLevel ++ "\n")) levelFiles |
137 | withFiles $ withINotify $ \inotify -> do | 169 | withFiles $ withINotify $ \inotify -> do |
138 | handleFiles inotify level levelFiles | 170 | handleFiles inotify level levelFiles |
171 | handleFiles inotify balance levelFiles | ||
139 | Jack.handleExceptions $ | 172 | Jack.handleExceptions $ |
140 | Jack.withClientDefault client $ \client' -> | 173 | Jack.withClientDefault client $ \client' -> |
141 | Jack.withPort client' input $ \input' -> | 174 | Jack.withPort client' input $ \input' -> |
@@ -149,13 +182,14 @@ trivmix Options{..} = do | |||
149 | 182 | ||
150 | mix :: MVar Level -> CFloat -> IO CFloat | 183 | mix :: MVar Level -> CFloat -> IO CFloat |
151 | mix level input = do | 184 | mix level input = do |
185 | notifyWatchdog | ||
152 | level' <- readMVar level | 186 | level' <- readMVar level |
153 | return $ (CFloat $ toFloat level') * input | 187 | return $ (CFloat $ toFloat level') * input |
154 | 188 | ||
155 | handleFiles :: INotify -> MVar Level -> [FilePath] -> IO () | 189 | handleFiles :: (Read l, Show l, Eq l) => INotify -> MVar l -> [FilePath] -> IO () |
156 | handleFiles inotify level files = do | 190 | handleFiles inotify level files = do |
157 | initLevel <- readMVar level | 191 | initLevel <- readMVar level |
158 | levelChanges <- (newChan :: IO (Chan Level)) | 192 | levelChanges <- newChan |
159 | stderrLock <- newEmptyMVar | 193 | stderrLock <- newEmptyMVar |
160 | let | 194 | let |
161 | handleFile file = addWatch inotify watchedAttrs file (const $ readLevel levelChanges level file stderrLock) | 195 | handleFile file = addWatch inotify watchedAttrs file (const $ readLevel levelChanges level file stderrLock) |
@@ -206,7 +240,7 @@ takeWhileM pred (x:xs) = do | |||
206 | False -> do | 240 | False -> do |
207 | return [] | 241 | return [] |
208 | 242 | ||
209 | readLevel :: Chan Level -> MVar Level -> FilePath -> MVar () -> IO () | 243 | readLevel :: (Read l, Show l, Eq l) => Chan l -> MVar l -> FilePath -> MVar () -> IO () |
210 | readLevel levelChan current file stderrLock = catch action handler | 244 | readLevel levelChan current file stderrLock = catch action handler |
211 | where | 245 | where |
212 | action = do | 246 | action = do |
@@ -228,7 +262,7 @@ readLevel levelChan current file stderrLock = catch action handler | |||
228 | then stripSpace' xs | 262 | then stripSpace' xs |
229 | else l | 263 | else l |
230 | 264 | ||
231 | writeLevel :: FilePath -> MVar () -> Level -> IO () | 265 | writeLevel :: Show l => FilePath -> MVar () -> l -> IO () |
232 | writeLevel file stderrLock level = withFileLock file Exclusive $ const $ do | 266 | writeLevel file stderrLock level = withFileLock file Exclusive $ const $ do |
233 | withMVarLock stderrLock $ | 267 | withMVarLock stderrLock $ |
234 | hPutStrLn stderr $ "Writing out level ‘" ++ (show level) ++ "’ to ‘" ++ file ++ "’" | 268 | hPutStrLn stderr $ "Writing out level ‘" ++ (show level) ++ "’ to ‘" ++ file ++ "’" |