Thursday, July 19, 2018

pygame 1.9.4 released

pygame 1.9.4
pygame 1.9.4 has been released into the wild!

TLDR; Some highlights.

  • python 3.7 support.
  • beta pypy support. See Are we pypy yet?.
  • pygame.draw fixes
  • pygame.math is not experimental anymore. Speedups and bugfixes.
  • Debian, Mac homebrew, mac virtualenv, manylinux and other platform fixes.
  • documentation fixes, jedi support for type ahead in editors like VSCode and VIM.
  • Surface.blits for blitting many surfaces at once more quickly.

Thanks

A very special thanks to the people who have volunteered commits to pygame since the last release. In alphabetical order...
Adam Di Carlo (@adicarlo) | Christian Bender (@christianbender) | Don Kirkby (@donkirkby) | endolith (@endolith) | hjpotter92 (@hjpotter92) | Ian Mallett (@imallett) | Lenard Lindstrom (@llindstrom) | Mathias Weber (@mweb) | Matti Picus (@mattip) | Nicholas Tollervey (@ntoll) | (@orangudan) | Raymon Skjørten Hansen (@raymonshansen) | René Dudfield (@illume) | Stefan Bethge (@kjyv) | Stuart Axon (@stuaxo) | Thomas Kluyver (@takluyver) | Tobias Persson (@Anisa)

I'm probably missing some people, and also missing some people who contributed in other ways.
For example, in discussions, issue reports, helping out on the wiki, the website, and for helping others
in the community, and providing good vibes. So whilst the commits are easy to use to make a list of people to thank, it's not inclusive of everyone who deserves thanks.

More details.

#451 #460 #467 #468 #469 #470
#444 link to help pages when compile fails.
#443 In set_error get_error tests ignore first error. Could be anything.
#442 Freetype requires pkg-config instead of freetype-config now.
#439 Surface.blits
#435 Adding pypy builds for Mac on travis.
#432 Appveyor pypy and pypy3 windows 32bit.
#431 Implement object alloc caching for rect.c to improve on pypy.
#427 PixelArray.close(), with PixelArray(surf) as px, context manager.
#426 Skip tests that rely on arrinter and pythonapi on pypy.
#420 pypy didn't like tp_dictoffset hack in events. Make our own setter, getter.
#418 draw.aaline should work with ARGB surfaces (like on mac).
#416 Vector cleanup
#415 So virtualenv gets a focused window on Mac too.
#414 Mac Travis homebrew fix
#413 Jedi confused by pygame imports. Make it happy.
#408 pygame.transform.threshold tests, keyword arguments, docs.
#403 pygame.math.Vector2/3 not experimental
#398 Clean up _camera_vidcapture.py unused code, and document a bit.
#394 Add pitch bend to MIDI library
#392 Add pypy builder to travis ci, and allow it to fail.
#391 ppc64le and other Debian fixes
#389 pygame.draw.circle with a thickness had a weird moiré pattern.
#387 test python 3.7 on travis CI.
#386 python 3.7 fixes.
#384 pygame.display doc fixes.
#381 import rect.inflate docs.
#363 Fix several typos, and improve grammar in the introduction.
#361 Add unit test for some key functions.
#360 update math.c for pypy.
#357 add UYVY support for better linux camera support.
#356 Fix aaellipse artifacts
703350f Update Rect slicing for Python 3
6d0e97a bug fix for freetype.Font.render_to()
#78 Add environment PYGAME_EXTRA_BASE to add an extra base directory to the start of the search path.
#77 Build alsa libs ourselves for manylinux builds.
#76 Docs fixup.

Wednesday, July 18, 2018

Draft of, ^Let's write a unit test!^

(BeginDraft)

So, I started writing this for people who want to 'contribute' to Free Libre and Open source projects.
It's not finished yet, but still useful, and I'd like a bit of feedback, and to start linking to it from the pygame developer docs. So there.
(/EndDraft)

A unit test is a piece of code which tests one thing works well in isolation from other parts of software. In this guide, I'm going to explain how to write one using the standard python unittest module, for the pygame game library. You can apply this advice to most python projects, or free/libre open source projects in general.

A minimal test.

What pygame.draw.ellipse should do: http://www.pygame.org/docs/ref/draw.html#pygame.draw.ellipse
Where to put the test: https://github.com/pygame/pygame/blob/master/test/draw_test.py

def test_ellipse(self):
    import pygame.draw
    surf = pygame.Surface((320, 200))
    pygame.draw.ellipse(surf, (255, 0, 0), (10, 10, 25, 20))

All the test does is call the draw function on the surface with a color, and a rectangle. That's it. A minimal, useful test. If you have a github account, you can even edit the test file in the browser to submit your PR. If you have email, or internet access you can email me or someone else on the internet and ask them to do add it to pygame.

But why write a unit test anyway?

Unit tests help pygame make sure things don't break on multiple platforms. When your code is running on dozens of CPUs and just as many operating systems things get a little tricky to test manually. So we write a unit test and let all the build robots do that work for us.

A great way to contribute to libre/free and open source projects is to contribute a test. Less bugs in the library means less bugs in your own code. Additionally, you get some public credit for your contribution.

