diff --git a/pyproject.toml b/pyproject.toml index 636b9b2..5698d20 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ requires-python = ">=3.10" dependencies = [ "pytuning", "numeral", - "pygame", + "sounddevice", "scipy", ] diff --git a/pytheory/play.py b/pytheory/play.py index 2c884c7..dfd7d46 100644 --- a/pytheory/play.py +++ b/pytheory/play.py @@ -1,19 +1,17 @@ from enum import Enum import numpy import scipy.signal - -import contextlib -with contextlib.redirect_stdout(None): - import pygame +import sounddevice as sd from .tones import Tone SAMPLE_RATE = 44_100 SAMPLE_PEAK = 4_096 + 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. + Defaults to one second. """ length = SAMPLE_RATE / float(hz) omega = numpy.pi * 2 / length @@ -21,41 +19,48 @@ def sine_wave(hz, peak=SAMPLE_PEAK, n_samples=SAMPLE_RATE): 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. + 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) + 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)) + 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() + # Convert milliseconds to seconds + seconds = ms / 1000 + + # sounddevice expects float32 samples between -1 and 1 + normalized_wave = sample_wave.astype(numpy.float32) / SAMPLE_PEAK + + # Play the audio and wait + sd.play(normalized_wave, SAMPLE_RATE) + sd.wait() class Synth(Enum): SINE = sine_wave SAW = sawtooth_wave -def play(tone_or_chord, temperament='equal', synth=Synth.SINE, t=1_000): - - pygame.mixer.pre_init(SAMPLE_RATE, -16, 1) - pygame.mixer.init() +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] + 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.)