ANN: Hemkay, the 100% Haskell MOD player

Previous Topic Next Topic
 
classic Classic list List threaded Threaded
21 messages Options
12
Reply | Threaded
Open this post in threaded view
|

ANN: Hemkay, the 100% Haskell MOD player

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
[hidden email]
http://www.haskell.org/mailman/listinfo/haskell-cafe
Reply | Threaded
Open this post in threaded view
|

Re: ANN: Hemkay, the 100% Haskell MOD player

Peter Verswyvelen-2
Nice work!

Did you try the OpenAL binding? Not sure if that works.


2009/12/14 Patai Gergely <[hidden email]>:

> 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
> [hidden email]
> http://www.haskell.org/mailman/listinfo/haskell-cafe
>
_______________________________________________
Haskell-Cafe mailing list
[hidden email]
http://www.haskell.org/mailman/listinfo/haskell-cafe
Reply | Threaded
Open this post in threaded view
|

Re: ANN: Hemkay, the 100% Haskell MOD player

Patai Gergely
> 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

_______________________________________________
Haskell-Cafe mailing list
[hidden email]
http://www.haskell.org/mailman/listinfo/haskell-cafe
Reply | Threaded
Open this post in threaded view
|

Re: ANN: Hemkay, the 100% Haskell MOD player

Patai Gergely
In reply to this post by Patai Gergely
> The more of these I see, the more guilt I feel over the condition of the
> portaudio module. There's a good chance that the performance issue you're
> seeing is a bad implementation of the portaudio library. Do you have any
> specific problems you ran into with portaudio? I'd love to revisit at
> some point...
Well, when I tried to push whole blocks instead of individual samples,
it seemed to miss some of these writes. It might do the same even now,
but it wouldn't be noticeable with a sample missing here and there
anyway.

