Southpaw-0.1.0.0: lib/Southpaw/AbbeyRoad/Capture.hs
-- |
-- Module : Southpaw.AbbeyRoad.Capture
-- Description : Microphone input
-- Copyright : (c) Jonatan H Sundqvist, 2015
-- License : MIT
-- Maintainer : Jonatan H Sundqvist
-- Stability : experimental|stable
-- Portability : POSIX (not sure)
--
-- Created July 22 2015
--
-- I am forever indebted to the author of this module: https://github.com/peterhil/hs-openal-proto/blob/master/src/Sound/OpenAL/Proto/Play.hs
-- Saved me from having to figure out how to construct memory views by myself.
-- TODO | - Proper error handling (IO exceptions, Either, or something else)
-- - Remove redundant imports
-- - Include the legal note from Peter's repo
-- SPEC | -
-- -
module Southpaw.AbbeyRoad.Capture where
---------------------------------------------------------------------------------------------------
-- We'll need these
---------------------------------------------------------------------------------------------------
import Control.Concurrent (threadDelay) --
import Control.Exception (bracket) --
import Control.Monad (unless, when) --
import Data.Int --
import Data.List (intersperse) --
import Data.Word --
import GHC.Float (float2Double) --
import Foreign -- Import the foreigners!
import Foreign.C.Types --
import Foreign.ForeignPtr --
import Foreign.ForeignPtr.Unsafe --
import Foreign.Ptr --
import Sound.ALUT --
import Sound.OpenAL --
import Sound.OpenAL.AL.BasicTypes (ALsizei) --
import Sound.OpenAL.ALC.Capture --
import System.Exit (exitFailure) --
import System.IO (hPutStrLn, stderr, openBinaryFile, IOMode(ReadMode), hClose, hFileSize, hGetBuf) --
import qualified Data.Vector.Storable as V --
import qualified Data.Vector.Storable.Mutable as VM --
---------------------------------------------------------------------------------------------------
-- Types
---------------------------------------------------------------------------------------------------
type Sample = Double
---------------------------------------------------------------------------------------------------
-- Data
---------------------------------------------------------------------------------------------------
samplerate = 44100 :: Int -- TODO: Don't hard-code sample rate
mono8BufferSize = bufferSize 1 (undefined :: Word8)
mono16BufferSize = bufferSize 1 (undefined :: Int16)
stereo8BufferSize = bufferSize 2 (undefined :: Word8)
stereo16BufferSize = bufferSize 2 (undefined :: Int16)
---------------------------------------------------------------------------------------------------
-- Functions
---------------------------------------------------------------------------------------------------
-- |
bufferSize :: Storable a => Int -> a -> Double -> Int
bufferSize nchannels sampleType secs = fromIntegral (numSamples secs) * sizeOf sampleType * nchannels
-- |
numSamples :: Double -> NumSamples
numSamples secs = round (fromIntegral(samplerate) * secs) :: NumSamples
---------------------------------------------------------------------------------------------------
-- |
sine :: Double -> [Sample]
sine freq = cycle $ take n $ map sin [0, d..]
where d = 2 * pi * freq / sr
n = truncate (sr / freq)
sr = fromIntegral samplerate
---------------------------------------------------------------------------------------------------
-- |
pcm :: (Integral a) => Int -> Sample -> a
pcm bits sample = truncate $ sample * (fromIntegral (2 ^ (bits - 1)) - 1)
---------------------------------------------------------------------------------------------------
-- |
-- # From http://dev.stephendiehl.com/hask/#ffi
vecPtr :: VM.MVector s CInt -> ForeignPtr CInt
vecPtr = fst . VM.unsafeToForeignPtr0
---------------------------------------------------------------------------------------------------
-- |
-- captureSamples :: Device -> Ptr a -> NumSamples -> IO ()
-- allocaBytes :: Int -> (Ptr a -> IO b) -> IO b
-- allocaBytesAligned :: Int -> Int -> (Ptr a -> IO b) -> IO b
-- TODO: Find out why captureOpenDevice returns Nothing when secs is 0
-- TODO: Pass in failure action (?)
-- TODO: Rename (eg. 'record' to 'startRecording') (?)
withCaptureDevice :: (Maybe String) -> Double -> (Device -> IO c) -> IO (Maybe c)
withCaptureDevice specifier secs onsuccess = bracket acquire finally between
where format = Mono16
finally = maybe (return False) (\mic -> putStrLn "Closing device..." >> captureStop mic >> captureCloseDevice mic)
record mic = captureStart mic >> return mic
acquire = captureOpenDevice specifier (fromIntegral samplerate) format (numSamples secs)
between mmic = case mmic of
Just mic -> record mic >> onsuccess mic >>= return . Just
Nothing -> return Nothing
-- |
-- TODO: Refactor
-- capture :: V.Storable a => (Maybe String) -> Double -> IO (MemoryRegion a)
-- capture :: Storable a => (Maybe String) -> Double -> (MemoryRegion a -> IO c) -> IO c
-- capture :: (Maybe String) -> Double -> IO (V.Vector Int16)
capture :: Maybe String -> Double -> (MemoryRegion CInt -> IO c) -> IO (Maybe c) -- According to GHCi
capture specifier duration action = withCaptureDevice specifier duration record
where num = numSamples duration
record mic = do
sleep $ realToFrac duration -- Sleep until we should stop recording
mutableV <- V.thaw . V.fromList . map (pcm 16) . take (fromIntegral num) $ sine 220 --
withForeignPtr (vecPtr mutableV) $ \ptr -> captureSamples mic ptr (fromIntegral num) --
rec <- V.freeze mutableV --
let (mem, size) = V.unsafeToForeignPtr0 rec --
withForeignPtr mem $ \ptr -> action $ MemoryRegion ptr (fromIntegral size) --
---------------------------------------------------------------------------------------------------
-- |
checkAlErrors :: IO [String]
checkAlErrors = do
errs <- get $ alErrors
return [ d | ALError _ d <- errs ]
-- |
checkAlcErrors :: Device -> IO [String]
checkAlcErrors device = do
errs <- get $ alcErrors device
return [ d | ALCError _ d <- errs ]
---------------------------------------------------------------------------------------------------
-- |
withFileContents :: FilePath -> (MemoryRegion a -> IO b) -> IO b
withFileContents filePath action =
bracket (openBinaryFile filePath ReadMode) hClose $ \handle -> do
numBytes <- fmap fromIntegral (hFileSize handle)
allocaBytes numBytes $ \buf -> do
bytesRead <- hGetBuf handle buf numBytes
when (bytesRead /= numBytes) $
ioError (userError "hGetBuf")
action (MemoryRegion buf (fromIntegral numBytes))