Skip to content

Commit

Permalink
Tweak API for tone generation and add docs
Browse files Browse the repository at this point in the history
  • Loading branch information
lordmauve committed Feb 18, 2018
1 parent 6fc21e7 commit 5214f11
Show file tree
Hide file tree
Showing 4 changed files with 59 additions and 13 deletions.
39 changes: 39 additions & 0 deletions doc/builtins.rst
Original file line number Diff line number Diff line change
Expand Up @@ -693,3 +693,42 @@ The ``animate()`` function returns an ``Animation`` instance:
when the animation duration runs out. The ``on_finished`` argument
to ``animate()`` also sets this attribute. It is not called when
``stop()`` is called. This function takes no arguments.


Tone Generator
--------------

Pygame Zero can play tones using a built-in synthesizer.

.. function:: tone.play(pitch, duration)

Play a note at the given pitch for the given duration.

Duration is in seconds.

The `pitch` can be specified as a number in which case it is the frequency
of the note in hertz.

Alternatively, the pitch can be specified as a string representing a note
name and octave. For example:

* ``'E4'`` would be E in octave 4.
* ``'A#5'`` would be A-sharp in octave 5.
* ``'Bb3'`` would be B-flat in octave 3.

Creating notes, particularly long notes, takes time - up to several
milliseconds. You can create your notes ahead of time so that this doesn't slow
your game down while it is running:

.. function:: tone.create(pitch, duration)

Create and return a Sound object.

The arguments are as for play(), above.

This could be used in a Pygame Zero program like this::

beep = tone.create('A3', 0.5)

def on_mouse_down():
beep.play()
1 change: 1 addition & 0 deletions doc/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Changelog
* New: Actors are no longer subclasses of Rect, though they provide the same
methods/properties. However they are now provided with floating point
precision.
* New: ``tone.play()`` function to allow playing musical notes.
* New: ``pgzrun.go()`` to allow running Pygame Zero from an IDE (see
:doc:`ide-mode`).
* Examples: add Asteroids example game (thanks to Ian Salmons)
Expand Down
6 changes: 3 additions & 3 deletions examples/basic/tones.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

TONES = [
('E4', 0.1, 0.1),
('E4', 0.1, 0.3),
('E4', 0.1, 0.1),
('E4', 0.1, 0.3),
('C4', 0.1, 0.1),
('E4', 0.1, 0.3),
Expand All @@ -13,7 +13,7 @@
('E3', 0.3, 0.3),
('A3', 0.3, 0.1),
('B3', 0.1, 0.3),
('A#3', 0.2, 0),
('A#3', 0.2, 0.1),
('A3', 0.1, 0.3),
('G3', 0.1, 0.15),
('E4', 0.1, 0.15),
Expand All @@ -30,7 +30,7 @@
('E3', 0.3, 0.3),
('A3', 0.3, 0.1),
('B3', 0.1, 0.3),
('A#3', 0.2, 0),
('A#3', 0.2, 0.1),
('A3', 0.1, 0.3),
('G3', 0.1, 0.15),
('E4', 0.1, 0.15),
Expand Down
26 changes: 16 additions & 10 deletions pgzero/tone.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@
and frequency. These are kept in a LRU cache which in typical applications
will reduce the number of times they need to be regenerated.
Rather than generating plain sine waves, tones are shaped by a basic and
hard-coded `Attack Decay Sustain Release (ADSR) envelope`__, which gives them a
slightly more sonorous timbre:
.. __: https://en.wikipedia.org/wiki/Synthesizer#ADSR_envelope
The approach we use here, generating sound samples in memory, is memory hungry
and can introduce pauses when tones are generated. Currently tones generate in
under 1ms on a 2.4GHz i7.
Expand Down Expand Up @@ -59,13 +65,13 @@ def sine_array_onecycle(hz):
return (np.sin(xvalues) * (2 ** 15)).astype(np.int16)


def create(hz, length):
"""Create a tone of a given duration at a given frequency in hertz.
def create(pitch, duration):
"""Create a tone of a given duration at the given pitch.
Return a Sound which can be played later.
"""
return _create(_convert_args(hz, length))
return _create(*_convert_args(pitch, duration))


@lru_cache()
Expand Down Expand Up @@ -122,11 +128,11 @@ def validate_note(note):
return note, accidental, int(octave)


def _convert_args(hz, length):
def _convert_args(hz, duration):
"""Convert the given arguments to _create parameters."""
if isinstance(hz, str):
hz = note_to_hertz(hz)
samples = int(length * SAMPLE_RATE)
samples = int(duration * SAMPLE_RATE)
if not samples:
raise InvalidNote("Note has zero duration")
return hz, samples
Expand All @@ -149,7 +155,7 @@ def _play_thread():
player_thread.setDaemon(True)


def play(hz, length):
def play(pitch, duration):
"""Plays a tone of a certain length from a note or frequency in hertz.
Tones have a maximum duration of 4 seconds. This limitation is imposed to
Expand All @@ -160,12 +166,12 @@ def play(hz, length):
create() and hold onto them, perhaps in an array.
"""
if length > MAX_DURATION:
if duration > MAX_DURATION:
raise InvalidNote(
'Note length %ss is too long: notes may be at most %ss long' %
(length, MAX_DURATION)
'Note duration %ss is too long: notes may be at most %ss long' %
(duration, MAX_DURATION)
)
args = _convert_args(hz, length)
args = _convert_args(pitch, duration)
if not player_thread.is_alive():
player_thread.start()
note_queue.put(args)
Expand Down

0 comments on commit 5214f11

Please sign in to comment.