The best part about it, is that it's a great way to learn python, and about the thing you are testing. Want to know how graphics algorithms should work, in lots of detail? Start writing tests for them.
The simplest test is to just call the function. Just calling it is a great first test. Easy, and useful.

At the time of writing there are 39 functions that aren't even called when running the pygame tests. Why not join me on this adventure?


Let's write a unit test!

In this guide I'm going to write a test for an pygame.draw.ellipse to make sure a thick circle has the correct colors in it, and not lots of black spots. There's a bunch of tips and tricks to help you along your way. Whilst you can just edit a test in your web browser, and submit a PR, it might be more comfortable to do it in your normal development environment.

Grab a fork, and let's dig in.

Set up git for github if you haven't already. Then you'll want to 'fork' pygame on https://github.com/pygame/pygame so you have your own local copy.
Note, we also accept patches by email, or on github issues. So you can skip all this github business if you want to. https://www.pygame.org/wiki/patchesandbugs
  • Fork the repository (see top right of the pygame repo page)
  • Make the change locally. Push to your copy of the fork.
  • Submit a pull request
So you've forked the repo, and now you can clone your own copy of the git repo locally.

$ git clone https://github.com/YOUR-USERNAME/pygame
$ cd pygame/
$ python test/draw_test.py 
...
----------------------------------------------------------------------
Ran 3 tests in 0.007s

OK

You'll see all of the tests in the test/ folder.

Browse the test folder online: https://github.com/pygame/pygame/tree/master/test


If you have an older version of pygame, you can use this little program to see the issue.


There is some more extensive documentation in the test/README file. Including on how to write a test that requires manual interaction.


Standard unittest module.

pygame uses the standard python unittest module. With a few enhancements to make it nicer for developing C code.
Fun fact: pygame included the unit testing module before python did.
We will go over the basics in this guide, but for more detailed information please see:
https://docs.python.org/3/library/unittest.html



How to run a single test?

Running all the tests at once can take a while. What if you just want to run a single test?

If we look inside draw_test.py, each test is a class name, and a function. There is a "DrawModuleTest" class, and there should be a "def test_ellipse" function.

So, let's run the test...

~/pygame/ $ python test/draw_test.py DrawModuleTest.test_ellipse
Traceback (most recent call last):
...
AttributeError: type object 'DrawModuleTest' has no attribute 'test_ellipse'


Starting with failure. Our test isn't there yet.

Good. This fails. It's because we don't have a test called "def test_ellipse" in there yet. What there is, is a method called 'todo_test_ellipse'. This is an extension pygame testing framework has so we can easily see which functionality we still need to write tests for.

~/pygame/ $ python run_tests.py --incomplete
...
FAILED (errors=39)

Looks like there are currently 39 functions or methods without a test. Easy pickings.


Digression: Low hanging fruit, help wanted. 

Something that's easy to do.

A little digression for a moment... what is low hanging fruit?

Low hanging fruit is easy to get off the tree. You don't need a ladder, or robot arms with a claw on the end. So I guess that's what people are talking about in the programming world when they say "low hanging fruit".

pygame low hanging fruit


Many projects keep a list of "low hanging fruit", or "help wanted" issues. Like the pygame low hanging fruit list. Ones other people don't think will be all that super hard to do. If you can't find any on there labeled like this, then ask them. Perhaps they'll know of something easy to do, but haven't had the time to mark one yet.

One little trick is that writing a simple test is quite easy for most projects. So if they don't have any marked "low hanging fruit", go take a look in their test folder and see if you can add something in there.

Don't be afraid to ask questions. If you look at an issue, and you can't figure it out, or get stuck on something, ask a nice question in there for help.

Digression: Contribution guide.

There's usually also a contribution guide.  Like the pygame Contribute wiki page. Or it may be called developer docs, or there may be a CONTRIBUTING.md file in the source code repository. Often there is a separate place the developers talk on. For pygame it is the pygame mailing list, but there is also a chat server. Find the details on the Info wiki page.

Back to the test.

The unittest module arranges tests inside functions that start with "test_" that live in a class.


This is a draft remember? So what is there left to finish in this doc?

[TODO: empty todo_test with whole class]
[TODO: image of what is wrong, moire pattern]
[TODO: show how we test that function]
[TODO: show pull request travis/appveyor running tests]






Wednesday, April 11, 2018

Berlin to Copenhagen by bike.

There's a bike path running from the north of Europe all the way to the south. Well, it's not completely finished yet, but it goes most of the way. It's part of the EuroVelo project, which aims to have a high quality cycling network made of 15 routes all around Europe completed by 2020.

All my clothes and possessions for a seven day trip from Berlin to Copenhagen. I was just going to tie a sack under my seat... but was convinced that perhaps these special bags are a good idea after all.
One part that's done well is the Berlin to Copenhagen leg(I guess not so surprising since they are two cities that are really into supporting bikes).
Scroll down on these photos someone took, if you want a look.
http://www.slowtravelberlin.com/in-photos-berlin-copenhagen-cycle-route/

