ANN: Hemkay, the 100% Haskell MOD player

Hello all, I just uploaded the fruit of a little side project. Hemkay [1] is an oldschool module music [2] player that performs all the hard work in Haskell. If there was any goal, it was to express the transformation from the song structure to the output of the mixer as a series of function compositions, maintaining a style that one might call idiomatic Haskell. Considering the dirtiness of the format in question, I'm quite pleased with the initial version. Still, I'd be curious to see how the overall quality of the code could be improved. In particular, retrieving and updating record fields is somewhat inconvenient. Also, the actual mixing (limited to the mixChunk function) is embarrassingly slow, and I wonder how much it could be improved without leaving the pure world. The program uses Portaudio for playback, but that might easily change in the future. The problem is that I couldn't get sound to work smoothly when producing samples in batches, so I'm sending them off one by one (!) at the moment, which doesn't help with performance either. I'm open to suggestions as to what library to use to push data to the sound card. Gergely [1] http://hackage.haskell.org/package/hemkay [2] http://en.wikipedia.org/wiki/MOD_(file_format) -- http://www.fastmail.fm - Or how I learned to stop worrying and love email again

Nice work!
Did you try the OpenAL binding? Not sure if that works.
2009/12/14 Patai Gergely
Hello all,
I just uploaded the fruit of a little side project. Hemkay [1] is an oldschool module music [2] player that performs all the hard work in Haskell. If there was any goal, it was to express the transformation from the song structure to the output of the mixer as a series of function compositions, maintaining a style that one might call idiomatic Haskell. Considering the dirtiness of the format in question, I'm quite pleased with the initial version.
Still, I'd be curious to see how the overall quality of the code could be improved. In particular, retrieving and updating record fields is somewhat inconvenient. Also, the actual mixing (limited to the mixChunk function) is embarrassingly slow, and I wonder how much it could be improved without leaving the pure world.
The program uses Portaudio for playback, but that might easily change in the future. The problem is that I couldn't get sound to work smoothly when producing samples in batches, so I'm sending them off one by one (!) at the moment, which doesn't help with performance either. I'm open to suggestions as to what library to use to push data to the sound card.
Gergely
[1] http://hackage.haskell.org/package/hemkay [2] http://en.wikipedia.org/wiki/MOD_(file_format)
-- http://www.fastmail.fm - Or how I learned to stop worrying and love email again
_______________________________________________ Haskell-Cafe mailing list Haskell-Cafe@haskell.org http://www.haskell.org/mailman/listinfo/haskell-cafe

Nice work! Thanks! :)
Did you try the OpenAL binding? Not sure if that works. No, I haven't really looked hard, to be honest. Portaudio looked simple enough, so I picked it. But it could be anything, since the mixing is done on my side anyway, and all I need is a way to push (preferably Float) samples to the sound unit. I'm sure there are several viable options, so the deciding factor is rather portability, and secondly ease of use.
-- http://www.fastmail.fm - The professional email service

Hello all, I did some refactoring, and separated the device independent part of Hemkay into a package of its own, hemkay-core [1]. This is a library that provides facilities to load MOD music and render it in various ways, including a direct-to-buffer option that's considerably more efficient than creating a list of samples. You can use it to create a MOD player with any sound-making API or even write the mixer output into a wav file. The hemkay package [2] is now reduced to an example that shows how to use the core library with PortAudio. It is also considerably faster than the previous version, because now I push as many samples as possible at a time without blocking. I had no luck with Mietek's callback interface so far, because it keeps randomly segfaulting on me, but when it works, it's about four times as fast as the current player. You are encouraged to try adapting it (using mixToBuffer) to other systems. Gergely [1] http://hackage.haskell.org/package/hemkay-core [2] http://hackage.haskell.org/package/hemkay -- http://www.fastmail.fm - Choose from over 50 domains or use your own

