Pythonic sound generator
authorStefano Rivera <stefano@rivera.za.net>
Fri, 16 May 2014 20:19:23 +0000 (22:19 +0200)
committerStefano Rivera <stefano@rivera.za.net>
Fri, 16 May 2014 20:19:23 +0000 (22:19 +0200)
data/sounds/SOURCES.txt
data/sounds/__init__.py [new file with mode: 0644]
naja/actions.py
naja/gen_sound.py [new file with mode: 0644]
tools/gen_sound.py

index b872f6c2cfb5566677270e35dae3394e093238c1..954a21bab7339753f55efb652949cbb08e13b616 100644 (file)
@@ -6,95 +6,4 @@ General Notes:
       sox -t raw -e signed-integer -c 2 -r 44100 -b 16 beep100.pcm my_sound.ogg
     on those instead.
 
-silence.ogg
------------
-
-Notes:
-    Generated 2 secs of silence - dd if=/dev/zero of=silence.pcm bs=176400 count=2 ; oggenc -r silence.pcm
-    Generated by Neil Muller, Aug 2010
-    Not copyrightable.
-
-zzzzz.ogg
----------
-
-Notes:
-   Generated by: tools/gen_sound.py 25 0.5 50 ; oggenc -o zzzzz.ogg -r beep100.pcm
-   Generate by Simon Cross, May 2014
-   License: MIT-style permissive license - see LICENSE.txt
-
-error.ogg
----------
-
-Notes:
-   Generated by: tools/gen_sound.py 1000 0.25 ; oggenc -o error.ogg -r beep1000.pcm
-   Generated by Neil Muller, May 2014
-   License: MIT-style permissive license - see LICENSE.txt
-
-startup.ogg
------------
-
-Notes:
-   Generated by: tools/gen_sound.py 200 0.25 ; tools/gen_sound.py 400 0.25 ; tools/gen_sound.py 600 0.25 ; tools/gen_sound.py 800 0.25 ;
-                 cat beep200.pcm beep400.pcm beep600.pcm beep800.pcm > startup.pcm ;
-                 oggenc -o startup.ogg -r startup.pcm
-   Generated by Neil Muller, May 2014
-   License: MIT-style permissive license - see LICENSE.txt
-
-
-shutdown.ogg
-------------
-
-Notes:
-   Generated by: tools/gen_sound.py 200 0.25 ; tools/gen_sound.py 400 0.25 ; tools/gen_sound.py 600 0.25 ; tools/gen_sound.py 800 0.25 ;
-                 cat beep800.pcm beep600.pcm beep400.pcm beep200.pcm > shutdown.pcm ;
-                 oggenc -o shutdown.ogg -r shutdown.pcm
-   Generated by Stefano Rivera, May 2014
-   License: MIT-style permissive license - see LICENSE.txt
-
-
-zoop.ogg
---------
-
-Notes:
-   Generated by:
-        for ((j=500; $j < 800; j= $j+20 )); do python ../../tools/gen_sound.py $j 0.01 50; done
-        cat beep5??.pcm beep6??.pcm  beep7??.pcm > zoop.pcm
-        oggenc -o zoop.ogg -r zoop.pcm
-   Generated by Neil Muller, May 2014
-   License: MIT-style permissive license - see LICENSE.txt
-
-chirp.ogg
----------
-
-Notes:
-   Generate by:
-       python ../../tools/gen_sound.py 1650 0.05 50 ; python ../../tools/gen_sound.py 1450 0.05 50
-       cat beep1450.pcm beep1650.pcm > chirp.pcm
-       oggenc -o chirp.ogg -r chirp.pcm
-   Generated by Neil Muller, May 2014
-   License: MIT-style permissive license - see LICENSE.txt
-
-
-grind.ogg
----------
-
-Notes:
-    Generate by:
-        ./tools/gen_sound.py 25 0.01 25 ; ./tools/gen_sound.py 120 0.01 25 ; ./tools/gen_sound.py 150 0.01 25 ; ./tools/gen_sound.py 170 0.01 25 ; ./tools/gen_sound.py 200 0.01 25 ; ./tools/gen_sound.py 250 0.01 25 ; ./tools/gen_sound.py 350 0.01 25 ; ./tools/gen_sound.py 300 0.01 25
-        cat beep100.pcm beep250.pcm beep100.pcm beep200.pcm beep170.pcm beep300.pcm beep170.pcm beep100.pcm beep300.pcm beep120.pcm beep100.pcm beep300.pcm beep170.pcm beep120.pcm beep200.pcm beep170.pcm beep150.pcm beep300.pcm beep250.pcm beep350.pcm beep350.pcm beep120.pcm beep150.pcm beep300.pcm beep120.pcm beep200.pcm beep150.pcm beep200.pcm beep100.pcm beep120.pcm beep100.pcm beep300.pcm beep350.pcm beep200.pcm beep100.pcm beep300.pcm beep200.pcm beep200.pcm beep120.pcm beep250.pcm beep120.pcm beep100.pcm beep150.pcm beep100.pcm beep350.pcm beep120.pcm beep170.pcm beep170.pcm beep250.pcm beep170.pcm beep250.pcm beep100.pcm beep170.pcm beep350.pcm beep250.pcm beep150.pcm beep250.pcm beep200.pcm beep350.pcm beep120.pcm beep100.pcm beep120.pcm beep350.pcm beep100.pcm beep120.pcm beep200.pcm beep170.pcm beep300.pcm beep350.pcm beep100.pcm beep300.pcm beep120.pcm beep350.pcm beep250.pcm beep350.pcm beep150.pcm beep100.pcm > grind.pcm
-        oggenc -o grind.ogg -r grind.pcm
-
-        # order selected by:
-
-        #! /bin/python
-        import random
-        files = ['beep100.pcm', 'beep150.pcm', 'beep200.pcm', 'beep350.pcm', 'beep120.pcm', 'beep170.pcm', 'beep250.pcm', 'beep300.pcm']
-        cmd = 'cat beep100.pcm %s beep100.pcm > grind.pcm'
-        rnd = []
-        for x in range(75):
-            rnd.append(random.choice(files))
-        print cmd % ' '.join(rnd)
-        
-    Generated by Neil Muller, May 2014
-    License: MIT-style permissive license - see LICENSE.txt
-
+See __init__.py and tools/gen_sound.py
diff --git a/data/sounds/__init__.py b/data/sounds/__init__.py
new file mode 100644 (file)
index 0000000..7794093
--- /dev/null
@@ -0,0 +1,25 @@
+import random
+
+from naja.gen_sound import Chunk, scale
+
+
+def grind():
+    yield Chunk('sine', freq=100, length=0.01, volume=25)
+    tones = []
+    for freq in (100, 150, 200, 350, 120, 170, 300):
+        tones.append(Chunk('sine', freq=freq, length=0.01, volume=25))
+    for i in range(75):
+        yield random.choice(tones)
+    yield Chunk('sine', freq=100, length=0.01, volume=25)
+
+
+SOUNDS = {
+    'chirp': scale(1650, 1449, -200, length=0.05, volume=50),
+    'error': Chunk('sine', freq=1000, length=0.25),
+    'grind': grind(),
+    'shutdown': scale(800, 199, -200),
+    'silence': Chunk('silence', length=2),
+    'startup': scale(200, 801, 200),
+    'zoop': scale(500, 800, 20, length=0.01, volume=50),
+    'zzzzz': Chunk('sine', freq=100, length=0.5, volume=50),
+}
index 596a505c7bc9178347a0b32386ca415beef4885d..2def1cfaf0a8f90bddd27dbc085423ad00074ad4 100644 (file)
@@ -1,4 +1,5 @@
 from naja.constants import ACTION_GLYPHS, BITS, CHESS_PIECES