Except, I'm going to do it at the end of April, not in winter like them. Which is usually the most dry time of year, and with a temperature that's not so hot or cold(usually). Fingers crossed it will be dry and warm. It will probably take 7 days if I can ride fast enough each day (about 100km). Eeek. And this part of the route (like many other parts) has a really nice website http://www.bike-berlin-copenhagen.com/route/brandenburg-stage
Going to go very minimal, and take my fast(ish) road bike. With just three sets of clothes(including the ones I'll be wearing), a netbook, and a toothbrush. It probably would be nice to have a tent, but I'll pass. Instead I'll sleep in the forest. If it rains, I'll just get wet.

Just kidding (or am I?). I'll probably cheat and stay in a hotel some nights rather than sleep in forest. I chose the laptop over the tent, because it's nice to be able to sit by a river in a field with no one around and write some nonsense.

A tiny paper notebook and pencils for drawing will also make it into my bike-pack.

Already I've started training a bit, since I didn't ride my bike for much of the winter. The first 6 hour test ride I did left me sore and tired. But that was going from almost zero riding over the last four months and straight into that. Not the smartest idea to go from nothing into such a long ride... but very enjoyable none the less.

Learning from long practice rides around Berlin. Sitting by a lake.
So what have I learnt so far?
  • Padded pants are a good idea.
  • Water is important.
  • Sunscreen is important even on cloudy days.
  • I am not fit.
  • Riding around is one of life's pleasures.
My next long practice ride is going to be with all of my gear on the bike. I expect to learn more. For example, I'll time how far I can go and at what speed. And I also want to try riding in some heavy rain - to test if my bags are actually waterproof. But unfortunately the weather has been pretty nice recently.

Other than experiments I plan to do, I'm reading about what other people do on their long bike rides. And also talking with people. But it's always hard to learn lessons from others, and the best way to find out what is important to me is to do practice rides.

I think I should be able to ride five hours each day with breaks in between, and still get up to my 100km target. But unsure really. By then the days should be quite long, and the nights with a fair bit of light.
  • Berlin day length: 14:30
  • Copenhagen day light length: 15:30
Spring flowers such as 'Tommies' and 'Snow drops' are popping their heads out for spring. Looking forward to seeing all the different landscapes along the way.
With 14-15 sunlight hour days, there's lots of time for slow riding if wind is in my face, for detours, and for sits under trees drawing. By not using the side bags, and not carrying all that much weight I should be able to go a lot faster than other people I've talked to who have done the ride. Their bikes were pretty bad, and they carried a lot more weight than what I'm going to.

I don't really have a good solution for route planning. I have a route I'd like to go, but I can't really put it on google maps easily, so it's visible on an app. So that's another thing I need to find out about. Luckily I've done lots of map development before... but I hope there's not much coding to do such a simple task, that surely other people have done before.

Also, I'm a bit worried about riding in the rain. On average it rains 8 out of 30 days that time of year. So it's quite likely I'll see rain. Generally, I just don't ride my bike if it rains. But I'll have to I guess, because 'waiting it out' doesn't work if it's raining all day.

Tuesday, April 03, 2018

pygame.Surface.blits

Drawing many things at once is faster than drawing things one thing at a time. So, today I made a function to do this with pygame.

It seems to take between 82%-85% of the time it takes to draw things one at a time. Which considering blit is often the bottleneck in many pygame apps is a pretty nice improvement. It will be landing in the pygame 1.9.4 release.

More info in the blits Pull Request.

Moss box

I've been looking to get more greenery into my studio, and I kind of like moss. It reminds me of when the times in my childhood running around rain forests barefoot.

Luckily, a good friend has a whole bunch of it growing out the back of her apartment. And it's likely to be destroyed soon by workers. Which made me not feel so bad about digging some up.


I scavenged some egg cartons

First I found some egg cartons to use as a base. Underneath the moss is a good few centimeters of dirt.

Unlike most of the wall moss I've seen around the place, this moss is alive. I was sort of horrified to learn that people buy dead preserved moss, and glue gun it into place. But this stuff is alive. Sometimes looking stuff up on Youtube kills the magic. Now I don't think wall moss is anywhere near as cool as I once thought it was. Thanks internet.

Here it is sitting on my standing desk.

Next I found an old wooden box. I lined the bottom of this with some plastic I had about the place. Yuk, I know. However, it should hopefully stop any water leaks.

There's two half egg carton containers I found (to hold 12 eggs) being used to fill the box. With spaces around the edge.

The nice thing about having it in a small box is it's easy to move about the place. So I can put it on top of my desk, or even under the desk where my bare feet are. My next project is probably to grow some green grasses in a box for that grass-on-feet-inside feeling.

Can you see the cardboard carton?
I'm not so sure if it will survive, but I'm hoping it will grow. So far I've just been spraying it with water, and occasionally touching and smelling the moss. Which takes me back to the rain forest a bit.

Friday, March 23, 2018

Investigating pypy frame drop

pypy has spikes in time it takes occasionally when using cpyext, otherwise known as pauses.
This is because it's deallocating lots of CPython objects all in one frame, rather than incrementally. This is likely to be addressed in a future release of pypy.

Mainly it's a problem when creating and deleting lots of CPython objects (like pygame.Rect). Not pure python objects.

To work around it for now,
  • the src/rect.c has been changed to have a free list of python objects, so it doesn't alloc and dealloc them. Instead it maintains it's own list of rect PyObject pointers, and reuses them. This is done in PR #431
  • Use PYPY_GC_NURSERY=1m incminimark environment variables when running pypy.
Below are timings of the default pygame, and default pypy (at time of writing). Then a benchmark showing the improvement with the rect freelist implemented in rect.c inside pygame. Then we also improve things by setting PYPY_GC_NURSERY=1m environment variable to change the behavior of the GC. I'm not aware of an API to do this within pygame itself unfortunately. Finally there are some cpython 3.6 timings for comparison.

hacked up benchmark.

The hacked up benchmark is in the branch pypy-hack-frame-bench of the pygame repo. It's based off examples.testsprite.

Because pypy currently doesn't compile matplot lib you need to run it in two steps. First to do the run (let it run for 20+ seconds). Then to load the data pickle, and show the graph.

pypy examples/testsprite.py -plot -noupdate_rects -width 320 -height 200 100
python examples/testsprite.py -plotpickle 

To run with the GC tweak...

PYPY_GC_NURSERY=1m pypy examples/testsprite.py -plot -noupdate_rects -width 320 -height 200 100
 
 

graph colors

  • Blue is the first 1200 frames
  • orange is the last 1200 frames of the run.
This is useful to see if there are any jit warm up affects happening, or different behaviour over time. (doesn't appear so).

Time per frame on pypy 5.10.0.

For this example we don't see any 'jit warm up' behavior.
screen shot 2018-03-23 at 07 21 05

Time per frame on pypy 5.10.0, rect freelist, standard GC.

screen shot 2018-03-23 at 12 05 42

Time per frame on pypy 5.10.0, rect freelist, PYPY_GC_NURSERY=1m.

screen shot 2018-03-23 at 12 04 42
This final result for pypy 'ok' because we want under 0.0166 seconds used per frame. Improvements inside pypy itself in the future should hope to remove many of these spikes.

Time per frame on python3.6

Here we see both the average time per frame and maximums are smaller on python3.6.
screen shot 2018-03-23 at 12 22 02

Time per frame on python3.6, rect freelist

It seems the freelist for rects on python3.6 has a minimal affect.
screen shot 2018-03-23 at 12 20 12

Time per frame on python3.6, gc.disable()

Here we can see using gc.disable() seems to have no affect in this program (on python 3.6). I also tried this on pypy, and it had no affect either (on this program).
screen shot 2018-03-23 at 12 25 14
Other things I tried was to do a gc.collect(0) just before display.flip(), however this didn't appear to help in this case. This is because we can know if we have 5-10ms free waiting for the display VSYNC to flip (when running at 60Hz). Perhaps this can be attempted again as the cpyext GC is improved.

Thursday, March 22, 2018

Windows pypy pygame build for testing.

How to install pypy (fast python written in python) on windows and use the pygame dev build.

pypy running pygame.examples.aliens on Windows

1) Get pypy. It's just a zip folder with all the stuff inside.
https://bitbucket.org/pypy/pypy/downloads/pypy2-v5.10.0-win32.zip
Unzip, and put into C:\pypy, so C:\pypy\pypy.exe exists.

OR get the pypy3.exe from https://bitbucket.org/pypy/pypy/downloads/pypy3-v5.10.1-win32.zip
Unzip, and put into C:\pypy3, so C:\pypy3\pypy3.exe exists.


2) Set the PATH environment variable, so that pypy.exe is found in your command prompt.
https://www.opentechguides.com/how-to/article/windows-10/113/windows-10-set-path.html

