Skip to content

Commit d4c8b45

Browse files
authored
Merge pull request gpiozero#729 from RPi-Distro/tones
Tones
2 parents 87a8409 + d866764 commit d4c8b45

File tree

14 files changed

+456
-120
lines changed

14 files changed

+456
-120
lines changed

docs/api_output.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,8 @@ Buzzer
8484
TonalBuzzer
8585
-----------
8686

87-
.. autoclass:: TonalBuzzer(pin, \*, initial_value=None, mid_note='A4', octaves=1, pin_factory=None)
88-
:members: play, stop, note_value, octaves, min_note, mid_note, max_note, is_active, value
87+
.. autoclass:: TonalBuzzer(pin, \*, initial_value=None, mid_tone=Tone('A4'), octaves=1, pin_factory=None)
88+
:members: play, stop, octaves, min_tone, mid_tone, max_tone, tone, is_active, value
8989

9090

9191
Motor

docs/api_tones.rst

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
===========
2+
API - Tones
3+
===========
4+
5+
.. module:: gpiozero.tones
6+
7+
GPIO Zero includes a :class:`Tone` class intended for use with the
8+
:class:`~gpiozero.TonalBuzzer`. This class is in the ``tones`` module of GPIO
9+
Zero and is typically imported as follows::
10+
11+
from gpiozero.tones import Tone
12+
13+
14+
Tone
15+
====
16+
17+
.. autoclass:: Tone
18+
:members:

docs/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ Table of Contents
5656
api_internal
5757
api_generic
5858
api_tools
59+
api_tones
5960
api_info
6061
api_pins
6162
api_exc

gpiozero/compat.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ def median(data):
9393
return (data[i - 1] + data[i]) / 2
9494

9595

96+
# Backported from py3.3
9697
def log2(x):
9798
return math.log(x, 2)
9899

gpiozero/exc.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,3 +208,6 @@ class ThresholdOutOfRange(GPIOZeroWarning):
208208

209209
class CallbackSetToNone(GPIOZeroWarning):
210210
"Warning raised when a callback is set to None when its previous value was None"
211+
212+
class AmbiguousTone(GPIOZeroWarning):
213+
"Warning raised when a Tone is constructed with an ambiguous number"

gpiozero/internal_devices.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -483,8 +483,12 @@ def usage(self):
483483
"""
484484
# XXX Use os.statvfs?
485485
df = subprocess.Popen(['df', '--output=pcent', self.filesystem], stdout=subprocess.PIPE)
486-
output = df.communicate()[0].split(b'\n')[1].split()[0]
487-
return float(output.decode('UTF-8').rstrip('%'))
486+
try:
487+
output = df.communicate()[0].split(b'\n')[1].split()[0]
488+
return float(output.decode('UTF-8').rstrip('%'))
489+
finally:
490+
df.stdout.close()
491+
df.wait()
488492

489493
@property
490494
def value(self):

gpiozero/output_devices.py

Lines changed: 82 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
from .devices import GPIODevice, Device, CompositeDevice
5454
from .mixins import SourceMixin
5555
from .threads import GPIOThread
56+
from .tones import Tone
5657

5758

5859
class OutputDevice(SourceMixin, GPIODevice):
@@ -626,74 +627,75 @@ class TonalBuzzer(SourceMixin, CompositeDevice):
626627
:class:`~gpiozero.pins.pigpio.PiGPIOFactory`.
627628
"""
628629