+from naja.sound import sound
 from naja.utils import bit_glyphs, move_glyph
 
 
@@ -141,6 +142,7 @@ class ShiftLocations(LocationAction):
     GLYPHS = (ACTION_GLYPHS.CHANGE_BOARD,)
 
     def perform_action(self, board, location):
+        sound.play_sound('change.ogg')
         board.shift_locations(self.data['direction'])
 
 
@@ -149,6 +151,7 @@ class RotateLocations(LocationAction):
     GLYPHS = (ACTION_GLYPHS.CHANGE_BOARD,)
 
     def perform_action(self, board, location):
+        sound.play_sound('change.ogg')
         board.rotate_locations(self.data['rot_direction'])
 
 
diff --git a/naja/gen_sound.py b/naja/gen_sound.py
new file mode 100644 (file)
index 0000000..8ad0479
--- /dev/null
@@ -0,0 +1,49 @@
+import math
+import struct
+
+
+def gen_sine(freq, secs, volume):
+    """
+    Generate imperfect sine waves
+
+    Design notes. Produces ~= (user requested) s of raw audio
+    We're aiming for an 8-bit'ish effect, so we're going with
+    8125 Hz, 8 bit sampling, but faking it out to
+    CDDA output (44100 Hz, 16 bit signed) for easier conversion to ogg
+    by multiply the value by 256 (after roundin) and repeating it 4 times.
+    """
+    OUTPUT_RATE = 8125
+    # We generate freq cycles and sample that OUTPUT_RATE times
+    per_cycle = OUTPUT_RATE // freq
+    data = []
+    for x in range(per_cycle):
+        rad = float(x) / per_cycle * 2 * math.pi
+        y = 256 * int(volume * math.sin(rad))
+        data.extend([struct.pack('<i', y)] * 4)
+    # This is correct because OUTPUT_RATE = CDDA rate / 4 and we repeat
+    # the samples 4 times, so this works out to CDDA rate
+    for x in range(int(freq * secs)):
+        yield b''.join(data)
+
+
+class Chunk(object):
+    def __init__(self, type, **kwargs):
+        self.type = type
+        self.volume = 95
+        self.length = 0.25
+        for k, v in kwargs.iteritems():
+            setattr(self, k, v)
+
+    def raw(self):
+        if self.type == 'silence':
+            for i in xrange(int(176400 * self.length)):
+                yield '\x00'
+
+        if self.type == 'sine':
+            for data in gen_sine(self.freq, self.length, self.volume):
+                yield data
+
+
+def scale(start, stop, step, type='sine', **kwargs):
+    for freq in range(start, stop, step):
+        yield Chunk(type, freq=freq, **kwargs)
index 7b13a819204f76d7635fcd38b6b31fc78a9734c4..1e9dd69a259f4c5188a4a8cc1296d29bdd8f42bb 100755 (executable)
@@ -1,72 +1,44 @@
 #!/usr/bin/env python
 