[1] http://hackage.haskell.org/package/hemkay-core [2] http://hackage.haskell.org/package/hemkay
Hmm, it's a pity that they don't build on Hackage due to a conflict between two bytestring versions -- the current and the one binary was compiled with, I assume. It would be nice if at least the haddock docs could be generated when this happens. Gergely -- http://www.fastmail.fm - Access all of your messages and folders wherever you are

On Mon, 14 Dec 2009, Patai Gergely wrote:
Hello all,
I just uploaded the fruit of a little side project. Hemkay [1] is an oldschool module music [2] player that performs all the hard work in Haskell.
Cool. The most complicated I tried was to import OctaMED printout to Haskore: http://darcs.haskell.org/haskore/src/Haskore/Interface/MED/Text.hs http://hackage.haskell.org/packages/archive/haskore/0.1/doc/html/Haskore-Int...
Still, I'd be curious to see how the overall quality of the code could be improved. In particular, retrieving and updating record fields is somewhat inconvenient. Also, the actual mixing (limited to the mixChunk function) is embarrassingly slow, and I wonder how much it could be improved without leaving the pure world.
I have a function for mixing sounds at different (relative) start times. I feel that it does not get maximum speed in GHC, but is still ready for realtime application. http://hackage.haskell.org/packages/archive/synthesizer-core/0.2.1/doc/html/...

The most complicated I tried was to import OctaMED printout to Haskore: http://darcs.haskell.org/haskore/src/Haskore/Interface/MED/Text.hs http://hackage.haskell.org/packages/archive/haskore/0.1/doc/html/Haskore-Int... Oh, I'll have to try that too!
I have a function for mixing sounds at different (relative) start times. I feel that it does not get maximum speed in GHC, but is still ready for realtime application. http://hackage.haskell.org/packages/archive/synthesizer-core/0.2.1/doc/html/... And how can you mix that with changing frequencies (effectively resampling on the fly)?
By the way, the latest code I have in my hands typically uses 2-3% CPU time on my machine (Core 2 Duo at 2 GHz), which is okay for now, but I think going at least ten times as fast is a realistic goal. Gergely -- http://www.fastmail.fm - Accessible with your email software or over the web

Patai Gergely schrieb:
I have a function for mixing sounds at different (relative) start times. I feel that it does not get maximum speed in GHC, but is still ready for realtime application. http://hackage.haskell.org/packages/archive/synthesizer-core/0.2.1/doc/html/... And how can you mix that with changing frequencies (effectively resampling on the fly)?
I would do resampling (with some of the Interpolation routines) and mixing in two steps, that is I would prepare (lazy) storable vectors with the resampled sounds and mix them. Since Haskell is lazy, this is still somehow "on the fly", although one could still wish to eliminate the interim storable vectors.

I would do resampling (with some of the Interpolation routines) and mixing in two steps, that is I would prepare (lazy) storable vectors with the resampled sounds and mix them. Since Haskell is lazy, this is still somehow "on the fly", although one could still wish to eliminate the interim storable vectors.
You could use stream fusion, although you will need to adapt that for the interpolation, but it should work.

I would do resampling (with some of the Interpolation routines) and mixing in two steps, that is I would prepare (lazy) storable vectors with the resampled sounds and mix them. And is that straightforward considering the peculiarities of tracked music? After all, frequency can change between ticks thanks to portamento effects, and samples can loop or end in the middle of a tick. Do I have to trim and pad the samples manually to be able to describe these transformations?
-- http://www.fastmail.fm - The way an email service should be

Patai Gergely schrieb:
I would do resampling (with some of the Interpolation routines) and mixing in two steps, that is I would prepare (lazy) storable vectors with the resampled sounds and mix them.
And is that straightforward considering the peculiarities of tracked music? After all, frequency can change between ticks thanks to portamento effects, and samples can loop or end in the middle of a tick. Do I have to trim and pad the samples manually to be able to describe these transformations?
I see no problem. I would generate a frequency and a volume control curve for each channel and apply this to the played instrument, then I would mix it. It is the strength of Haskell to separate everything into logical steps and let laziness do things simultaneously. Stream fusion can eliminate interim lists, and final conversion to storable vector using http://hackage.haskell.org/package/storablevector-streamfusion/ can eliminate lists at all.

