From a88ecf55639d551f7a9ef24a29c8392db190ebc8 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Thu, 6 Sep 2018 09:16:29 -0400 Subject: [PATCH] play chords! --- Pipfile | 2 ++ Pipfile.lock | 72 +++++++++++++++++++++++++++++++++++++++++++++--- pytheory/core.py | 1 + pytheory/play.py | 61 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 132 insertions(+), 4 deletions(-) create mode 100644 pytheory/play.py diff --git a/Pipfile b/Pipfile index b83ab34..2c006c8 100644 --- a/Pipfile +++ b/Pipfile @@ -6,6 +6,8 @@ name = "pypi" [packages] pytuning = "*" numeral = "*" +pygame = "*" +scipy = "*" [dev-packages] pytest = "*" diff --git a/Pipfile.lock b/Pipfile.lock index 23899f3..032f6b7 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "ab92ebe81a8de7d7b1e4cde82bf3ad82c995dcf5d6cdfc3e9d7f1f86bbc6210d" + "sha256": "b26c1f52ba816c24a18d541485dc99c1f1afe5d03bf5a3db3491d374d780e644" }, "pipfile-spec": 6, "requires": { @@ -63,6 +63,36 @@ "markers": "python_version != '3.0.*' and python_version != '3.2.*' and python_version != '3.3.*' and python_version >= '2.7' and python_version != '3.1.*'", "version": "==1.15.1" }, + "pygame": { + "hashes": [ + "sha256:06dc92ccfea33b85f209db3d49f99a2a30c88fe9fb80fa2564cee443ece787b5", + "sha256:0919a2ec5fcb0d00518c2a5fa99858ccf22d7fbcc0e12818b317062d11386984", + "sha256:0a8c92e700e0042faefa998fa064616f330201890d6ea1c993eb3ff30ab53e99", + "sha256:220a1048ebb3d11a4d48cc4219ec8f65ca62fcafd255239478677625e8ead2e9", + "sha256:315861d2b8428f7b4d56d2c98d6c1acc18f08c77af4b129211bc036774f64be2", + "sha256:3469e87867832fe5226396626a8a6a9dac9b2e21a7819dd8cd96cf0e08bbcd41", + "sha256:54c19960180626165512d596235d75dc022d38844467cec769a8d8153fd66645", + "sha256:5ba598736ab9716f53dc943a659a9578f62acfe00c0c9c5490f3aca61d078f75", + "sha256:60ddc4f361babb30ff2d554132b1f3296490f3149d6c1c77682213563f59937a", + "sha256:6a49ab8616a9de534f1bf62c98beabf0e0bb0b6ff8917576bba22820bba3fdad", + "sha256:6d4966eeba652df2fd9a757b3fc5b29b578b47b58f991ad714471661ea2141cb", + "sha256:700d1781c999af25d11bfd1f3e158ebb660f72ebccb2040ecafe5069d0b2c0b6", + "sha256:73f4c28e894e76797b8ccaf6eb1205b433efdb803c70f489ebc3db6ac9c097e6", + "sha256:786eca2bea11abd924f3f67eb2483bcb22acff08f28dbdbf67130abe54b23797", + "sha256:7bcf586a1c51a735361ca03561979eea3180de45e6165bcdfa12878b752544af", + "sha256:82a1e93d82c1babceeb278c55012a9f5140e77665d372a6d97ec67786856d254", + "sha256:9e03589bc80a21ae951fca7659a767b7cac668289937e3756c0ab3d753cf6d24", + "sha256:aa8926a4e34fb0943abe1a8bb04a0ad82265341bf20064c0862db0a521100dfc", + "sha256:aa90689b889c417d2ac571ef2bbb5f7e735ae30c7553c60fae7508404f46c101", + "sha256:c9f8cdefee267a2e690bf17d61a8f5670b620f25a981f24781b034363a8eedc9", + "sha256:d9177afb2f46103bfc28a51fbc49ce18987a857e5c934db47b4a7030cb30fbd0", + "sha256:deb0551d4bbfb8131e2463a7fe1943bfcec5beb11acdf9c4bfa27fa5a9758d62", + "sha256:e7edfe57a5972aa9130ce9a186020a0f097e7a8e4c25e292109bdae1432b77f9", + "sha256:f0ad32efb9e26160645d62ba6cf3e5a5828dc4e82e8f41f9badfe7b685b07295" + ], + "index": "pypi", + "version": "==1.9.4" + }, "pytuning": { "hashes": [ "sha256:50b16a339351c902fdcd1ffc038d25fd537959b316ec8621fe4f6009de4fa67f" @@ -70,6 +100,40 @@ "index": "pypi", "version": "==0.7.2" }, + "scipy": { + "hashes": [ + "sha256:0611ee97296265af4a21164a5323f8c1b4e8e15c582d3dfa7610825900136bb7", + "sha256:08237eda23fd8e4e54838258b124f1cd141379a5f281b0a234ca99b38918c07a", + "sha256:0e645dbfc03f279e1946cf07c9c754c2a1859cb4a41c5f70b25f6b3a586b6dbd", + "sha256:0e9bb7efe5f051ea7212555b290e784b82f21ffd0f655405ac4f87e288b730b3", + "sha256:108c16640849e5827e7d51023efb3bd79244098c3f21e4897a1007720cb7ce37", + "sha256:340ef70f5b0f4e2b4b43c8c8061165911bc6b2ad16f8de85d9774545e2c47463", + "sha256:3ad73dfc6f82e494195144bd3a129c7241e761179b7cb5c07b9a0ede99c686f3", + "sha256:3b243c77a822cd034dad53058d7c2abf80062aa6f4a32e9799c95d6391558631", + "sha256:404a00314e85eca9d46b80929571b938e97a143b4f2ddc2b2b3c91a4c4ead9c5", + "sha256:423b3ff76957d29d1cce1bc0d62ebaf9a3fdfaf62344e3fdec14619bb7b5ad3a", + "sha256:42d9149a2fff7affdd352d157fa5717033767857c11bd55aa4a519a44343dfef", + "sha256:625f25a6b7d795e8830cb70439453c9f163e6870e710ec99eba5722775b318f3", + "sha256:698c6409da58686f2df3d6f815491fd5b4c2de6817a45379517c92366eea208f", + "sha256:729f8f8363d32cebcb946de278324ab43d28096f36593be6281ca1ee86ce6559", + "sha256:8190770146a4c8ed5d330d5b5ad1c76251c63349d25c96b3094875b930c44692", + "sha256:878352408424dffaa695ffedf2f9f92844e116686923ed9aa8626fc30d32cfd1", + "sha256:8b984f0821577d889f3c7ca8445564175fb4ac7c7f9659b7c60bef95b2b70e76", + "sha256:8f841bbc21d3dad2111a94c490fb0a591b8612ffea86b8e5571746ae76a3deac", + "sha256:c22b27371b3866c92796e5d7907e914f0e58a36d3222c5d436ddd3f0e354227a", + "sha256:d0cdd5658b49a722783b8b4f61a6f1f9c75042d0e29a30ccb6cacc9b25f6d9e2", + "sha256:d40dc7f494b06dcee0d303e51a00451b2da6119acbeaccf8369f2d29e28917ac", + "sha256:d8491d4784aceb1f100ddb8e31239c54e4afab8d607928a9f7ef2469ec35ae01", + "sha256:dfc5080c38dde3f43d8fbb9c0539a7839683475226cf83e4b24363b227dfe552", + "sha256:e24e22c8d98d3c704bb3410bce9b69e122a8de487ad3dbfe9985d154e5c03a40", + "sha256:e7a01e53163818d56eabddcafdc2090e9daba178aad05516b20c6591c4811020", + "sha256:ee677635393414930541a096fc8e61634304bb0153e4e02b75685b11eba14cae", + "sha256:f0521af1b722265d824d6ad055acfe9bd3341765735c44b5a4d0069e189a0f40", + "sha256:f25c281f12c0da726c6ed00535ca5d1622ec755c30a3f8eafef26cf43fede694" + ], + "index": "pypi", + "version": "==1.1.0" + }, "sympy": { "hashes": [ "sha256:286ca070d72e250861dea7a21ab44f541cb2341e8268c70264cf8642dbd9225f" @@ -119,11 +183,11 @@ }, "pytest": { "hashes": [ - "sha256:2d7c49e931316cc7d1638a3e5f54f5d7b4e5225972b3c9838f3584788d27f349", - "sha256:ad0c7db7b5d4081631e0155f5c61b80ad76ce148551aaafe3a718d65a7508b18" + "sha256:453cbbbe5ce6db38717d282b758b917de84802af4288910c12442984bde7b823", + "sha256:a8a07f84e680482eb51e244370aaf2caa6301ef265f37c2bdefb3dd3b663f99d" ], "index": "pypi", - "version": "==3.7.4" + "version": "==3.8.0" }, "rope": { "hashes": [ diff --git a/pytheory/core.py b/pytheory/core.py index 98fdf30..198553c 100644 --- a/pytheory/core.py +++ b/pytheory/core.py @@ -5,3 +5,4 @@ from .systems import System, SYSTEMS from .scales import Scale, TonedScale from .chords import Chord, Fretboard from .charts import CHARTS, charts_for_fretboard +from .play import play diff --git a/pytheory/play.py b/pytheory/play.py new file mode 100644 index 0000000..6074506 --- /dev/null +++ b/pytheory/play.py @@ -0,0 +1,61 @@ +from enum import Enum +import numpy +import scipy.signal + +import contextlib +with contextlib.redirect_stdout(None): + import pygame + +from .tones import Tone + +SAMPLE_RATE = 44_100 +SAMPLE_PEAK = 4_096 + +pygame.mixer.pre_init(SAMPLE_RATE, -16, 1) +pygame.mixer.init() + +def sine_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE): + """Compute N samples of a sine wave with given frequency and peak amplitude. + Defaults to one second. + """ + length = SAMPLE_RATE / float(hz) + omega = numpy.pi * 2 / length + xvalues = numpy.arange(int(length)) * omega + onecycle = peak * numpy.sin(xvalues) + return numpy.resize(onecycle, (n_samples,)).astype(numpy.int16) + +def sawtooth_wave(hz, peak=SAMPLE_PEAK, rising_ramp_width=1, n_samples=SAMPLE_RATE): + """Compute N samples of a sine wave with given frequency and peak amplitude. + Defaults to one second. + rising_ramp_width is the percentage of the ramp spend rising: + .5 is a triangle wave with equal rising and falling times. + """ + t = numpy.linspace(0, 1, 500 * 440/hz, endpoint=False) + wave = scipy.signal.sawtooth(2 * numpy.pi * 5 * t, width=rising_ramp_width) + wave = numpy.resize(wave, (n_samples,)) + # Sawtooth waves sound very quiet, so multiply peak by 4. + return (peak * 6 * wave.astype(numpy.int16)) + + +def _play_for(sample_wave, ms): + """Play the given NumPy array, as a sound, for ms milliseconds.""" + sound = pygame.sndarray.make_sound(sample_wave) + sound.play(-1) + pygame.time.delay(ms) + sound.stop() + + +class Synth(Enum): + SINE = sine_wave + SAW = sawtooth_wave + +def play(tone_or_chord, temperament='equal', synth=Synth.SINE, t=1_000): + + if isinstance(tone_or_chord, Tone): + chord = [synth(tone_or_chord.pitch(temperament=temperament, symbolic=True))] + else: + chord = [synth(tone.pitch(temperament=temperament, symbolic=True)) for tone in tone_or_chord.tones] + + _play_for(sum(chord), ms=t) + +# 69 + 12*np.log2(hz_nonneg/440.)