Or you can just do this each time you run a new cmd prompt.
set PATH=%PATH%;C:\pypy\;C:\pypy3\

Or just cd C:\pypy to test it out.

3) Ensure pip is installed (pip is a weirdly named tool for installing things).
pypy.exe -m ensurepip
4) Install a dev build of pygame.
pypy.exe -m pip install pygame --pre

5) Check if pygame is installed properly by running an included example.
pypy.exe -m pygame.examples.aliens

ps. use pypy3.exe for python 3 version of pypy above rather than pypy.exe(python 2) if that's what you like.

Tuesday, March 20, 2018

pygame on pypy usable

Hi,

TLDR; I'm at the pypy sprint, and working through the remaining pygame-on-pypy-cpyext issues.
https://youtu.be/WN1slc5O8os


Surprisingly to me... it's already usable. That is pygame (same one that runs on cpython), works on pypy through its C extension API. pypy has good support for the CPython API (through a recompile) now. PyPy is the python language with a fast JIT, so your code can approach C speeds. And in some cases it can be faster than C.

There was an issue with events stopping keyboard/mouse/etc from working. Lots of details in this issue describing the changes needed, so I hope other extensions encountering this will find it useful.
https://github.com/pygame/pygame/issues/419
But now that's fixed, every pygame app I tried on it has worked.

Cat sitting in the fog and snow of Switzerland

Why is this exciting?

This is exciting to me because:
  • pure python code being fast on pypy(after warmup), also mixed with the fast bits in C/asm.
  • cpyext is getting faster in pypy. There is already work and discussion towards it being faster than CPython.
  • maintaining one pygame code base is easier than maintaining several (pygame cffi/ctypes/cython, ...).
  • with one code base it should be fast on both pygame, and pypy(in time).
