From bd7874ef606ae78bb8b626bd01906481feb784d6 Mon Sep 17 00:00:00 2001 From: Gregor Kleen Date: Sun, 7 Jun 2015 19:33:45 +0200 Subject: Rewrite in haskell --- .gitignore | 1 + LICENSE | 24 +++++ Makefile | 8 -- Setup.hs | 2 + default.nix | 7 ++ src/Trivmix.hs | 103 ++++++++++++++++++++ trivmix.c | 296 --------------------------------------------------------- trivmix.cabal | 35 +++++++ trivmix.h | 21 ---- trivmix.nix | 20 ++++ 10 files changed, 192 insertions(+), 325 deletions(-) create mode 100644 .gitignore create mode 100644 LICENSE delete mode 100644 Makefile create mode 100644 Setup.hs create mode 100644 default.nix create mode 100644 src/Trivmix.hs delete mode 100644 trivmix.c create mode 100644 trivmix.cabal delete mode 100644 trivmix.h create mode 100644 trivmix.nix diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e2f5dd2 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +result \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..68a49da --- /dev/null +++ b/LICENSE @@ -0,0 +1,24 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to diff --git a/Makefile b/Makefile deleted file mode 100644 index 91f8d09..0000000 --- a/Makefile +++ /dev/null @@ -1,8 +0,0 @@ -.PHONY: all test - -all: trivmix -test: all - valgrind ./trivmix testDir - -trivmix: trivmix.c - gcc -lm -Wall `pkg-config --cflags --libs jack` -o trivmix trivmix.c diff --git a/Setup.hs b/Setup.hs new file mode 100644 index 0000000..9a994af --- /dev/null +++ b/Setup.hs @@ -0,0 +1,2 @@ +import Distribution.Simple +main = defaultMain diff --git a/default.nix b/default.nix new file mode 100644 index 0000000..2c2a8cc --- /dev/null +++ b/default.nix @@ -0,0 +1,7 @@ +let + pkgs = import {}; +in rec { + trivmix = pkgs.stdenv.lib.overrideDerivation (pkgs.myHaskellPackages.callPackage ./trivmix.nix {}) (attrs : { + src = ./.; + }); +} diff --git a/src/Trivmix.hs b/src/Trivmix.hs new file mode 100644 index 0000000..019ee32 --- /dev/null +++ b/src/Trivmix.hs @@ -0,0 +1,103 @@ +{-# LANGUAGE RecordWildCards #-} + +import Foreign.C.Types (CFloat(..)) +import qualified Sound.JACK as Jack +import qualified Sound.JACK.Audio as Audio + +import Options.Applicative + +import Data.Maybe + +import System.Directory +import System.FilePath +import System.Posix.Files +import System.Posix.IO +import System.Environment + +import Control.Concurrent +import Control.Concurrent.MVar + +import qualified Control.Monad.Trans.Class as Trans + +import Control.Exception +import System.IO.Error + +import System.INotify + +data Options = Options + { input :: String + , output :: String + , initialLevel :: Float + , stateDir :: FilePath + } + +optionParser :: Parser Options +optionParser = Options <$> + strOption ( long "input" + <> metavar "JACK" + ) + <*> strOption ( long "output" + <> metavar "JACK" + ) + <*> (fromMaybe 0 <$> optional (option auto ( long "level" + <> metavar "FLOAT" + ) + ) + ) + <*> strOption ( long "dir" + <> metavar "DIRECTORY" + ) + +main :: IO () +main = execParser opts >>= trivmix + where + opts = info (helper <*> optionParser) + ( fullDesc + <> progDesc "Setup a JACK mixing input/output pair controlled by fifos in a state directory" + <> header "Trivmix - A trivial mixer" + ) + +trivmix :: Options -> IO () +trivmix Options{..} = do + name <- getProgName + createDirectoryIfMissing True stateDir + level <- newMVar initialLevel + let levelFile = stateDir "level" + onLevelFile levelFile initialLevel $ withINotify $ \n -> do + addWatch n [Modify] levelFile (const $ handleLevel level levelFile) + Jack.handleExceptions $ + Jack.withClientDefault name $ \client -> + Jack.withPort client input $ \input' -> + Jack.withPort client output $ \output' -> + Audio.withProcessMono client input' (mix level) output' $ + Jack.withActivation client $ Trans.lift $ do + Jack.waitForBreak + +mix :: MVar Float -> CFloat -> IO CFloat +mix level input = do + level' <- readMVar level + return $ (CFloat level') * input + +onLevelFile :: FilePath -> Float -> IO a -> IO a +onLevelFile file initial action = do + exists <- doesFileExist file + let acquire = case exists of + True -> return () + False -> createFile file mode >>= closeFd + mode = foldl unionFileModes nullFileMode [ ownerReadMode + , ownerWriteMode + , groupReadMode + , groupWriteMode + ] + release = case exists of + True -> return () + False -> removeFile file + bracket_ acquire release action + +handleLevel :: MVar Float -> FilePath -> IO () +handleLevel level file = catch action handler + where + action = readFile file >>= readIO >>= swapMVar level >>= const (return ()) + handler e = if isUserError e + then readMVar level >>= \l -> writeFile file (show l) + else throw e diff --git a/trivmix.c b/trivmix.c deleted file mode 100644 index 720c409..0000000 --- a/trivmix.c +++ /dev/null @@ -1,296 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "trivmix.h" - -#define PIDFILE "pid" -#define INPUTFILE "input" -#define OUTPUTFILE "output" -#define NAMEFILE "name" -#define GAINFILE "gain" -#define DIRMODE S_IRWXU | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH -#define FILEMODE S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH -#define INOTIFYBUFLEN 1024 * (sizeof(struct inotify_event) + 16) -#define NOTIFYMASK IN_MODIFY | IN_CREATE | IN_DELETE | IN_ONESHOT - -mixState state = { .input = NULL, .output = NULL, .name = NULL, .dBGain = -90 }; -char *workdir; -char *inotifyBuffer; -bool changedDir = false; - -void initState() -{ - const char *defaultName = "mixer"; - - state.input = malloc(sizeof(char)); - *(state.input) = '\0'; - state.output = malloc(sizeof(char)); - *(state.output) = '\0'; - state.name = malloc(sizeof(char) * (1 + strlen(defaultName))); - strcpy(state.name, defaultName); -} - -void signalHandler(int signal) -{ - fprintf(stderr, "Received signal: %d\n", signal); - cleanExit(1); -} - -void setupSignalHandler() -{ - signal(SIGABRT, signalHandler); - signal(SIGFPE, SIG_IGN); - signal(SIGILL, SIG_IGN); - signal(SIGINT, signalHandler); - signal(SIGTERM, signalHandler); - signal(SIGSEGV, signalHandler); -} - -void parseArgs(int argc, char *argv[]) -{ - char opt; - - while ((opt = getopt(argc, argv, "g:n:i:o:")) != -1) - { - switch (opt) - { - case 'g': - sscanf(optarg, "%f", &(state.dBGain)); - break; - case 'n': - state.name = realloc(state.name, sizeof(char) * (1 + strlen(optarg))); - strcpy(state.name, optarg); - break; - case 'i': - state.input = realloc(state.input, sizeof(char) * (1 + strlen(optarg))); - strcpy(state.input, optarg); - break; - case 'o': - state.output = realloc(state.output, sizeof(char) * (1 + strlen(optarg))); - strcpy(state.output, optarg); - break; - } - } - - if (optind > argc - 1) - { - fprintf(stderr, "Usage: %s [-g {gain}] [-n {name}] [-i {input}] [-o {output}] {working directory}\n", argv[0]); - cleanExit(2); - } - - workdir = argv[optind++]; -} - -void setWorkdir() -{ - if (!(mkdir(workdir, DIRMODE) == 0 || errno == EEXIST) || chdir(workdir) == -1) - errMsg(1, &errno, "Failed to change to workdir", workdir); - changedDir = true; -} - -void writePid() -{ - FILE *pidFile; - - openSyncFile(&pidFile, PIDFILE, "wx", false); - if (fprintf(pidFile, "%d\n", getpid()) <= 1) - errMsg(1, &errno, "Failed to write to pidfile", PIDFILE); - fclose(pidFile); -} - -void syncState() -{ - readState(); - writeState(); -} - -void readState() -{ - FILE *pidFile; - FILE *inputFile; - FILE *outputFile; - FILE *nameFile; - FILE *gainFile; - int newInt; - char newString[64]; - float newFloat; - int ret; - - if (openSyncFile(&pidFile, PIDFILE, "r", true) == -1) - { - ret = fscanf(pidFile, "%d", &newInt); - fclose(pidFile); - - if (ret == 0 || ret == EOF || newInt != getpid()) - cleanExit(0); - } - else - cleanExit(0); - - if (openSyncFile(&inputFile, INPUTFILE, "r", true) == -1) - { - ret = fscanf(inputFile, "%63s", newString); - fclose(inputFile); - - if (ret != 0 && ret != EOF) - { - // TODO Try to set new jack input here - state.input = realloc(state.input, sizeof(char) * (1 + strlen(newString))); - strcpy(state.input, newString); - } - } - if (openSyncFile(&outputFile, OUTPUTFILE, "r", true) == -1) - { - ret = fscanf(outputFile, "%63s", newString); - fclose(outputFile); - - if (ret != 0 && ret != EOF) - { - // TODO Try to set new jack output here - state.output = realloc(state.output, sizeof(char) * (1 + strlen(newString))); - strcpy(state.output, newString); - } - } - - if (openSyncFile(&gainFile, GAINFILE, "r", true) == -1) - { - ret = fscanf(gainFile, "%f", &newFloat); - fclose(gainFile); - - if (ret != 0 && ret != EOF) - { - if (newFloat < -90) - newFloat = -90; - state.dBGain = newFloat; - } - } -} - -void writeState() -{ - FILE *pidFile; - FILE *inputFile; - FILE *outputFile; - FILE *nameFile; - FILE *gainFile; - int ret; - - openSyncFile(&pidFile, PIDFILE, "w", false); - ret = fprintf(pidFile, "%d\n", getpid()); - if (ret < 1) - errMsg(1, &errno, "Failed to write pidfile", PIDFILE); - fclose(pidFile); - - openSyncFile(&inputFile, INPUTFILE, "w", false); - ret = fprintf(inputFile, "%s\n", state.input); - if (ret < 1) - errMsg(1, &errno, "Failed to write file", INPUTFILE); - fclose(inputFile); - - openSyncFile(&outputFile, OUTPUTFILE, "w", false); - ret = fprintf(outputFile, "%s\n", state.output); - if (ret < 1) - errMsg(1, &errno, "Failed to write file", OUTPUTFILE); - fclose(outputFile); - - openSyncFile(&nameFile, NAMEFILE, "w", false); - ret = fprintf(nameFile, "%s\n", state.name); - if (ret < 1) - errMsg(1, &errno, "Failed to write file", NAMEFILE); - fclose(nameFile); - - openSyncFile(&gainFile, GAINFILE, "w", false); - ret = fprintf(gainFile, "%.2f\n", state.dBGain); - if (ret < 1) - errMsg(1, &errno, "Failed to write file", GAINFILE); - fclose(gainFile); -} - -int openSyncFile(FILE **file, char *fileName, char *fileMode, bool errOK) -{ - int ret = -1; - - if ((*file = fopen(fileName, fileMode)) == NULL) - { - if (errOK == false) - errMsg(1, &errno, "Failed to open file", fileName); - else - { - ret = errno; - } - } - - return ret; -} - -int main(int argc, char *argv[]) -{ - int inotifyFD; - - initState(); - inotifyBuffer = malloc(INOTIFYBUFLEN); - setupSignalHandler(); - - parseArgs(argc, argv); - setWorkdir(); - - writePid(); - writeState(); - - inotifyFD = inotify_init(); - if (inotifyFD < 0) - errMsg(1, NULL, "Failed to setup inotify", NULL); - - do - { - syncState(); - printf("State:\n input = %s\n output = %s\n name = %s\n gain = %.2fdB\n", state.input, state.output, state.name, state.dBGain); - inotify_add_watch(inotifyFD, ".", NOTIFYMASK); - } - while (read(inotifyFD, inotifyBuffer, INOTIFYBUFLEN) >= 0); - - exit(0); -} - -void cleanExit(int r) -{ - if (changedDir == true) - { - if (unlink(PIDFILE) != 0 && errno != ENOENT) - errMsg(-1, &errno, "Failed to delete pidfile", PIDFILE); - } - free(state.input); - free(state.output); - free(state.name); - free(inotifyBuffer); - exit(r); -} - -void errMsg(int r, int *errno, char *head, char *detail) -{ - int err; - bool error = false; - - if (errno != NULL) - { - error = true; - err = *errno; - } - - fprintf(stderr, "%s\n", head); - if (detail != NULL) - fprintf(stderr, " %s\n", detail); - if (error == true) - fprintf(stderr, " (%d) %s\n", err, strerror(err)); - - if (r >= 0) - cleanExit(r); -} diff --git a/trivmix.cabal b/trivmix.cabal new file mode 100644 index 0000000..ae9a72e --- /dev/null +++ b/trivmix.cabal @@ -0,0 +1,35 @@ +-- Initial trivmix.cabal generated by cabal init. For further +-- documentation, see http://haskell.org/cabal/users-guide/ + +name: trivmix +version: 0.0.0 +-- synopsis: +-- description: +license: PublicDomain +license-file: LICENSE +author: Gregor Kleen +maintainer: aethoago@141.li +-- copyright: +category: Sound +build-type: Simple +-- extra-source-files: +cabal-version: >=1.10 + +executable trivmix + main-is: Trivmix.hs + -- other-modules: + -- other-extensions: + build-depends: base >=4.7 && <4.8 + , jack >=0.7 && <1 + , optparse-applicative >=0.11 && <1 + , directory >=1.2 && <2 + , filepath >=1.3 && <2 + , unix >=2.7 && <3 + , hinotify >=0.3 && <1 + , transformers >=0.3 && <1 + hs-source-dirs: src + default-language: Haskell2010 + +-- Local Variables: +-- firestarter: "nix-shell -p haskellPackages.cabal2nix --command 'cabal2nix ./.' | tee trivmix.nix" +-- End: diff --git a/trivmix.h b/trivmix.h deleted file mode 100644 index 03b5860..0000000 --- a/trivmix.h +++ /dev/null @@ -1,21 +0,0 @@ -typedef struct mixState { - char *input; - char *output; - char *name; - float dBGain; -} mixState; - -typedef enum bool { - false, - true -} bool; - -void parseArgs(int argc, char *argv[]); -void setWorkdir(); -void syncState(); -void readState(); -void writeState(); -int openSyncFile(FILE **file, char *fileName, char *fileMode, bool errOK); - -void cleanExit(int r); -void errMsg(int r, int *errno, char *head, char *detail); diff --git a/trivmix.nix b/trivmix.nix new file mode 100644 index 0000000..746d548 --- /dev/null +++ b/trivmix.nix @@ -0,0 +1,20 @@ +# This file was auto-generated by cabal2nix. Please do NOT edit manually! + +{ cabal, filepath, hinotify, jack, optparseApplicative +, transformers +}: + +cabal.mkDerivation (self: { + pname = "trivmix"; + version = "0.0.0"; + src = ./.; + isLibrary = false; + isExecutable = true; + buildDepends = [ + filepath hinotify jack optparseApplicative transformers + ]; + meta = { + license = self.stdenv.lib.licenses.publicDomain; + platforms = self.ghc.meta.platforms; + }; +}) -- cgit v1.2.3