The interface for writeStream is not very fortunate. It forces me to
pack samples into lists no matter what, even if the interleaved list is
actually easier to produce. So it should probably provide alternative
interfaces for interleaved lists (which would actually be possible right
away if writeStream didn't ignore its third argument altogether), and
maybe an array interface as well. I don't know if the callback interface
works better, maybe that's also worth a shot.

Ultimately, it would be probably best if it gave the programmer a higher
abstraction, where it is passed a potentially infinite list of samples,
and takes care of all the buffering duties. Also, this could be made to
play nice with stream fusion in order to get the maximum performance out
of it.

Gergely

--
http://www.fastmail.fm - One of many happy users:
  http://www.fastmail.fm/docs/quotes.html

_______________________________________________
Haskell-Cafe mailing list
[hidden email]
http://www.haskell.org/mailman/listinfo/haskell-cafe
Reply | Threaded
Open this post in threaded view
|

Re: ANN: Hemkay, the 100% Haskell MOD player

John Van Enk
It's been well over a year. I'll see what I can do when I get some free(er) time.

2009/12/14 Patai Gergely <[hidden email]>
> The more of these I see, the more guilt I feel over the condition of the
> portaudio module. There's a good chance that the performance issue you're
> seeing is a bad implementation of the portaudio library. Do you have any
> specific problems you ran into with portaudio? I'd love to revisit at
> some point...
Well, when I tried to push whole blocks instead of individual samples,
it seemed to miss some of these writes. It might do the same even now,
but it wouldn't be noticeable with a sample missing here and there
anyway.

The interface for writeStream is not very fortunate. It forces me to
pack samples into lists no matter what, even if the interleaved list is
actually easier to produce. So it should probably provide alternative
interfaces for interleaved lists (which would actually be possible right
away if writeStream didn't ignore its third argument altogether), and
maybe an array interface as well. I don't know if the callback interface
works better, maybe that's also worth a shot.

Ultimately, it would be probably best if it gave the programmer a higher
abstraction, where it is passed a potentially infinite list of samples,
and takes care of all the buffering duties. Also, this could be made to
play nice with stream fusion in order to get the maximum performance out
of it.

Gergely

--
http://www.fastmail.fm - One of many happy users:
 http://www.fastmail.fm/docs/quotes.html

_______________________________________________
Haskell-Cafe mailing list
[hidden email]
http://www.haskell.org/mailman/listinfo/haskell-cafe


_______________________________________________
Haskell-Cafe mailing list
[hidden email]
http://www.haskell.org/mailman/listinfo/haskell-cafe
Reply | Threaded
Open this post in threaded view
|

Re: ANN: Hemkay, the 100% Haskell MOD player

M Xyz
In reply to this post by Patai Gergely


--- On Mon, 12/14/09, M Xyz <[hidden email]> wrote:

From: M Xyz <[hidden email]>
Subject: Re: [Haskell-cafe] ANN: Hemkay, the 100% Haskell MOD player
To: "Patai Gergely" <[hidden email]>
Date: Monday, December 14, 2009, 5:50 PM




--- On Mon, 12/14/09, Patai Gergely <[hidden email]> wrote:
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...

Patai, I asked a similar question about a week ago, inquiring about efficient buffers
for audio. I got a good response:

http://thread.gmane.org/gmane.comp.lang.haskell.cafe/67258/focus=67293

Working with immutable trees instead of arrays still freaks me out, but page 289 of RWH
actually made me feel a little better about it. :)



_______________________________________________
Haskell-Cafe mailing list
[hidden email]
http://www.haskell.org/mailman/listinfo/haskell-cafe
Reply | Threaded
Open this post in threaded view
|

Re: ANN: Hemkay, the 100% Haskell MOD player

Patai Gergely
In reply to this post by Patai Gergely
> Patai, I asked a similar question about a week ago, inquiring about
> efficient buffers for audio. I got a good response:
>
> http://thread.gmane.org/gmane.comp.lang.haskell.cafe/67258/focus=67293
Well, the general lesson seems to be that using stream fusion or
something analogous is the way to go. In this particular case I don't
need any fancy processing, and I have no feedback loops. All I do in my
mixing routine is to calculate a weighted (volume+panning) sum of
samples and stretch them in time as dictated by their frequency. The
song is flattened into a series of play states first, so all the
information required for mixing is readily available.

I could try rewriting the mixer to return an iterative generator
function instead, which could be passed to an appropriate sound
interface. The latter would have to be written once and for all.

Gergely

--
http://www.fastmail.fm - Does exactly what it says on the tin

_______________________________________________
Haskell-Cafe mailing list
[hidden email]
http://www.haskell.org/mailman/listinfo/haskell-cafe
Reply | Threaded
Open this post in threaded view
|

Re: ANN: Hemkay, the 100% Haskell MOD player

Mietek Bąk
Hi Patai,

Your message has motivated me to publish my own PortAudio binding,
which provides a simpler, more efficient callback-based interface:
http://github.com/mietek/portaudio

The documentation is incomplete, but along with the example program,
it should be enough to get you going:
http://github.com/mietek/portaudio/blob/master/examples/Play.hs

Best regards,

--
Mietek Bąk
_______________________________________________
Haskell-Cafe mailing list
[hidden email]
http://www.haskell.org/mailman/listinfo/haskell-cafe
Reply | Threaded
Open this post in threaded view
|

Re: ANN: Hemkay, the 100% Haskell MOD player

Patai Gergely
> The documentation is incomplete, but along with the example program,
> it should be enough to get you going:
> http://github.com/mietek/portaudio/blob/master/examples/Play.hs
Yes, it looks like something worth giving a go. I'll try it soon,
thanks.

Gergely

--
http://www.fastmail.fm - The way an email service should be

_______________________________________________
Haskell-Cafe mailing list
[hidden email]
http://www.haskell.org/mailman/listinfo/haskell-cafe
Reply | Threaded
Open this post in threaded view
|

Re: ANN: Hemkay, the 100% Haskell MOD player

Patai Gergely
In reply to this post by Mietek Bąk
Hello again,

> Your message has motivated me to publish my own PortAudio binding,
> which provides a simpler, more efficient callback-based interface:
> http://github.com/mietek/portaudio
I tried this, and after rewriting the code a bit, I managed to decrease
CPU load by 70-80%, which is not bad for starters. However, I'm getting
random segfaults, and I've no idea why.

Here's the modified player module (hpaste kindly rearranged the empty
lines for some reason):

