There’s Something About Code

June 1, 2009

Python audio output

Filed under: Code — Tags: , , , — Knut Eldhuset @ 10:01

Playing MOD files requires outputting digital sound at sample rates proportional to the desired pitch. In the Amiga, this was accomplished by setting the sample rate of the hardware channels. Using high level audio interfaces may require setting a fixed sample rate for the lifetime of the audio channel. Converting the MOD file to a WAV file would also require a fixed sample rate. Thus, sample rate conversion is needed.

The most basic MOD files have four channel sequencing. When played on stereo hardware, two sequencer channels are output to each hardware channel. The sound samples need to be converted from mono to stereo, then mixed together with the other channels.

In the Amiga, the sequencer played a new sample each vertical blanking interval. The screen refresh rate in the PAL version of the computer was 50Hz. I have used the pyglet library to play audio. By subclassing the StreamingSource class and providing an implementation of the _get_audio_data method, the timing of the sequencer takes care of itself automatically. The _get_audio_data method returns an audio chunk equivalent to what the Amiga played per vertical blanking interval. Pyglet will simply request more data when needed.

The code for the MOD player can be found here. The code excerpts below are taken from the file player.py.

The Python Multimedia Services library contains functions for doing the necessary raw audio operations. The audio operations are located in the audioop module. The ratecv function takes care of sample rate conversion:

ratecv(fragment, width, nchannels, inrate, outrate, state[, weightA[, weightB]])

It takes an audio fragment as input, and returns the fragment converted to the desired sample rate, as well as the new state. The new state is passed as input the next time the function is invoked. Here is how it looks in the MOD player:

44
45
46
47
48
49
50
51
52
53
54
55
56
57
    def _ratecv(self, sounds):
        output = []
        for n, (sound, state) in enumerate(zip(sounds, self.ratecv_state)):
            while True:
                o, state = ratecv(sound, self.bytes, 1, 
                              int(round(len(sound) / self.tick_time) / self.bytes), 
                              self.rate, 
                              state)
                #Length may be off by one, so process until OK
                if len(o) == int(round(self.rate * self.tick_time * self.bytes)):
                    break
            output.append(o)
            self.ratecv_state[n] = state
        return output

Self.bytes is the number of bytes per sample. The number of channels is 1, since sound is a mono sample. The inrate will vary according to the pitch at which the sample fragment is played back. This is controlled by the sequencer by varying the length of the fragment. The inrate is then calculated based on the fact that this particular fragment fills self.tick_time seconds. The state is stored in an array for later use.

When mixing several channels into one, one needs to make sure that there will be no clipping of the resulting sound sample. This is done by dividing by the number of channels that are to be mixed:

63
64
   def _scale(self, output):
        return [mul(o, self.bytes, 1.0 / (len(output) / self.channels)) for o in output]

The mul function takes care of the scaling. As all the other audioop functions, it needs to know how many bytes are user per sample.

mul(fragment, width, factor)

The tostereo function takes a mono sample and returns a stereo sample. One can supply scaling factors for each of the left and right channels.

tostereo(fragment, width, lfactor, rfactor)

This is used below to put every even numbered sequencer channel in the left stereo channel, and every odd numbered sequencer channel in the right stereo channel.

66
67
    def _tostereo(self, output):
        return [tostereo(o, self.bytes, n % 2, (n + 1) % 2) for n, o in enumerate(output)]

Putting all this together, the variable sample rate sequencer channels can be transformed into a constant sample rate stereo output:

69
70
71
72
73
74
75
76
77
78
79
80
81
82
    def _get_audio_data(self, num_bytes):
        sound = self.sequencer.tick()
        if sound is None:
            pyglet.app.exit()
            return
        self._mute(sound)
        sound = [lin2lin(s, 1, self.bytes) for s in sound]
        output = self._ratecv(sound)
        output = self._scale(output)
        output = self._tostereo(output)
        stereo = mix(output, self.bytes)
        audio = AudioData(stereo, self.audio_length, self.timestamp, self.tick_time)
        self.timestamp += self.tick_time
        return audio

For a standard MOD file, the sequencer returns 4 samples per tick. These are converted from 8 bit to 16 bit using the lin2lin function:

lin2lin(fragment, width, newwidth)

The samples are then rate converted, scaled and converted to stereo. The mix function is a helper function to mix a list of samples. The audioop module only provides an add function to mix two channels, so I made the helper function to mix an arbitrary number of channels. The method ends with creating an AudioData object with the parameters needed for pyglet to play the sound.

May 16, 2009

GLiPy – The OpenGL IPython Terminal

Filed under: Tools — Tags: , , , — Knut Eldhuset @ 12:23

For those of you that use the standard Python shell when trying out stuff, IPython is an enhanced shell with lots of useful features, including tab completion and command history across sessions. GLiPy is an enhancement over IPython. It provides plotting of numpy structures using OpenGL, as shown below.GLiPy and numpy
A side effect of the graphical nature of GLiPy and the fact that it has a GUI, is that it has a message pump going. This means you can experiment with Wx or pyglet without starting additional threads to get an application object up and running. The short example below shows me loading the Sine source from pyglet and playing a short sine wave.GLiPy and pyglet
The difference between running these three lines in GLiPy and IPython, is that the sound plays for 4 seconds when run in GLiPy, while using IPython you only get a short beep. The reason for this is that there is no timer set up to get the next chunk of data from the sine source. To get the rest of the sine wave, you have to do pyglet.app.run() which then blocks your main thread.

Powered by WordPress