packages feed

word-note-sox-0.1.0.0: Composition/Sound/SoX.hs

-- |
-- Module      :  Composition.Sound.SoX
-- Copyright   :  (c) Oleksandr Zhabenko 2019-2024
-- License     :  MIT
-- Maintainer  :  oleksandr.zhabenko@yahoo.com
--
-- Some functionality that is primarily implemented using 'String' and 'System.Process' in the algorithmic-composition-basic and related packages. Is rewritten and groupped to be probably more suitable for concurrent and asynchronous usage.

{-# LANGUAGE NoImplicitPrelude, OverloadedStrings, OverloadedLists #-}
{-# OPTIONS_HADDOCK -show-extensions #-}


module Composition.Sound.SoX where

import GHC.Base hiding (foldr) 
import GHC.Num ((+),(*))
import GHC.Real (fromIntegral,rem,(/))
import Text.Show (Show(..))
import Text.Read
import Data.Tuple (fst)
import Data.List hiding (lines,words,take,head,uncons,foldr,dropWhile) 
import System.Directory
import System.IO (FilePath, stderr, putStrLn)
import Data.ByteString.Lazy hiding (null,take,filter,isSuffixOf,foldr,dropWhile)
import Data.ByteString.Lazy.Char8 (lines,words,take,foldr,dropWhile)  
--import System.Exit (ExitCode(ExitSuccess))
import EndOfExe2 (showE0Dup)
import Numeric (showFFloat)
import Data.InsertLeft (takeFromEndG, splitAtEndG)
import System.Process.Typed

type SoXEffects = [String]

-- Taken from the Composition.Sound.IntermediateF module from @algorithmic-composition-basic@ here so that they are more used this way.  

-- | Takes a filename to be applied a SoX chain of effects as list of 'String' (the second argument). Produces the temporary
-- new file with the name ((name-of-the-file) ++ (\"effects.wav\"  OR \"effects.flac\") -- the type is preserved), which then is removed. 
--
-- The syntaxis is that every separate literal for SoX must be a new element in the list. Please, for more information, refer to SoX documentation.
-- Please, check by yourself whether you have enough permissions to work with the corresponding 'FilePath's.
soxE :: FilePath -> SoXEffects -> IO ()
soxE file arggs = do
  let effile = file ++ "effects" ++ efw2 file
  (code,_,herr) <- readProcess (proc (showE0Dup "sox") ([file,effile] ++ arggs))
  case code of
    ExitSuccess -> renameFile effile file
    _ -> do
       exist <- doesFileExist effile
       if exist then do 
                  hPut stderr herr
                  removeFile effile
                  putStrLn $ "Composition.Sound.SoX.soxE: Applying SoX on the file \"" ++ file ++ "\" has not been successful. The file " ++ file ++ " has not been changed at all. "
       else do
         hPut stderr herr
         putStrLn $ "Composition.Sound.SoX.soxE: Creation of the file \"" ++ effile ++ "\" has not been successful. The file " ++ file ++ " has not been changed at all. "

w2f :: FilePath -> FilePath
w2f file 
  | ts == ".wav" = zs ++ ".flac" 
  | otherwise = error "Composition.Sound.SoX.w2f: The file is not a WAV file! "
      where (zs,ts) = splitAtEndG 4 file  
{-# INLINE w2f #-}

f2w :: FilePath -> FilePath
f2w file 
  | ts == ".flac" = zs ++ ".wav" 
  | otherwise = error "Composition.Sound.SoX.f2w: The file is not a FLAC file! "
      where (zs,ts) = splitAtEndG 5 file
{-# INLINE f2w #-}

wOrf :: FilePath -> String
wOrf file 
  | us == ".wav" = "w"
  | us == "flac" = "f"
  | otherwise = error "Composition.Sound.SoX.wOrf: The file is neither a WAV nor a FLAC file!"
      where us = takeFromEndG 4 file 
{-# INLINE wOrf #-}

cfw2wf :: FilePath -> FilePath
cfw2wf file
 | wf == "w" = w2f file
 | wf == "f" = f2w file
 | otherwise = error "Composition.Sound.SoX.cfw2wf: The file is neither a WAV nor a FLAC file! "
     where wf = wOrf file
{-# INLINE cfw2wf #-}

efw2 :: FilePath -> String
efw2 file 
 | us == ".wav" = us
 | us == "flac" = '.':us
 | otherwise = error "Composition.Sound.SoX.efw2: The file is neither a WAV nor a FLAC file!"
     where us = takeFromEndG 4 file
{-# INLINE efw2 #-}

efw2vv :: FilePath -> String
efw2vv file 
 | us == ".wav" = ".flac"
 | us == "flac" = ".wav"
 | otherwise = error "Composition.Sound.SoX.efw2vv: The file is neither a WAV nor a FLAC file! "
     where us = takeFromEndG 4 file 
{-# INLINE efw2vv #-}

--------------------------

-- | Applies \"fade q\" effect to both ends of the supported by SoX sound file 'FilePath' so that concatenating them consequently after such application 
-- leads to no clipping. Otherwise, the clipping exists if not prevented by may be some other means. For more information, please, refer to the
-- SoX documentation.
fadeEnds :: FilePath -> IO ()
fadeEnds = fadeEndsMilN 10
{-# INLINE fadeEnds #-}

-- | Applies \"fade q\" effect to both ends of the supported by SoX sound file 'FilePath' so that concatenating them consequently after such application 
-- leads to no clipping. Otherwise, the clipping exists if not prevented by may be some other means. The duration of the changes are usually 
-- smaller than for 'fadeEnds' function and is equal to 0.001 \* n sec (where n is in range [1..10]). 
-- For more information, please, refer to the SoX documentation.
fadeEndsMilN :: Int -> FilePath -> IO ()
fadeEndsMilN n file = soxE file ["fade","q", showFFloat (Just 4) (if (n `rem` 11) /= 0 then 0.001 * fromIntegral (n `rem` 11) else 0.002) "","-0.0"]

-- | Applies \"fade\" effect (the type is specified by the 'Char' argument, for more information, please, refer to the SoX documentation) to the both ends 
-- of the sound with header (supported by SoX). The 'Float' arguments specify the percentages of the length of the sound that is faded-in and faded-out 
-- respectively. Otherwise, the function returns an error.
fadeEndsTMN :: Char -> Float -> Float -> FilePath -> IO ()
fadeEndsTMN c per1 per2 file 
 | compare per1 0 == GT && compare per2 0 == GT && compare (per1 + per2) 100 /= GT = do
    d0 <- durationA file
    soxE file ["fade", case c of {'h' -> "h"; 'p' -> "p"; 'q' -> "q"; 't' -> "t"; ~_ -> "l"}, showFFloat (Just 4) (d0 * per1 / 100.0) "","-0.0", 
      showFFloat (Just 4) (d0 * per2 / 100.0) ""]
 | otherwise = error "Composition.Sound.SoX.fadeEndsTMN: the percentages sum is out of the (0..100] range. "

-- | Variant of the 'fadeEndsTMN' with the both equal percentages specified by the 'Float' argument. It must be in the range (0..50]. Otherwise, the function 
-- returns error.
fadeEndsTMB :: Char -> Float -> FilePath -> IO ()
fadeEndsTMB c per 
 | per > 0 && per <= 50 = fadeEndsTMN c per per
 | otherwise = error "Composition.Sound.SoX.fadeEndsTMB: The percentage is out of the (0..50] range. "
{-# INLINE fadeEndsTMB #-}

----------------------------------
--

takeU :: ByteString -> ByteString
takeU u
 | take 1 u == "-" = take 9 u
 | otherwise = take 8 u
{-# INLINE takeU #-}

-- | Function 'getMaxAG' returns a maximum amplitude of the sound in the file in the given lower and upper bounds represented as a tuple of 'Int' values.
getMaxAG :: ULencode -> FilePath -> (Int, Int) -> IO ByteString
getMaxAG ul file (lowerbound, upperbound) 
  | null . showE0Dup $ "sox" =   error "Composition.Sound.SoX.getMinAG: The SoX executable is not properly installed."
  | otherwise = do
     (_, _, herr) <- soxOpG1 ul [] file [] ["trim", show lowerbound ++ "s", "=" ++ show upperbound ++ "s", "stat"]
     let zs = lines herr 
     return (let u = (words $ zs !! 3) !! 2 in takeU u)

-- | Function 'getMinAG' returns a minimum amplitude of the sound in the file in the given lower and upper bounds represented as a tuple of 'Int' values.
getMinAG :: ULencode -> FilePath -> (Int, Int) -> IO ByteString
getMinAG ul file (lowerbound, upperbound) 
  | null . showE0Dup $ "sox" = error "Composition.Sound.SoX.getMinAG: The SoX executable is not properly installed."
  | otherwise = do
      (_, _, herr1) <- soxOpG1 ul [] file [] ["trim", show lowerbound ++ "s", "=" ++ show upperbound ++ "s", "stat"]
      let zs = lines herr1
      return (let u = (words $ zs !! 4) !! 2 in takeU u)
 
-- | Function 'selMaxAbsG' returns a maximum by absolute value amplitude of the sound and allows by its second value in the tuple determine whether it is a maximum or minimum.
-- Bool 'True' corresponds to maximum value, 'False' - to minimum value.
selMaxAbsG :: ULencode -> FilePath -> (Int, Int) -> IO (ByteString, Bool)
selMaxAbsG ul file (lowerbnd, upperbnd) = do
  tX <- getMaxAG ul file (lowerbnd, upperbnd)
  tN <- getMinAG ul file (lowerbnd, upperbnd)
  return (maxAbs (tX, tN))

data ULencode = W | UL1 | UL0 | UL deriving (Eq, Ord)

instance Show ULencode where
  show W = "(False, False)" -- Only working with .wav files.
  show UL1 = "(False, True)" -- .ul appears.
  show UL0 = "(True, False)" -- .ul disappears.
  show _ = "(True, True)" -- .ul is constantly used.

class SoundFileExts a where
  getExts :: a -> (String,String)
  isFileExtsR :: a -> FilePath -> FilePath -> Bool
  isFileExtsR ul file1 file2 = xs `isSuffixOf` file1 && ys `isSuffixOf` file2
    where (xs,ys) = getExts ul

instance SoundFileExts ULencode where
  getExts W = (".wav",".wav")
  getExts UL1 = (".wav",".ul")
  getExts UL0 = (".ul",".wav")
  getExts _ = (".ul",".ul")

-- | The variant of the 'soxOpG' that is used if the second file is not used (or in the situation where some
-- other file is used, too, e. g. with the .prof extension). For the functions in the module, this corresponds
-- to the \"-n\" second file argument.
soxOpG1 :: ULencode -> [String] -> FilePath -> [String] -> [String] -> IO (ExitCode, ByteString, ByteString)
soxOpG1 ul xss file1 yss zss
 | (fst . getExts $ ul) `isSuffixOf` file1 =
    if ul < UL0 then readProcess (proc (showE0Dup "sox") (filter (not . null) . mconcat $ [xss, [file1], yss, ["-n"], zss]))
    else readProcess (proc (showE0Dup "sox") (filter (not . null) . mconcat $ [xss, ulAccessParameters, [file1], yss, ["-n"], zss])) 
 | otherwise = error "Composition.Sound.SoX.soxOpG1: A given file has inappropriate file extension, or there has occurred some other error. Please, check the arguments. "

-- | Function 'durationAG' returns a duration of the audio file in seconds.
durationAG :: ULencode -> FilePath -> IO Float
durationAG ul file 
  | null . showE0Dup $ "soxi" = error "Composition.Sound.SoX.durationAG: The SoX executable is not properly installed."
  | otherwise = do
      (_, hout) <- readProcessStdout (proc (showE0Dup "soxi") (if ul < UL0 then ["-D",file] else mconcat [["-D"],ulAccessParameters,[file]]))
      let x0 = foldr (:) [] hout
      return (read x0::Float)

-- | A variant of the 'durationAG' with the first argument being 'W'.
durationA :: FilePath -> IO Float
durationA = durationAG W
{-# INLINE durationA #-}

-- | Function 'upperBndG' returns a maximum number of samples for use in other functions.
upperBndG :: ULencode -> FilePath -> IO Int
upperBndG ul file 
  | null . showE0Dup $ "soxi" = error "Composition.Sound.SoX.upperBndG: The SoX executable is not properly installed."
  | otherwise = do 
       (_, hout) <- readProcessStdout (proc (showE0Dup "soxi") (if ul < UL0 then ["-s",file] else mconcat [["-s"],ulAccessParameters,[file]])) 
       let x0 = foldr (:) [] hout
       return (read x0::Int) 

-- | A variant of the 'selMaxAbsG' with the first argument being 'W'.
selMaxAbs :: FilePath -> (Int, Int) -> IO (ByteString, Bool)
selMaxAbs = selMaxAbsG W
{-# INLINE selMaxAbs #-}

-- | Function 'maxAbs' allows to choose a maximum by absolute value if the values are written as 'ByteString'. Bool 'True' corresponds to maximum value, 'False' - to minimum value
maxAbs :: (ByteString, ByteString) -> (ByteString, Bool)
maxAbs ([], _) = ([], False)
maxAbs (_, []) = ([], False)
maxAbs (xs, ys) 
 | dropWhile (== '-')  xs > dropWhile (== '-') ys = (xs, True)
 | otherwise = (ys, True)
{-# INLINE maxAbs #-}

ulAccessParameters :: [String]
ulAccessParameters = ["-r22050","-c1"]
{-# INLINE ulAccessParameters #-}