I see no problem. I would generate a frequency and a volume control curve for each channel and apply this to the played instrument, then I would mix it. Yes, that's basically what I do now: I flatten the song into a series of play states where for each active channel I store the pointer to the current sample, its frequency, volume and panning (none of these three parameters change within a chunk), and use that information to perform mixing. The mixing step is quite ad hoc, but at least it's simple enough, so it doesn't get out of hand.
It is the strength of Haskell to separate everything into logical steps and let laziness do things simultaneously. Stream fusion can eliminate interim lists, and final conversion to storable vector using http://hackage.haskell.org/package/storablevector-streamfusion/ can eliminate lists at all. But in my understanding that elimination is only possible if lists are not used as persistent containers, only to mimic control structures. Now I rely on samples being stored as lists, so I can represent looping samples with infinite lists and not worry about the wrap-around at all. So in order to have any chance for fusion I'd have to store samples as vectors and wrap them in some kind of unfold mechanism to turn them into lists that can be potentially fused away. In other words, besides a 'good consumer', I need a 'good producer' too.
However, there seems to be a conflict between the nature of mixing and stream processing when it comes to efficiency. As it turns out, it's more efficient to process channels one by one within a chunk instead of producing samples one by one. It takes a lot less context switching to first generate the output of channel 1, then generate channel 2 (and simultaneously add it to the mix) and so on, than to mix sample 1 of all channels, then sample 2 etc., since we can write much tighter loops when we only deal with one channel at a time. On the other hand, stream fusion is naturally fit to generate samples one by one. It looks like the general solution requires a fusable transpose operation, otherwise we're back to hand-coding the mixer. Have you found a satisfying solution to this problem? Gergely -- http://www.fastmail.fm - A no graphics, no pop-ups email service

Patai Gergely schrieb:
It is the strength of Haskell to separate everything into logical steps and let laziness do things simultaneously. Stream fusion can eliminate interim lists, and final conversion to storable vector using http://hackage.haskell.org/package/storablevector-streamfusion/ can eliminate lists at all.
But in my understanding that elimination is only possible if lists are not used as persistent containers, only to mimic control structures. Now I rely on samples being stored as lists, so I can represent looping samples with infinite lists and not worry about the wrap-around at all. So in order to have any chance for fusion I'd have to store samples as vectors and wrap them in some kind of unfold mechanism to turn them into lists that can be potentially fused away. In other words, besides a 'good consumer', I need a 'good producer' too.
Right. The conversion from storablevector to stream-fusion:Stream is such a good producer.
However, there seems to be a conflict between the nature of mixing and stream processing when it comes to efficiency. As it turns out, it's more efficient to process channels one by one within a chunk instead of producing samples one by one. It takes a lot less context switching to first generate the output of channel 1, then generate channel 2 (and simultaneously add it to the mix) and so on, than to mix sample 1 of all channels, then sample 2 etc., since we can write much tighter loops when we only deal with one channel at a time.
Yes, I would also do it this way. So in the end you will have some storablevectors as intermediate data structures.

However, there seems to be a conflict between the nature of mixing and stream processing when it comes to efficiency. As it turns out, it's more efficient to process channels one by one within a chunk instead of producing samples one by one. It takes a lot less context switching to first generate the output of channel 1, then generate channel 2 (and simultaneously add it to the mix) and so on, than to mix sample 1 of all channels, then sample 2 etc., since we can write much tighter loops when we only deal with one channel at a time. On the other hand, stream fusion is naturally fit to generate samples one by one. It looks like the general solution requires a fusable transpose operation, otherwise we're back to hand-coding the mixer. Have you found a satisfying solution to this problem?
I wonder if data-parallel haskell won't be able to help here, mod rendering is a scatter-gather style of processing, the problem is that the different channels trigger different processing.
participants (4)
-
Henning Thielemann
-
Patai Gergely
-
Peter Verswyvelen
-
Vladimir Zlatanov