629-
def __init__(self, pin=None, initial_value=None, mid_note='A4', octaves=1,
630-
pin_factory=None):
631-
self._validate_note_range(mid_note, octaves)
630+
def __init__(self, pin=None, initial_value=None, mid_tone=Tone("A4"),
631+
octaves=1, pin_factory=None):
632+
self._mid_tone = None
632633
super(TonalBuzzer, self).__init__(
633634
pwm_device=PWMOutputDevice(
634635
pin=pin, pin_factory=pin_factory
635636
), pin_factory=pin_factory)
636637
try:
638+
self._mid_tone = Tone(mid_tone)
639+
if not (0 < octaves <= 9):
640+
raise ValueError('octaves must be between 1 and 9')
637641
self._octaves = octaves
642+
try:
643+
self.min_tone.note
644+
except ValueError:
645+
raise ValueError(
646+
'%r is too low for %d octaves' %
647+
(self._mid_tone, self._octaves))
648+
try:
649+
self.max_tone.note
650+
except ValueError:
651+
raise ValueError(
652+
'%r is too high for %d octaves' %
653+
(self._mid_tone, self._octaves))
638654
self.value = initial_value
639655
except:
640656
self.close()
641657
raise
642658

643659
def __repr__(self):
644660
try:
645-
return '<gpiozero.%s object on pin %r, is_active=%s>' % (
646-
self.__class__.__name__, self.pwm_device.pin, self.is_active)
661+
if self.value is None:
662+
return '<gpiozero.TonalBuzzer object on pin %r, silent>' % (
663+
self.pwm_device.pin,)
664+
else:
665+
return '<gpiozero.TonalBuzzer object on pin %r, playing %s>' % (
666+
self.pwm_device.pin, self.tone.note)
647667
except:
648-
return super(OutputDevice, self).__repr__()
649-
650-
def _note_to_midi(self, note):
651-
if isinstance(note, bytes):
652-
note = note.decode('ascii')
653-
if isinstance(note, str):
654-
nt, num = note[:-1].replace('#', 'S'), int(note[-1])
655-
notes = 'C CS D DS E F FS G GS A AS B'.split(' ')
656-
note = (num + 1) * 12 + notes.index(nt)
657-
return note
658-
659-
def _note_to_freq(self, note):
660-
midi = self._note_to_midi(note)
661-
return 2**((midi-69)/12)*440
662-
663-
def _validate_note_range(self, mid_note, octaves):
664-
self._mid_note = self._note_to_midi(mid_note)
665-
self._octaves = octaves
666-
if octaves < 1:
667-
raise ValueError('octaves must be at least 1')
668-
if self.min_note < 1:
669-
plural = 's' if octaves > 1 else ''
670-
raise ValueError(
671-
'mid_note too low for {} octave{}'.format(octaves, plural))
672-
673-
def note_value(self, note):
674-
"""
675-
Convert the given note to a normalized value scaled -1 to 1 relative to
676-
the buzzer's range. If note is :data:`None`, :data:`None` is returned.
677-
"""
678-
if note is None:
679-
return None
680-
note = self._note_to_midi(note)
681-
return (note - self.mid_note) / (self.max_note - self.mid_note)
668+
return super(TonalBuzzer, self).__repr__()
682669

683-
def play(self, note):
670+
def play(self, tone):
684671
"""
685-
Play the given note e.g. ``play(60)`` (MIDI) or ``play('C4')`` (note).
686-
``play(None)`` turns the buzzer off.
672+
Play the given *tone*. This can either be an instance of
673+
:class:`~gpiozero.tones.Tone` or can be anything that could be used to
674+
construct an instance of :class:`~gpiozero.tones.Tone`.
675+
676+
For example::
677+
678+
>>> from gpiozero import TonalBuzzer
679+
>>> from gpiozero.tones import Tone
680+
>>> b = TonalBuzzer(17)
681+
>>> b.play(Tone("A4"))
682+
>>> b.play(Tone(220.0)) # Hz
683+
>>> b.play(Tone(60)) # middle C in MIDI notation
684+
>>> b.play("A4")
685+
>>> b.play(220.0)
686+
>>> b.play(60)
687687
"""
688-
if note is None:
688+
if tone is None:
689689
self.value = None
690690
else:
691-
freq = self._note_to_freq(note)
692-
if self.min_frequency <= freq <= self.max_frequency:
691+
if not isinstance(tone, Tone):
692+
tone = Tone(tone)
693+
freq = tone.frequency
694+
if self.min_tone.frequency <= tone <= self.max_tone.frequency:
693695
self.pwm_device.pin.frequency = freq
694696
self.pwm_device.value = 0.5
695697
else:
696-
raise ValueError("note out of the device's range")
698+
raise ValueError("tone is out of the device's range")
697699

