packages feed

hemokit-0.5: apps/FFT.hs

module Main where

import           Control.Monad
import           Control.Monad.Trans
import           Data.Complex
import           Data.Conduit
import qualified Data.Conduit.List as CL
import           Data.List
import           Data.Vector (Vector)
import qualified Data.Vector as V
import           Numeric.FFT.Vector.Unnormalized
import           System.IO
import           Text.Printf

import           Hemokit
import           Hemokit.Start

import           Hemokit.Internal.Utils (untilNothing)


rollingFFTConduit :: (Monad m) => Int -> ConduitM (Vector Double) [Vector Double] m ()
rollingFFTConduit size = mapOutput (map (V.map magnitude . execute fft . ground) . transposeV 14) (rollingBuffer size)
  where
    fft = plan dftR2C size


packets :: EmotivDevice -> Source IO EmotivState
packets d = void $ untilNothing (liftIO (readEmotiv d)) (yield . fst)

buffer :: Monad m => Int -> Conduit a m [ a ]
buffer n = forever (CL.take n >>= yield)


-- | Rolls a buffer of size n over the input, always taking one element in,
-- throwing an old one out.
-- Only starts returning buffers once the buffer is filled.
--
-- Implemented using a Difference List.
-- This allows fast skipping of buffers, e.g. for using only every 5th one.
rollingBuffer :: (Monad m) => Int -> Conduit a m [ a ]
rollingBuffer 0 = return ()
rollingBuffer n | n < 0     = error "rollingBuffer: negative buffer size"
                | otherwise = fillup 0 id
  where
    -- Consume until buffer is filled with n elements.
    fillup have front
      | have < n  = await >>= maybe (return ()) (\x -> fillup (have+1) (front . (x:)))
      | otherwise = roll front
    -- Then keep kicking one element out, taking a new element in, yielding the buffer each time.
    roll front = do yield (front [])
                    await >>= maybe (return ()) (\x -> roll (tail . front . (x:)))


printAll :: Sink [V.Vector Double] IO ()
-- printAll = awaitForever $ \tds -> liftIO $ putStrLn (unlines (map showFFT tds))
-- printAll = awaitForever $ \tds -> liftIO $ putStrLn (unlines (map graphFFT tds))
printAll = do
  liftIO $ hSetBuffering stdout (BlockBuffering (Just 8000))
  awaitForever $ \tds -> liftIO $ putStrLn (unlines (map graphFFT [last tds])) >> hFlush stdout -- >> threadDelay 1000000


-- | Converts a length M list of length N vectors into a length N list of length M vectors.
-- Example: [ v1a v1b v1c ]      [ v1a, v2a ]
--          [ v2a v2b v2c ]  ->  [ v1b, v2b ]
--                               [ v1c, v2c ]
transposeV :: Int -> [ V.Vector a ] -> [ V.Vector a ]
transposeV n vs = [ V.fromList (map (V.! i) vs) | i <- [ 0 .. n - 1 ] ]

showFFT :: V.Vector Double -> String
showFFT ms = unwords . V.toList . V.map (formatNumber . maxed) $ ms
    where
      formatNumber n = printf "%.3f" n
      -- formatNumber n = printf "%2.0f" n

      -- simple      = id
      -- distributed = (/ V.sum ms)
      maxed       = (/ V.maximum ms)


graphFFT :: V.Vector Double -> String
-- graphFFT ms = (unlines . transpose . V.toList . V.map (formatNumber . maxed) $ ms) ++ showFFT ms
graphFFT ms = (unlines . transpose . V.toList . V.map (formatNumber . maxed) $ ms)
    where
      maxed = (/ V.maximum ms)
      formatNumber n = replicate space ' ' ++ replicate filled '|'
        where
          chars  = 40
          filled = floor (n * fromIntegral chars)
          space  = chars - filled


toChar :: Double -> Char
toChar m
  | m < 0.25  = ' '
  | m < 0.5   = '.'
  | m < 0.75  = 'o'
  | otherwise = '#'

-- | Reduces a data series by its average.
-- This is useful to bring a signal moving around at some level "to the ground".
-- Example: 4 5 4 3 -> 0 1 0 -1
ground :: V.Vector Double -> V.Vector Double
ground v = V.map (subtract avg) v
  where
    avg = V.sum v / fromIntegral (V.length v)


main :: IO ()
main = do
  m'device <- getEmotivDeviceFromArgs =<< parseArgs "FFT on Emotiv data" emotivArgsParser

  case m'device of
    Left err -> error err
    Right device -> do

      let sensorData = mapOutput (V.map fromIntegral . sensors) (packets device)

      sensorData $= rollingFFTConduit 256 $$ printAll