Here's our old pal solarwolf from early 2000s running on pypy.
https://youtu.be/WN1slc5O8os
Still lots of work to do (especially around PixelArray buffers and such). Then of course, there is the issue of binary wheels, so that  `pip install pygame`  works without needing to compile things from source.

How is the speed? (when do we use this tool? Is it fast enough?)

If your code is already quite well optimized, and not spending much time in python, you can't expect to see an improvement. However, if you are pushing boundaries in your python code, you can expect very good increases.

Some examples where you can expect it to be faster:
  • if profiling and a pygame function (like blit) isn't at the top of the slow bits.
  • collision detection (if you aren't using fancy algorithms).
  • a pure python ray caster.
  • writing a music synthesizer in python python.
Where it can be slower.
  • if you are going into C code for a lot of small operations. Like when using lots of pygame.Rect in a tight loop. This is because (currently) the cost of going from PyPy code into and out of CPython API code (like pygame) is a bit slow.
For me, I'm interested mostly in this for a physics art project which was really slow, and also for a software music synth written in pure python. Even more interesting is running pypy as a separate process for these tasks, and run the gui process with CPython.


Ray tracing 3D scenes. 

Here I run a python and pygame python ray tracer by Ian Mallett.

Ray Tracer to for realistic 3D lighting.

On PyPy - 18.6 seconds.
On Python 2.7 - 9 minutes, 28.1 seconds

That's 30x faster.  Making many more things possible in python - and at speed.


The fog has lifted at the Leysin pypy sprint.

Thursday, March 15, 2018

Drawing sound (as a waveform in pygame).

I recently gave an example of pygame sound generation examples, and a few people asked for more. So... here we go!

There's an infinite number of ways to visualize sound. A classic way is to draw it as a waveform.
Sound samples drawn as a Waveform. Scaled into a 320x200 sized Surface.

A sound could be made up of 44100 samples per second. Where each sample is often a 16 bit number (or 8bit or a 32bit floating point).
Python comes with a built in array for efficiently storing numbers. We can store samples in there, with integers between -32768 and 32768. This is a signed 16bit(2 byte) number. Two to the power of 16 is 65536, and if we divide that by two we see the minimum and maximum value that can hold. pow(2, 16) / 2 == 32768.

Below is the annotated code, also with an example of an array of samples representing a square wave.
We loop over the samples and draw a line from the previous sample to the current one. 0 to 1, 1 to 2, 2 to 3, ... N-1 to N.


You can also find it in the pygame CookBook at https://www.pygame.org/wiki/DrawWaveform
(maybe easier to read than on this blog)


# python built in array type is useful for storing sound data.
import array
# a square wave tone, with sample values between -32766 and 32766.
samples = array.array('h', [32766, 32766, 32766, 32766, 32766, 32766,
32766, 32766, 32766, 32766, 32766, 32766, 32766, 32766, 32766,
32766, 32766, 32766, 32766, 32766, 32766, 32766, 32766, 32766,
32766, 32766, 32766, 32766, -32766, -32766, -32766, -32766,
-32766, -32766, -32766, -32766, -32766, -32766, -32766, -32766,
-32766, -32766, -32766, -32766, -32766, -32766, -32766, -32766,
-32766, -32766, -32766, -32766, -32766, -32766, -32766])


def scale_samples_to_surf(width, height, samples):
    """ Returns a generator containing (x, y) to draw a waveform.

    :param width: width of surface to scale points to.
    :param height: height of surface to scale points to.
    :param samples: an array of signed 1 byte or signed 2 byte.
    """
    assert samples.typecode in ['h', 'b']
    # precalculate a bunch of variables, so not done in the loop.
    len_samples = len(samples)
    width_per_sample = width / len_samples
    height_1 = height - 1

    if samples.typecode == 'h':
        # for array typecode 'h', -32768 to 32768
        factor = 1.0 / 65532
        normalize_modifier = int(65532 / 2)
    elif samples.typecode == 'b':
        # for array typecode 'b', -127 to 127
        factor = 1.0 / 256
        normalize_modifier = int(256 / 2)

    return ((
        int((sample_number + 1) * width_per_sample),
        int(
            (1.0 -
                (factor *
                    (samples[sample_number] + normalize_modifier)))
            * (height_1)
        ))
    for sample_number in range(len_samples))

def draw_wave(surf,
              samples,
              wave_color = (0, 0, 0),
              background_color = pg.Color('white')):
    """Draw array of sound samples as waveform into the 'surf'.

    :param surf: Surface we want to draw the wave form onto.
    :param samples: an array of signed 1 byte or signed 2 byte.
    :param wave_color: color to draw the wave form.
    :param background_color: to fill the 'surf' with.
    """
    assert samples.typecode in ['h', 'b']
    if background_color is not None:
        surf.fill(background_color)
    width, height = surf.get_size()
    points = tuple(scale_samples_to_surf(width, height, samples))
    pg.draw.lines(surf, wave_color, False, points)

# Here we should how to draw it onto a screen.
waveform = pg.Surface((320, 200)).convert_alpha()
draw_wave(waveform, samples)
screen.fill((255, 255, 255))
screen.blit(waveform, (160, 100))
 
 

Wednesday, March 14, 2018

Sound generation pygame examples.


Here's a few sound generation examples with pygame (and no numpy/scipy).

If there's interest I'll expand this into a bigger example? Let me know.


All the basics for making a music program (sampler/synth).
  • some sample rate conversion,
  • bit rate conversion
  • tone generation using generators (square wave)
  • python arrays used as buffers for pygame.Sound (zero copy).



""" Some examples for generating and converting sounds for pygame.

Python 2.7, 3.6

Shows:
    - a simple 'square wave' generated
    - resampling sample rates (eg, 8363 to 44100)
    - using built in python array for making pygame.Sound samples.
    - samples at different bit sizes
    - converting from signed 8 to signed 16bit
    - how initializing the mixer changes what samples Sound needs.
    - Using the python stdlib audioop.ratecv for sample rate conversion.

Square Wave
  https://en.wikipedia.org/wiki/Square_wave
MOD (file format)
  https://en.wikipedia.org/wiki/MOD_(file_format)

pygame.mixer.get_init
    https://www.pygame.org/docs/ref/mixer.html#pygame.mixer.get_init
pygame.Sound
    https://www.pygame.org/docs/ref/mixer.html#pygame.mixer.Sound

array (python stdlib)
    https://docs.python.org/3/library/array.html
wave (python stdlib)
    https://docs.python.org/3/library/wave.html
audioop.ratecv (python stdlib)
    https://docs.python.org/3/library/audioop.html?highlight=audio#audioop.ratecv

"""

from array import array
import pygame as pg

class Tone(pg.mixer.Sound):
    """This generates a 'Square wave' with a generator.

    Then creates an array of samples, and passes that into pygame.Sound.
    """

    def __init__(self, frequency, array_type, volume=.1):
        self.frequency = frequency
        if array_type == 'b':
            # we have to convert the 1 byte 'b' samples to 2 byte 'h'.
            samples = self.signed_char_to_signed_short(
                self.make_samples_b()
            )
        elif array_type == 'h':
            samples = self.make_samples_h()
        else:
            raise ValueError('array_type not supported')

        pg.mixer.Sound.__init__(self, buffer=samples)
        self.set_volume(volume)

    def make_samples_b(self):
        """ Builds samples array between -127 and 127.
            Array type 'h'.
        """
        mixer_frequency = pg.mixer.get_init()[0]
        mixer_format = pg.mixer.get_init()[1]
        period = int(round(mixer_frequency / self.frequency))
        max_amplitude = 2 ** (abs(mixer_format) - 1) - 1
        max_amplitude = int(max_amplitude / 256)
        # print(f'mixer_frequency:{mixer_frequency}, mixer_format:{mixer_format}')
        # print(f'period:{period}, max_amplitude:{max_amplitude}')

        # 'b' array is signed char, 1 byte
        # https://docs.python.org/3/library/array.html
        samples = array('b',
            (max_amplitude if time < period / 2 else -max_amplitude
                for time in range(period))
        )
        return samples

    def signed_char_to_signed_short(self, b_samples):
        """ Converts 1 byte signed char samples to 2 byte signed short.

            127 to 32767
        """
        # just a simple linear conversion.
        factor = int(32767 / 127)
        return array('h', (sample * factor for sample in b_samples))

    def make_samples_h(self):
        """ Builds samples array between -32767 snd 32767.
            Array type 'h'.
        """
        mixer_frequency = pg.mixer.get_init()[0]
        mixer_format = pg.mixer.get_init()[1]
        period = int(round(mixer_frequency / self.frequency))
        max_amplitude = 2 ** (abs(mixer_format) - 1) - 1
        # print(f'mixer_frequency:{mixer_frequency}, mixer_format:{mixer_format}')
        # print(f'period:{period}, max_amplitude:{max_amplitude}')

        # 'h' array is signed short, 2 bytes
        # https://docs.python.org/3/library/array.html
        samples = array('h',
            (max_amplitude if time < period / 2 else -max_amplitude
                for time in range(period))
        )
        return samples


class Sample(pg.mixer.Sound):
    """ For playing a sample.

    Takes a file, and reads it in as 8bit signed data.

    Then converts it to the 16bit signed size the pygame.mixer needs.
    """
    def __init__(self, fname, volume=.1):
        with open(fname, 'rb') as f:
            samples = self.signed_char_to_signed_short (
                array('b', f.read())
            )
            pg.mixer.Sound.__init__(self, buffer=samples)
        self.set_volume(volume)

    def signed_char_to_signed_short(self, b_samples):
        """ Converts 1 byte signed char samples to 2 byte signed short.
            127 to 32767
        """
        # just a simple linear conversion.
        import time
        t0=time.time()
        factor = int(32767 / 127)
        samples = array('h', (
            max(sample, -127) * factor if sample < 0 else
            min(sample, 127) * factor
            for sample in b_samples))
        t1=time.time()
        print(t1-t0)
        return samples


def fetch_example_mod_file(mod_fname):
    """ Grab a file that has a sound samples in it from the net.

    'MOD is a computer file format used primarily to represent music,
    and was the first module file format. MOD files use the ".MOD"
    file extension, except on the Amiga which doesn't rely on
    filename extensions, instead it reads a file's header to
    determine filetype. A MOD file contains a set of instruments in
    the form of samples, a number of patterns indicating how and when
    the samples are to be played, and a list of what
    patterns to play in what order.'

    https://en.wikipedia.org/wiki/MOD_(file_format)
    """
    import os
    url = 'https://api.modarchive.org/downloads.php?moduleid=101996'

    if not os.path.exists(mod_fname):
        import urllib2
        print ('Fetching %s .mod into file: %s' % (url, mod_fname))
        data = urllib2.urlopen(url).read()
        with open(mod_fname, 'w') as modf:
            modf.write(data)


def resample(mod_fname):
    """ An example of resampling audio to a different framerate.

    eg, from 8363 one byte samples per second to
        44100 two byte samples per second.
    """
    import audioop
    import wave
    from io import BytesIO

    in_framerate = 8363
    in_sampwidth = 1
    in_nchannels = 1

    out_framerate = 44100

    num_seconds = 5
    with open(mod_fname, 'rb') as f:
        # Throw away the start data of this mod file.
        #   Better samples later on.
        f.read(8363*2)
        in_frame_data = f.read(in_framerate * num_seconds)

    # https://docs.python.org/3/library/audioop.html?highlight=audio#audioop.ratecv
    newfragment, newstate = audioop.ratecv(
        in_frame_data,
        in_sampwidth,
        in_nchannels,
        in_framerate,
        out_framerate,
        None)

    # print(f'len(newfragment):{len(newfragment)}')
    # A perfect conversion is not possible, because the sample
    #   rates do not divide equally. However, the number
    #   of samples should be close.
    assert (out_framerate * num_seconds) - len(newfragment) < 10
    pg.mixer.Sound(buffer=newfragment).play(-1)



# TODO:
# Converting between modo and stereo?
#   audioop.tomono and audioop.tostereo
#   https://docs.python.org/3/library/audioop.html?highlight=audio#audioop.tomono

# How to draw a wave form?
#   using pygame.draw.lines transforming audio into
#   Surface space.
#   Meaning, scaling audio samples into a particular
#   sized part of the screen.

# More sound generator types.
#   Saw tooth.



if __name__ == "__main__":
    # https://www.pygame.org/docs/ref/mixer.html#pygame.mixer.init
    pg.mixer.pre_init(44100, -16, 1, 1024)
    pg.init()

    pg.display.set_caption('Playing square wave, 808 frequency')
    pg.display.set_mode((320, 200))

    mod_fname = 'outrun_3.mod'

    fetch_example_mod_file(mod_fname)

    # play on repeat, -1 means loop indefinitely.
    # https://www.pygame.org/docs/ref/mixer.html#pygame.mixer.Sound.play
    if 0:
        Tone(frequency=808, array_type='b').play(-1)

    if 0:
        try:
            Sample(mod_fname).play(-1)
        except IOError:
            print ('no %s' % mod_fname)
    if 0:
        pg.mixer.music.load(mod_fname)
        pg.mixer.music.play()

    if 1:
        resample(mod_fname)

    going = True
    while going:
        for e in pg.event.get():
            if e.type in [pg.QUIT, pg.KEYDOWN]:
                going = False

Tuesday, March 13, 2018

pypy sprint

Been doing a bit of reading this morning on pypy things.

Trying to prepare myself a bit for the coming 'winter sprint'. One topic I'm interested in for the sprint is finishing off remaining issues for pygame running on pypy. Probably not the most common reason for people to go to the Swiss mountains I guess! So this means learning about the cpyext part of pypy... the one which allows it to use the python C API, and modules that use it.

Open source sprints are great. A bunch of people get together trying to make things. A very rare time when people get to meet other people-from-the-internet. It's a great place to learn, and to see how projects you're interested in are made.

I guess I'm a little bit too excited, and started playing with pypy early. Below are some notes from my investigations finding out how to do things.

Getting started, finding docs.

There's a getting started guide for pypy which has some basic notes on running tests, and compiling stuff.

Also, cython has a pypy section in their user guide - http://cython.readthedocs.io/en/latest/src/userguide/pypy.html

cpyext is the module which implements the python C API in pypy.

A wiki page called 'c-api' contains some documentation including a talk on cpyext. It's a little bit dated (from 7 years ago), but still interesting reading(and watching).

This wiki page was interesting:  "In which we document cpyext compatibility progress, this time with cython and pandas."
https://bitbucket.org/pypy/pypy/wiki/cpyext_2_-_cython%20and%20pandas

It's a blog of development of pypy cpyext getting numpy and pandas features working. From this I noticed a few interesting techniques... like running tests separated processes. Very useful when debugging. There is also an older document called 'Adventures in cpyext compatibility (ended)'. With even more notes on issues that came up, along with their work arounds.

Of course the Python C API docs are also useful when one wants to do Python C API things.

Use the source (Luke).

But the best source of information on cpyext seems to be the source code itself.
https://bitbucket.org/pypy/pypy/src/default/pypy/module/cpyext/

Lots and lots and lots of tests. And lots and lots of code. Bed time reading perhaps.

It seems to be a whole implementation of CPython, with bridges into pypy. But with tests. Lots and lots of tests for the python C API.

What are useful debugging techniques?

CPython has a thing called a debug build which is useful.
https://docs.python.org/3/c-api/intro.html#debugging-builds

I don't know of any special debug tools for pypy (gdb extensions or such like).


Wheels on pypy?

I found this page "pypy binary wheels for some popular packages", and a blog post about it.

It seems pypy doesn't compile on the ancient centos 5 that many linux uses. Perhaps the new manylinux which uses a newer linux will work.

It seems the piwheels project (compiles wheels for raspberry pi automatically) doesn't have pypy support yet. piwheels is really great, in that it makes lots of ARM builds available so people don't need to compile stuff themselves on the raspberrypi.

No idea if windows, or mac binary wheels work for pypy. Without binary wheels for these platforms, most pypy users won't be able to install a C based library like pygame on pypy.

Which platform?

It seems linux amd64 is the best supported, but I'm also interested in Mac, and Windows builds.
Especially setting them up on Appveyor, and Travis /Mac.

Translating on windows is the document which describes the windows builds. Not sure my cheap windows laptop has enough memory for it. It's only got 4GB of ram, and I expect it's going to take hours to compile.

The buildbot contains Nightly builds for windows as well as other platforms. So I might try using them to debug stuff, and I guess do extension development. Failing that, maybe I can copy a build off someones computer!

Hopefully I won't need to hire some remote servers for compiling stuff. It doesn't seem like I'd need to be recompiling all of pypy each time for extension development.

Thursday, March 08, 2018

pygameweb - 0.0.4 - Northern Rockhopper Penguin

pygameweb is the source code for the pygame website at https://www.pygame.org/


Northern Rockhopper Penguin (photo by Brian Gratwicke)
  • #52 Adding ability to deploy through travisci. And docs.
  • #46 Wiki readability. Wiki tools on left out of reading line.
  • Twitter card for releases. For nicer view when posting to twitter.
  • Wiki table of contents improvements.
  • builds docs for pygame, when something lands on pygame github master branch.
  • project release feeds bugfix.
  • Only show one recent release for each project. Some projects release a few times in a row.
  • Wiki, nicer pre code handling.
  • Wiki code uses inline colors, so copy/paste works with colors too.
  • use https for login and register buttons, even when on http (see Why can't we turn off HTTPS?).
  • Ask more bad robot web crawlers not to be bad.
See a full diff of changes since last release.

Tuesday, March 06, 2018

On the threshold of a journey with a Jedi, a Sphinx, and a Homebrew.

Feels like practice.
It all started some days ago, late at night,
with a comment made by someone on the internet...
"Threshold does not take keyword arguments"
As is often the case when you want to correct
something on the internet,
this comment lead me on a journey
(not one where I sell numpy for 22 million monies,
a different type of journey
involving text editors and compilers,
and lots of failing tests).
Software archeology -- not with Indiana,
but at least there was a Jedi and a Sphinx.

"Relax. Don't worry. And have a homebrew." -- Charlie Papazian

And with a warm brew in hand,
herein begins my tale...

Starting at the end, where all poor stories start,
with the comment I will send "TheBob427" (actual real nick),
right after I publish this blog post... [-1]





Dear TheBob427,

yes, this function doesn't take keyword arguments and you are right
that it's super buggy. However, now after some days of bashing my keyboard,
it should actually work. Additionally it should be slightly less weird.
It was a fun journey. I guess you'll have to wait for the next release to use it.
Which should be here 'soon'. Which in open source minutes is 
approximately the same amount of time as 'when it's done'.

yours sincerely,
illume

ps. you could also use PixelArray.replace instead.



So, I updated the pygame.transform.threshold function, tests and docs [0].
Apart from that function being buggy and super confusing...

It was sort of a test of how we could improve all the pygame functions for pygame 3000.
Whilst it still doesn't follow the new pygame C API yet[1],
I wanted to try a few things like 'new' C99/C11 features
(thanks ffmpeg for tricking MS into dragging their compiler into the 90s),
new docs features (param types, and examples) and such like.

It's quite a bit of C and python test changes,
and took me quite a few days longer than I expected.
Simplifying here. Simplifying there. Simplifying everywhere.

One thing it does now is use sphinx type parameters.
Apart from the docs being improved, tools like
jedi can use them for autocomplete and type inference.

So, now editors like vim, sublime, and vscode can autocomplete with pygame.
(I had to change a few things for the 'odd' but 'cool' pygame
'missing module' imports feature so the type inference worked in jedi [2])

Fancy.

For examples, sphinx can now easily include source code.
So we can include pygame.examples code, or even test cases.
The 'nulledge' python search code search doesn't seem all that good
for many pygame things, and it lacks https. So i'm thinking of dropping
that and manually linking to relevant examples/test code.

Additionally, we can link to github source code, and even
the documentation files themselves. Which is handy for
'edit this'/'fork on github' style buttons.


Feels like practice.



[-1] a comment, https://www.reddit.com/r/pygame/comments/80zyqg/threshold_does_not_take_keyword_arguments/dvae53d/
[0] threshold function pr, https://github.com/pygame/pygame/pull/408
[1] pygame C api issue, https://github.com/pygame/pygame/issues/333
[2] jedi confused with pygame pr, https://github.com/pygame/pygame/pull/413