698700
def stop(self):
699701
"""
@@ -702,33 +704,46 @@ def stop(self):
702704
"""
703705
self.value = None
704706

707+
@property
708+
def tone(self):
709+
"""
710+
Returns the :class:`~gpiozero.tones.Tone` that the buzzer is currently
711+
playing, or :data:`None` if the buzzer is silent. This property can
712+
also be set to play the specified tone.
713+
"""
714+
if self.pwm_device.pin.frequency is None:
715+
return None
716+
else:
717+
return Tone.from_frequency(self.pwm_device.pin.frequency)
718+
719+
@tone.setter
720+
def tone(self, value):
721+
self.play(value)
722+
705723
@property
706724
def value(self):
707725
"""
708-
Represents the state of the buzzer as a value between 0 (representing
726+
Represents the state of the buzzer as a value between -1 (representing
709727
the minimum note) and 1 (representing the maximum note). This can also
710728
be the special value :data:`None` indicating that the buzzer is
711729
currently silent.
712730
"""
713731
if self.pwm_device.pin.frequency is None:
714732
return None
715733
else:
716-
freq = self.pwm_device.pin.frequency
717-
mid_freq = self.mid_frequency
718734
try:
719-
return log2(freq / mid_freq) / self.octaves
735+
return log2(
736+
self.pwm_device.pin.frequency / self.mid_tone.frequency
737+
) / self.octaves
720738
except ZeroDivisionError:
721-
return 0
739+
return 0.0
722740

723741
@value.setter
724742
def value(self, value):
725743
if value is None:
726744
self.pwm_device.pin.frequency = None
727745
elif -1 <= value <= 1:
728-
if value == 0:
729-
freq = self.mid_frequency
730-
else:
731-
freq = self.mid_frequency * 2**(self.octaves * value)
746+
freq = self.mid_tone.frequency * 2 ** (self.octaves * value)
732747
self.pwm_device.pin.frequency = freq
733748
self.pwm_device.value = 0.5
734749
else:
@@ -751,49 +766,28 @@ def octaves(self):
751766
return self._octaves
752767

753768
@property
754-
def min_note(self):
755-
"""
756-
The minimum note available (i.e. the MIDI note represented when value
757-
is -1).
758-
"""
759-
return self.mid_note - 12 * self.octaves
760-
761-
@property
762-
def mid_note(self):
763-
"""
764-
The middle note available (i.e. the MIDI note represented when value is
765-
0).
766-
"""
767-
return self._mid_note
768-
769-
@property
770-
def max_note(self):
771-
"""
772-
The maximum note available (i.e. the MIDI note represented when value
773-
is 1).
774-
"""
775-
return self.mid_note + 12 * self.octaves
776-
777-
@property
778-
def min_frequency(self):
769+
def min_tone(self):
779770
"""
780-
The frequency of the minimum note.
771+
The lowest tone that the buzzer can play, i.e. the tone played
772+
when :attr:`value` is -1.
781773
"""
782-
return self._note_to_freq(self.min_note)
774+
return self._mid_tone.down(12 * self.octaves)
783775

784776
@property
785-
def mid_frequency(self):
777+
def mid_tone(self):
786778
"""
787-
The frequency of the middle note.
779+
The middle tone available, i.e. the tone played when :attr:`value` is
780+
0.
788781
"""
789-
return self._note_to_freq(self.mid_note)
782+
return self._mid_tone
790783

791784
@property
792-
def max_frequency(self):
785+
def max_tone(self):
793786
"""
794-
The frequency of the maximum note.
787+
The highest tone that the buzzer can play, i.e. the tone played when
788+
:attr:`value` is 1.
795789
"""
796-
return self._note_to_freq(self.max_note)
790+
return self._mid_tone.up(12 * self.octaves)
797791

798792

799793
class PWMLED(PWMOutputDevice):

0 commit comments

Comments
 (0)