http://hpaste.org/fastcgi/hpaste.fcgi/view?id=14347

Instead of building a list of samples right away, I use an unfoldr-style
generator to fill the buffer, whose initial state is created by
mixGenerator, and its stepper function is nextSample.

In order to get this working, I renamed your module to avoid conflict
with the other PortAudio binding, and I had to change the dependencies
in the cabal file to base >= 4 && < 5 because of the exception handling
code.

There's also some broken MVar-based code to handle the end of the song,
you can ignore that for the time being.

Gergely

--
http://www.fastmail.fm - The professional email service

_______________________________________________
Haskell-Cafe mailing list
[hidden email]
http://www.haskell.org/mailman/listinfo/haskell-cafe
Reply | Threaded
Open this post in threaded view
|

Re: ANN: Hemkay, the 100% Haskell MOD player

Henning Thielemann
In reply to this post by Patai Gergely

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-Interface-MED-Text.html

> 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/Synthesizer-Storable-Cut.html#v%3Aarrange
_______________________________________________
Haskell-Cafe mailing list
[hidden email]
http://www.haskell.org/mailman/listinfo/haskell-cafe
Reply | Threaded
Open this post in threaded view
|

Re: ANN: Hemkay, the 100% Haskell MOD player

Patai Gergely
>   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-Interface-MED-Text.html
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/Synthesizer-Storable-Cut.html#v%3Aarrange
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

_______________________________________________
Haskell-Cafe mailing list
[hidden email]
http://www.haskell.org/mailman/listinfo/haskell-cafe
Reply | Threaded
Open this post in threaded view
|

Re: ANN: Hemkay, the 100% Haskell MOD player

Henning Thielemann
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/Synthesizer-Storable-Cut.html#v%3Aarrange
> 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.
_______________________________________________
Haskell-Cafe mailing list
[hidden email]
http://www.haskell.org/mailman/listinfo/haskell-cafe
Reply | Threaded
Open this post in threaded view
|

Re: ANN: Hemkay, the 100% Haskell MOD player

Vladimir Zlatanov
> 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.
_______________________________________________
Haskell-Cafe mailing list
[hidden email]
http://www.haskell.org/mailman/listinfo/haskell-cafe
Reply | Threaded
Open this post in threaded view
|

Re: ANN: Hemkay, the 100% Haskell MOD player

Patai Gergely
In reply to this post by Henning Thielemann
> 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

_______________________________________________
Haskell-Cafe mailing list
[hidden email]
http://www.haskell.org/mailman/listinfo/haskell-cafe
Reply | Threaded
Open this post in threaded view
|

Re: ANN: Hemkay, the 100% Haskell MOD player

Patai Gergely
In reply to this post by Peter Verswyvelen-2
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

_______________________________________________
Haskell-Cafe mailing list
[hidden email]
http://www.haskell.org/mailman/listinfo/haskell-cafe
Reply | Threaded
Open this post in threaded view
|

Hackage build failure woes

Patai Gergely
In reply to this post by Peter Verswyvelen-2
> [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

_______________________________________________
Haskell-Cafe mailing list
[hidden email]
http://www.haskell.org/mailman/listinfo/haskell-cafe
Reply | Threaded
Open this post in threaded view
|

Re: ANN: Hemkay, the 100% Haskell MOD player

Henning Thielemann
In reply to this post by Patai Gergely
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.
_______________________________________________
Haskell-Cafe mailing list
[hidden email]
http://www.haskell.org/mailman/listinfo/haskell-cafe
Reply | Threaded
Open this post in threaded view
|

Re: ANN: Hemkay, the 100% Haskell MOD player

Patai Gergely
> 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

_______________________________________________
Haskell-Cafe mailing list
[hidden email]
http://www.haskell.org/mailman/listinfo/haskell-cafe
Reply | Threaded
Open this post in threaded view
|

Re: ANN: Hemkay, the 100% Haskell MOD player

Henning Thielemann
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.

_______________________________________________
Haskell-Cafe mailing list
[hidden email]
http://www.haskell.org/mailman/listinfo/haskell-cafe
12