Friday, January 3, 2014

Learning to drive the Flex 6700 programmatically

I now have several days of hacking under my belt and have made some nice progress in learning the ropes and getting the radio to use useful things.

As I intimated previously, I am really impressed with what Flex have built (...and indeed are continuing to build).  The 6x00 is a unique radio and it has been very much fun indeed to get acquainted with the programmatic interface of the radio as an appliance over the ethernet as well as enjoying operating it with the SmartSDR Windows software.

My initial goals have been to get a basic grasp of the control functions and data streams over ethernet.  Flex have a nicely designed set of control sentences that are constructed and sent to the radio once you are connected.  Before connecting, you can discover radios on your LAN via a periodic broadcast  that each radio makes once it is initialized and ready for client connections.

To facilitate the usual business of sending command and reconciling responses with those commands I have built a blocking command queue in Haskell.  This handles boring stuff like allocating monotonically ascending message IDs and also simplifies internal addressing of radios so that any connected radio is henceforth addressed by serial number.  Incoming traffic (which can be command responses as well as asynchronous status messages) are immediately delivered into different queues from which various handlers can subscribe to consume them.  Calls to send commands to the radio are handled through a central dispatcher that blocks on the specific response becoming available in the responses queue.  Haskell makes this sort of thing rather easy and completely thread-safe through the use of the STM monad, which implements software transitional memory.  The STM library has a range of useful concurrency tools, amongst which is the TQueue (transactional queue) that I use for all the received message queuing.

Haskell also handles threading beautifully - and it's fast and very flexible.  It runs its own scheduler that allocates code to be executed on available hardware threads.  Even if you restrict Haskell to a single thread, it will still do a great job of cooperatively multitasking between concurrent routines in your program... even when they are doing IO.

Importantly, Haskell is also cross-platform (at least for most platforms people care about: Mac, Windows, Linux).  The core of Haskell runs on many other processors and platforms (such as ARM/ Raspberry Pi) but of course that doesn't mean that all the libraries I need would work beyond the core desktop platforms.

Once the command dispatch mechanism was working I added some convenience wrappers around a number of the available command sentences, including:
  • Getting version numbers (a simple initial command to test things are working)
  • Querying for active receiver slices
  • Creating and deleting receiver slices
  • Tuning a receiver slice
  • Requesting an audio stream (from a slice)

An early objective of my experimentation was to get streaming audio to be played on my computer and in preparation for that I have had to survey the available cross-platform audio libraries.  I've had experience with Apple's excellent CoreAudio before, but I've never tried to use a cross-platform library.  It turns out there were several to choose from: PortAudio, FxModRaw, PulseAudio, OpenAL and a few others I didn't recognize.  The main feature I needed was a 'simple' streaming PCM playback and for the future also audio-in.  In practice things are a little messy with these libraries with several of them being reliant on versions of a binary dependency that I found difficult to compile on the Mac at least ("difficult" meaning having to hack build files to get things working).  The OpenAL library, despite its age - or perhaps, because of it - was very easy to get set up.  The only problem with OpenAL is that it isn't really a streaming engine per se.  Rather, it provides a basic mechanism to queue buffers sequentially for playback.  It is left as an exercise for the consumer to set up whatever higher-level sample management mechanism they may deem appropriate.  Because of this, I had to set about writing a buffer management component that asynchronously accepts samples from a source and queues filled buffers to playback (retrieving used buffers once OpenAL has finished with them).

Once this little audio engine was completed I turned my attention to the VITA stream format.  Unsurprisingly, there is no off-the-shelf decoder for VITA-49 standard streaming in the Haskell package repository 'hackage'.  This has meant rolling my own from specifications that I have found online.  Haskell has some great tools for marshalling binary data too.  Specifically, the ByteString and Binary packages provide high-performance processing of data packets/streams.  The Get and BitGet monads in the Binary package make building bitwise decoders very easy.  As well as the VITA packet decoding, I have used a Get decoder to prepare the audio sample stream into an appropriate format for OpenAL (which currently only supports int16 samples).  Here's the code:

case decodeVRTIFPacket msgBytes of
    Left err -> errorM logComms $ "Could not decode VRT IF packet because: " ++ err
    Right VRTIFPacket{..} -> do
        -- Current stream payload is alternate L-R stereo pairs of IEEE-754 32-bit floating point samples.
        -- These are delivered at 24000 L-R samples/s 
-- We need to read Word32 samples from the packet, convert these to a float, average them and then 
-- convert these again to the dynamic range of an Int16 for sending to OpenAL
let decodeStereoPairsToMonoInt16 :: Get Int16
            decodeStereoPairsToMonoInt16 = do
               left <- getWord32be >>= return . wordToFloat
                right <- getWord32be >>= return . wordToFloat
                let mono = round $ (left + right / 2) * fromIntegral (maxBound :: Int16)
                return mono
            allPairs = whileM (fmap not isEmpty) decodeStereoPairsToMonoInt16
        (!result, _) = runGet allPairs vp_data
        case result of
           Left !err -> putStrLn $ "Error decoding audio data: " ++ err
           Right !monoSamples -> putMVar samples monoSamples -- Send to audio engine

Even if you are not familiar with Haskell, you can probably grok was is going on here.
A VRT (VITA-49) packet is decoded and if that succeeds then another decoder is run on its "vp_data" component.  This decoder is mostly composed of the "decodeStereoPairsToMonoInt16" function, which lives in the Get monad as you can see from the "Get Int16" return type.  That function only handles a single left-right pair of samples - by reading them from the byte stream as 32 bit words in network order and converting each to a Haskell Float.  These two values are then averaged, scaled to the dynamic range of an Int16 and then returned as a single mono sample.  To perform this treatment on all the bytes in the stream ("allPairs"), the monadic loop function 'whileM' is used to apply this decoder to the stream until it is "not isEmpty" (i.e. empty).  The runGet function actually performs the Get action on the stream, returning the result (the stream of mono samples if all is well).  Assuming the mono samples are indeed returned, then these are handed to the sound engine by filling an 'MVar' with "putMVar samples mono samples".  MVars are another Haskell concurrency feature that implement a kind of threadsafe pigeon-hole where values can be 'put' and 'taken' by different threads with put blocking if the pigeon-hole is still full and take blocking if it is still empty.  The buffer-filler of the audio engine uses this mechanism to asynchronously take any number of proffered samples and ready these for playback.  

With the basics of control and audio working I will now turn my attention to some UI.
I will have a similar question as to which cross-platform UI library to use.  In previous experiments I have looked at GTK and wxHaskell.  Most of my actual dev time on Haskell GUI projects has been with GTK.  This works OK on a Mac with X as a dependency... although in theory you can build a gtk library that works directly against Cocoa and therefore removes that issue.  I have no idea whether GTK works nicely on Windows at this time.  wxHaskell might be a better choice on this occasion, so I'll spend a little time trying to figure out where that library stands (the underlying wxWidgets have just been updated to v3).

1 comment:

  1. Great post! Thank you for all the detail. Hope to have my 6x00 next month.