Digital signal processing, audio processing and the like are all rather complex topics of study. I have a personal interest in these fields as I try to create guitar processing effects from time to time. Today’s post is all about taking the first steps in getting our hands on some audio and associated information from within Haskell.
hsndfile
For today’s post, I’ll be using the library hsndfile to do all of the heavy lifting as far as opening audio files and interpreting information. The files that we’ll work with will need to be in wave format. The demonstration in this post will simply open an audio file, read some information about the file and then close the file.
Project setup
I’ve created a Haskell project using cabal so that I can manage the hsndfile dependency locally to this application. You may already have this installed globally on your system, but if you follow along here, you should have it installed to your project in not time.
Just select all of the defaults when setting up your project (well, that’s what I did, anyway). We need to add hsndfile as a dependency to our project, so we’ll specify this in our sndtest.cabal file. Open it up and make sure that your build-depends reads as follows.
build-depends: base ==4.5.*,
hsndfile ==0.5.3
Of course, you may have some different version of base, but here’s where I was at anyway. Create a new file in your project called Test.hs. We’ll now fill out this file with the code that will open a file, read its information, close the file and then display the information to screen.
moduleMainwhereimportSound.File.SndfileasSFmain::IO()main=do-- open the file that we want to know aboutf<-SF.openFile"test.wav"SF.ReadModeSF.defaultInfo-- read the information about the file outletinfo=SF.hInfof-- close the fileSF.hClosef-- display information about the fileputStrLn$"format: "++(show$SF.formatinfo)putStrLn$"sample rate: "++(show$SF.samplerateinfo)putStrLn$"channels: "++(show$SF.channelsinfo)putStrLn$"frames: "++(show$SF.framesinfo)
This is pretty straight forward. First up, we import Sound.File.SndFile qualified as SF so we know when we’re using something from this import. Dissecting the main function, firstly we open the file using openFile. This function expects the path to the audio file (in this case we’re using “test.wav” which by the way you’ll have to find something), we’re only reading from the file at the moment so we specify ReadMode and finally we have the info parameter which is useful to us when we’re writing a new file (so we can tell it what format to write in, etc), but for reading we just use defaultInfo.
We now read the stream information about the file using hInfo, the result of which will give us back a value of type Info. This info packet tells us the number of frames in the file, the sample rate, number of channels, header and sample format, number of sections and if the file is seekable or not.
Now that we have the information from the stream, we can close it off. We do this with hClose. Now we can interrogate the Info value with a series of print statements. We’ve got a module ready to run, but we need to tell our project that it’s the entry point to run. In the sndtest.cabal file, make sure you set your main-is: attribute like so.
main-is: Test.hs
Build and Run<
We’ve created our project and finished our code. Let’s build and run the application. The first build is going to take a bit more time as cabal-dev will need to resolve all of the dependencies that it doesn’t yet have. Get this process moving with the following command.
$ cabal-dev install
All going well, you should be able to launch your executable and check out the results:
Haskell goes to great lengths to control state but one way you can achieve mutable state in Haskell is by use of IORef. IORef gives you the ability to assign a reference to a variable in the IO monad. This at least decorates your code in such a way that it’s obvious to you, the developer and Haskell that there will be side effects. Today’s post, I’ll create a very simple example usage of IORef. We’ll construct a counter that we can increment and decrement.
Declaring our type
importData.IORefdataCounter=Counter{x::IORefInt}
First of all, we import Data.IORef to give us access to IORef. We declare our counter data type using record style, the only member of which is the value that counts. It’s an IORef Int to mean it references a variable in the IO monad that will be of type Int. So, it’s not so blatant that you’re dragging the integer value around with you, rather you’re dragging something closer to a pointer to the value or reference. To build one of our types, we need to use newIORef which references our actual value and we wrap it up in our Counter data type.
makeCounter takes in an initial integer that will seed our counter and returns a Counter in the IO monad. Getting our hands on the reference and doing something with it is pretty simple with the use of modifyIORef. Using this information, we can increment our counter with the following function.
modifyIORef actually gives us the ability to pass a function to modify the referenced value. Be careful with modifyIORef though. As with a lot of things in Haskell, this is lazy. We’re operating on IO actions here so it’s all “promises to do something” or “will do it later when I need to” type operations, so repeatedly calling this without emitting the value will make these promises pile up. There is a strict and non-lazy evaluated version called modifyIORef'. Finally, when we want to get our hands on the referenced value and do something with it (in our example here, we’ll just present it to screen) we use readIORef. readIORef will take our IORef and just made it a value in the IO monad meaning we can simply use <- to emit the value.
In the previous post, we had setup a basic array data type to act as a very big number. We implemented an initialise, increment and decrement function. Today we’ll extend on this library by adding some zero testing. It is of interest to us when our numbers are zero and when they aren’t.
Scanning our number
The plan of attack is to use a scan string quad-word scasq. We’re going to enumerate our array, testing each block for zero. If we make it to the end of the array, we have a zero number. If our scanning got interrupted by a non-zero number, our overall number couldn’t possibly be zero.
Here’s the code.
; --------------------------------------------; bignum_is_zero ; determines if a big number is zero ; ; input ; rsi - address of the number ; --------------------------------------------_bignum_is_zero:pushrcx; save off any registers that pushrbx; we'll use in this functionpushrdimovrdi,rsi; setup the destination pointer; for the scan operationmovrcx,bignum_size; the number of blocks that we'll scanxorrax,rax; the value that we'll be scanning for repescasq; while we're still finding the value in; rax at [rdi], keep scanningcmprcx,0; if we exhausted our counter at the end; of the scan, we have a zero numberje_bignum_is_zero_yes; jump to a true state xorrax,rax; rax = 0 = false, non-zero jmp_bignum_is_zero_done_bignum_is_zero_yes:movrax,1; rax = 1 = true, zero _bignum_is_zero_done:poprdi; restore our saved registers poprbxpoprcxret
Now that we’ve got this function, we can wrap it up with a print so we can visually see if a number is zero or not. This will also give you basic usage of the function above.
print_zeroness:; define the strings that we'll show to screendefszzero_msg,"Thenumberwaszero"defsznon_zero_msg,"Thenumberwasnotzero"; test the number that's at [esi]call_bignum_is_zero; check if it "is zero", cmprax,1; jump over if it wasn'tjneprint_zeroness_not_zero; print the "was zero" messageprintzero_msgjmpprint_zeroness_doneprint_zeroness_not_zero:; print the "wasn't zero" messageprintnon_zero_msgprint_zeroness_done:ret
In action
Now that we’ve got a little helper function to wrap up our is-zeroness and reporting on this to the console, we’re free to test it out as follows.
movrdi,num_a; zero out our number call_bignum_zeromovrsi,num_a; test if it is zerocallprint_zeronessmovrdi,num_a; increment our numbercall_bignum_incmovrsi,num_a; re-test if it's zerocallprint_zeroness
To which we end up with a result looking like this.
$ bigmath ./bigmath
The number was zeroThe number was not zero
As expected. Initially the number was zero, after incrementation is was not.
There’s some basic logic testing for your big number.
Following on from my previous posts about functors, applicative functors and applying applicative it would only be natural for me to post a follow up on the topic of Monads. Monads take shape very similarly to applicative functors so for this post to make sense, I suggest you read the previous articles so that you have a chance to follow along.
What is a Monad?
At a code level, a Monad is a type that is an instance of the Monad type class. At a concept level a Monad allows you to provide a function that is context-unaware to operate over values that are wrapped in a context. Immediately, you’d think that this is weaker functionality that what was defined for applicative functors - and you’d be right in thinking so, but Monads do provide a very natural way to write code against our types. Here is the type definition for a Monad.
Moving through all of the defined functions, you first see return. return is the same as pure which we defined when making an applicative functor. return’s role in this type is to lift a value into a context. The next function you see is >>= which is pronounced “bind”. This is the major difference right here. >>= is what takes a value wrapped in a context m a and takes a function with a parameter of a returning a wrapped b as m b, with the function returning a b in a context m b. The remaining two operations are rarely delved into as their default implementations suffice majority of scenarios.
Laws
There are some laws that need to be abided by when implementing Monads. Left Identity suggests that
x >>= f is the same thing as f x
Right Identity suggests that
m >>= return is the same as m
Associativity suggests that
(m >>= f) >>= g is the same as m >>= (\x -> f x >>= g)
Such that it shouldn’t matter how these calls are nested together.
Custom context
Applying this knowledge to our rather useless example “CustomContext” discussed here, we can make this type a monad with the following definition.
Ok, so - as prophesied return is doing the same thing as pure did for us - it’s just lifting a plain value into our context. >>= or “bind” on the other hand is taking the value out of its context and applying a function to it. We can check out our bind implementation in action with the following simple examples.
Our value is having a function applied to it. Values are getting de-contexted, worked-on than re-contexted. Pretty straight forward. To show you how >>= can work for us, I’ve created a function that will accumulate even numbers as they are received into it. If an odd number is encountered, the accumulated total get wiped back to zero until conditions are met to have another two consecutive calls with even numbers. Here’s how the function looks.
Using guards, we filter out odd numbers sending the result back to zero otherwise we continue to accumulate. Note how the inputs are just plain integers but the output is an integer wrapped in one of our CustomContext types. With the use of >>= the following calls are possible.
First of all, we’re freely using wrapped and un-wrapped contexts thanks to that bind operator. More interestingly (and demonstrative to our purposes here), the last example appears to be in error, but it’s really not. Think about it this way.
So you can see here that the next sub-sequent call to accumEven would finish with a 0 (zero) value as the 13 would be carried onto the next call and would be tested for evenness. Even if this isn’t the best example, it still demonstrates how bind is sending information between the calls.
do Notation
Another nicety when working with Monads is do notation. do notation allows you to clean up a lot of the boilerplate that builds up around the functionality you write when working with monadic values. Here’s a very simple function that builds a CustomContext from two other CustomContexts by adding them.
You can see how things start to get a bit gnarly once you chain more calls together. Using do notation, this gets cleaned up pretty well. The above can be re-written like so.
That’s so much easier to read! There’s do notation at work for you, anyway. Well, that’s all there is for Monads right now. As soon as I try something useful with them, I’ll post a follow-up article of Monads in Application.