Charlie Harvey

More haskell morse code. This time with audio!

morse key

Today I added audio support to the morse translator that I made yesterday. There ought to be a screencast of me demoing it, but screenr is being a dick, as it sometimes is for no discernable reason.

I found the admirably minimalistic Data.WAVE library with a little bit of searching. This I installed by typing $ cabal install WAVE

When Data.WAVE is combined with a function called "sound" that David Milani posted on stack overflow it becomes pretty straightforward to define sounds for the dits and dahs. And for the spaces between them. I used the gaps suggested by the wikipedia article -- 1 dit between dits and dahs, one dah between letters and 7 dits between words. The function turns a sound into a big array of samples, which are actually Int32s.

With that infrastructure in place I added 3 new command line options to morse.hs

-v
Turns up the verbosity, which was handy for debugging
-a
Rather than putting a string in the terminal outputs your morse as a wav in morse-out.wav
-p
Tries to use aplay to play morse-out.wav after it is created.

So now I can run my script to generate exciting morse sounds like these!

As @russelbanned pointed out on twitter, it would also be nice to be able to listen to the audio and get morse or a string out. But that is a project for another day.

morse.hs

Here is the actual code in its current iteration. module Main where import qualified Data.Map as M (Map, toList, fromList, lookup) import System.Console.GetOpt import System.Process import Data.Maybe (fromMaybe) import Data.Char (toLower) import System.Environment import Data.WAVE import Data.List (intersperse) import Data.Int (Int32) import Data.List.Split (splitOn) data Options = Options { optUnmorse :: Bool, optAudio :: Bool, optPlayAudio :: Bool, optVerbose :: Bool } deriving Show type MorseString = String defaultOptions :: Options defaultOptions = Options { optUnmorse = False, optAudio = False, optPlayAudio = False, optVerbose = False } options :: [OptDescr (Options -> Options)] options = [ Option ['u'] ["unmorse"] (NoArg (\ opts -> opts { optUnmorse = True })) "Translate from morse back to string", Option ['a'] ["audio"] (NoArg (\ opts -> opts { optAudio = True })) "Translate from string to audio. Output file to morse-out.wav and play using aplay", Option ['p'] ["playaudio"] (NoArg (\ opts -> opts { optPlayAudio = True })) "Play morse-out.wav using aplay", Option ['v'] ["verbose"] (NoArg (\ opts -> opts { optVerbose = True })) "Go on about it at great length" ] myOpts :: [String] -> IO (Options, [String]) myOpts argv = case getOpt Permute options argv of (o,n,[] ) -> return (foldl (flip id) defaultOptions o, n) (_,_,errs) -> ioError (userError (concat errs ++ usageInfo header options)) where header = "Usage: morse [uavp] < file" letters :: (M.Map Char MorseString) letters = M.fromList [ ('a', ".-") , ('b', "-...") , ('c', "-.-.") , ('d', "-..") , ('e', ".") , ('f', "..-.") , ('g', "--.") , ('h', "....") , ('i', "..") , ('j', ".---") , ('k', "-.-") , ('l', ".-..") , ('m', "--") , ('n', "-.") , ('o', "---") , ('p', ".--.") , ('q', "--.-") , ('r', ".-.") , ('s', "...") , ('t', "-") , ('u', "..-") , ('v', "...-") , ('w', ".--") , ('x', "-..-") , ('y', "-.--") , ('z', "--..") , ('1', ".----") , ('2', "..---") , ('3', "...--") , ('4', "....-") , ('5', ".....") , ('6', "-....") , ('7', "--...") , ('8', "---..") , ('9', "----.") , ('0', "-----") , (' ', "_") , ('.', "_") ] invertLetters = M.fromList $ map (\(x,y) -> (y,x)) $ M.toList letters charToMorse :: Char -> MorseString charToMorse x = case M.lookup (toLower x) letters of Just x' -> x' ++ " " Nothing -> "" stringToMorse :: String -> MorseString stringToMorse xs = m ++ "\n" where m = concatMap charToMorse xs morseToChar :: MorseString -> Char morseToChar x = case M.lookup x invertLetters of Just x' -> x' Nothing -> '?' morseToString :: MorseString -> String morseToString xs = s ++ "\n" where s = map morseToChar $ words xs -- Sound implementation samplesPS = 16000 -- samples per second bitrate = 16 -- or 32 bit if you need beepFreq = 800 -- dits and dahs come out at this frequency operatorSpeedFactor = 4 -- higher is slower, could make this into a commandline option morseSpeed = operatorSpeedFactor/10 -- Used in the file header header :: WAVEHeader header = WAVEHeader 1 samplesPS bitrate Nothing -- From an answer by David Milani at: -- https://stackoverflow.com/questions/5658391/generating-wav-sound-data-in-haskell sound :: Double -- | Frequency -> Int -- | Samples per second -> Double -- | Length of sound in seconds -> Int32 -- | Volume, (maxBound :: Int32) for highest, 0 for lowest -> [Int32] sound freq samples len volume = take (round $ len * (fromIntegral samples)) $ map (round . (* fromIntegral volume)) $ map sin [0.0, (freq * 2 * pi / (fromIntegral samples))..] -- space between dots and dahs space :: [Int32] space = sound 0 samplesPS (morseSpeed * 0.25) 0 -- gap between words longSpace :: [Int32] longSpace = sound 0 samplesPS (morseSpeed * 1.25) 0 medSpace :: [Int32] medSpace = sound 0 samplesPS (morseSpeed * 0.5) 0 dit :: [Int32] dit = sound beepFreq samplesPS (morseSpeed * 0.25) (maxBound `div` 2) dah :: [Int32] dah = sound beepFreq samplesPS (morseSpeed * 0.75) (maxBound `div` 2) stringToMorseWavFile :: String -> IO () stringToMorseWavFile xs = makeWavFile $ concatMap (map (:[])) $ stringToMorseSounds xs stringToMorseSounds :: String -> [[Int32]] stringToMorseSounds xs = map charToMorseSound $ stringToMorse xs charToMorseSound :: Char -> [Int32] charToMorseSound x = case x of '.' -> concat [dit,space] '-' -> concat [dah,space] '_' -> longSpace ' ' -> medSpace otherwise -> space makeWavFile :: Data.WAVE.WAVESamples -> IO () makeWavFile xs = putWAVEFile "morse-out.wav" (WAVE header xs) main :: IO () main = do (os, _) <- getArgs >>= myOpts if optUnmorse os then interact morseToString else if optAudio os then do xs <- getContents putStr $ verboseWav os xs stringToMorseWavFile xs if optPlayAudio os then do putStr $ verbosePlay os xs pid <- runCommand "aplay -q morse-out.wav" waitForProcess pid return () else return () else interact stringToMorse where verboseWav os xs = if optVerbose os then stringToMorse xs ++ "Writing to morse-out.wav\n" else "" verbosePlay os xs = if optVerbose os then "Playing morse-out.wav\n" else ""

Pic from wikipedia, courtesy of Lou Sadler, under public domain.


Comments

  • Be respectful. You may want to read the comment guidelines before posting.
  • You can use Markdown syntax to format your comments. You can only use level 5 and 6 headings.
  • You can add class="your language" to code blocks to help highlight.js highlight them correctly.

Privacy note: This form will forward your IP address, user agent and referrer to the Akismet, StopForumSpam and Botscout spam filtering services. I don’t log these details. Those services will. I do log everything you type into the form. Full privacy statement.