-# Generate imperfect sine waves
-#
-# Design notes. Produces ~= (user requested) s of raw audio
-# We're aiming for an 8-bit'ish effect, so we're going with
-# 8125 Hz, 8 bit sampling, but faking it out to
-# CDDA output (44100 Hz, 16 bit signed) for easier conversion to ogg
-# by multiply the value by 256 (after roundin) and repeating it 4 times.
-
+from subprocess import Popen, PIPE
+import os
 import sys
-import math
-import struct
 
-OUTPUT_RATE = 8125
-DEFAULT_VOL = 95
+
+def gen_raw(description):
+    for chunk in description:
+        for blob in chunk.raw():
+            yield blob
 
 
-def gen_sine(freq, secs, volume):
-    filename = 'beep%s.pcm' % freq
-    # We generate freq cycles and sample that OUTPUT_RATE times
-    per_cycle = OUTPUT_RATE // freq
-    data = []
-    for x in range(per_cycle):
-        rad = float(x) / per_cycle * 2 * math.pi
-        y = 256 * int(volume * math.sin(rad))
-        data.extend([struct.pack('<i', y)] * 4)
-    output = open(filename, 'wb')
-    # This is correct because OUTPUT_RATE = CDDA rate / 4 and we repeat
-    # the samples 4 times, so this works out to CDDA rate
-    for x in range(int(freq * secs)):
-        output.write(b''.join(data))
-    output.close()
-    print ('Wrote output to %s' % filename)
+def encode(filename, data):
+    print "Writing %s" % filename
+    p = Popen(('oggenc', '-o', filename, '--raw', '--quiet', '-'),
+              stdin=PIPE, cwd='data/sounds')
+    for blob in data:
+        p.stdin.write(blob)
+    p.stdin.close()
+    assert p.wait() == 0
 
 
-def usage():
-    print ('Unexpected input')
-    print ('Usage gen_sound <freq> [<length>] [<volume>]')
-    print (' where <freq> is the frequency in Hz (int)')
-    print (' [<length>] is the time in seconds (float) - default 0.25')
-    print (' and [<volume>] is the volume (integer between 0 and 127)'
-           ' - default %s' % DEFAULT_VOL)
+def write(basename, description):
+    return encode('%s.ogg' % basename, gen_raw(description))
 
 
-if __name__ == "__main__":
-    try:
-        freq = int(sys.argv[1])
-        if len(sys.argv) > 2:
-            secs = float(sys.argv[2])
-        else:
-            secs = 0.25
-        if len(sys.argv) > 3:
-            volume = int(sys.argv[3])
-        else:
-            volume = DEFAULT_VOL
-    except Exception as exc:
-        usage()
-        print ('Error was: %s' % exc)
-        sys.exit(1)
+def main():
+    sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
+    from data.sounds import SOUNDS
+    from naja.gen_sound import Chunk
 
-    if volume > 128 or volume < 0:
-        usage()
-        print ('Invalid volume: %s' % volume)
-        sys.exit(1)
+    sounds = SOUNDS.keys()
+    if len(sys.argv) > 1:
+        sounds = (sys.argv[1:])
+    for sound in sounds:
+        description = SOUNDS[sound]
+        if isinstance(description, Chunk):
+            description = (description,)
+        write(sound, description)
 
-    if freq > 2000 or freq < 100:
-        usage()
-        print ('Invalid freq: %s' % volume)
-        sys.exit(1)
 
-    gen_sine(freq, secs, volume)
+if __name__ == '__main__':
+    main()