├── .gitignore ├── LICENSE ├── README.md ├── pysinewave ├── __init__.py ├── __main__.py ├── sinewave.py ├── sinewave_generator.py ├── tests │ └── test_utilities.py └── utilities.py ├── requirements.txt └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | #.idea/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 The Python Packaging Authority 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PySineWave 2 | 3 | PySineWave offers an easy way to generate and play sine waves that can make smooth, continuous transitions in pitch and volume in real time. These sine waves are created, threaded and played (fed into a sound channel) behind the scenes. All you have to do is create SineWaves and call their easy-to-use functions! 4 | 5 | ## Installation 6 | Open the terminal, and type: 7 | ```python 8 | pip install pysinewave 9 | ``` 10 | 11 | ## Code Example 12 | This code will play a sinewave that smoothly decreases its pitch. 13 | ```python 14 | import time 15 | 16 | from pysinewave import SineWave 17 | 18 | # Create a sine wave, with a starting pitch of 12, and a pitch change speed of 10/second. 19 | sinewave = SineWave(pitch = 12, pitch_per_second = 10) 20 | 21 | # Turn the sine wave on. 22 | sinewave.play() 23 | 24 | # Sleep for 2 seconds, as the sinewave keeps playing. 25 | time.sleep(2) 26 | 27 | # Set the goal pitch to -5. 28 | sinewave.set_pitch(-5) 29 | 30 | # Sleep for 3 seconds, as the sinewave smoothly slides its pitch down from 12 to -5, and stays there. 31 | time.sleep(3) 32 | ``` 33 | 34 | ## Useful Functions 35 | 36 | Use `SineWave.set_pitch(pitch)` to change the pitch of a SineWave object. The SineWave object will smoothly transition to this new pitch at a rate of `SineWave.pitch_per_second`. 37 | 38 | Use `SineWave.set_volume(decibels)` to change the volume of a SineWave object. The SineWave object will smoothly transition to this new volume at a rate of `SineWave.decibels_per_second`. 39 | 40 | Use `SineWave.play()` and `SineWave.stop()` to start and stop the SineWave, respectively. 41 | 42 | Use `SineWave.set_pitch_per_second(pitch_per_second)` and `SineWave.set_decibels_per_second(decibels_per_second)` to change the values of `SineWave.pitch_per_second` and `SineWave.decibels_per_second`, respectively. 43 | 44 | ## Channel management 45 | 46 | You are provided the ablility to output stereo audio. To do so, specify `channels=2` when instanciating your SineWave object. In stereo mode, you have the possibility to choose which specific channel will output SineWave. 47 | 48 | There are 3 available parameters : `'lr'` which stands for "left-right" and outputs the audio in both left and right channels, `'l'` for "left", which only outputs audio in the left channel and `'r'` which stands for — you guessed it — "right" which outputs audio only in the right channel. 49 | 50 | ## A Note on Pitch and Volume 51 | You may want to directly modify the frequency and amplitude of a SineWave. We do provide two alternative functions, `SineWave.set_frequency(hertz)` and `SineWave.set_amplitude(percent)`, however we suggest that you use `SineWave.set_pitch(pitch)` and `SineWave.set_volume(decibels)` instead. 52 | 53 | Why? The brain naturally perceives *ratios* between sound's frequency and amplitude much better than differences. This means that working directly with frequency will cause high frequencies to be much harder to distinguish than low frequencies. Similarly for amplitude. 54 | 55 | The conversion between pitch and frequency (in Hz) is: **frequency = 440 * 2^((pitch-9)/12)**. For instance, note that a pitch of 0 is middle C, i.e. a frequency of 261.63 Hz. 56 | 57 | The conversion between volume (in decibels) and amplitude is: **amplitude = 2^(volume/10)**. For instance, increasing the volume by 10 decibels doubles the amplitude of the sine wave. 58 | 59 | Here's a helpful table showing the relationship between frequency, pitch, and musical notes for one octave: 60 | 61 | | Pitch | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 62 | |:---------:|:------:|:------:|:------:|:------:|:------:|:------:|:------:|:------:|:------:|:------:|:------:|:------:|:------:| 63 | | Frequency | 261.63 | 277.18 | 293.66 | 311.13 | 329.63 | 349.23 | 369.99 | 392.00 | 415.30 | 440.00 | 466.16 | 493.88 | 523.25 | 64 | | Note | C | C#/Db | D | D#/Eb | E | F | F#/Gb | G | G#/Ab | A | A#/Bb | B | C | 65 | 66 | If you don't know anything about music theory, no worries! Just be sure to stick to `SineWave.set_pitch(pitch)` and `SineWave.set_volume(decibels)`. Your Python projects will thank you. 67 | -------------------------------------------------------------------------------- /pysinewave/__init__.py: -------------------------------------------------------------------------------- 1 | from pysinewave.sinewave import SineWave 2 | from pysinewave.sinewave_generator import SineWaveGenerator -------------------------------------------------------------------------------- /pysinewave/__main__.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from pysinewave import SineWave 4 | 5 | fur_elise = [ 12,11,12,11,12,7,10,8, 6 | 5,5,5,-3,0,5,7,7,7,0,4,7,8,8,8,0,12,11,12,11,12,7,10,8, 7 | 5,5,5,-3,0,5,7,7,7,0,8,7,5,5,5,5] 8 | 9 | o_christmas_tree = [-5,-5, 0, 0, 0, 0, 2, 2, 4, 4, 4, 4, 4, 4, 2, 4, 5, 5, -1, -1, 2, 2, 0, 0, 10 | -5,-5, 0, 0, 0, 0, 2, 2, 4, 4, 4, 4, 4, 4, 2, 4, 5, 5, -1, -1, 2, 2, 0, 0, 11 | 7, 7, 4, 9, 9, 9, 7, 7, 5, 5, 5, 5, 5, 5, 2, 7, 7, 7, 5, 5, 4, 4, 4, 4, 12 | -5,-5, 0, 0, 0, 0, 2, 2, 4, 4, 4, 4, 4, 4, 2, 4, 5, 5, -1, -1, 2, 2, 0, 0] 13 | 14 | white_christmas = [4,4,4,4,5,4,3,4,5,5,5,5,6,7,7,7,7,9,11,12,14,12,11,9,7,7,7,7,7,7,0,2, 15 | 4,3,4,3,4,9,9,7,0,-1,0,-1,0,7,7,5,4,4,4,4,5,4,2,0,2,2,2,2,2,2,2,2, 16 | 4,4,4,4,5,4,3,4,5,5,5,5,6,7,7,7,7,9,11,12,14,12,11,9,7,7,7,7,7,7,0,2, 17 | 4,3,4,3,4,9,9,7,12,12,12,12,12,12,0,2,4,4,4,4,7,2,2,-5,0] 18 | 19 | 20 | def play_song(song, note_per_second=4): 21 | 22 | sinewave = SineWave(song[0],24) 23 | 24 | sinewave.play() 25 | 26 | for pitch in song: 27 | sinewave.set_pitch(pitch) 28 | time.sleep(1/note_per_second) 29 | 30 | #play_song(white_christmas, 2) 31 | play_song(o_christmas_tree, 2) 32 | -------------------------------------------------------------------------------- /pysinewave/sinewave.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import numpy as np 4 | import sounddevice as sd 5 | 6 | from pysinewave import utilities 7 | from pysinewave import sinewave_generator 8 | 9 | class SineWave: 10 | '''Generates and plays a continuous sinewave, with smooth transitions in frequency (pitch) 11 | and amplitude (volume).''' 12 | 13 | def __init__(self, pitch=0, pitch_per_second=12, decibels=0, decibels_per_second=1, channels=1, channel_side="lr", 14 | samplerate=utilities.DEFAULT_SAMPLE_RATE): 15 | 16 | self.sinewave_generator = sinewave_generator.SineWaveGenerator( 17 | pitch=pitch, pitch_per_second=pitch_per_second, 18 | decibels = decibels, decibels_per_second=decibels_per_second, 19 | samplerate=samplerate) 20 | 21 | # Create the output stream 22 | self.output_stream = sd.OutputStream(channels=channels, callback= lambda *args: self._callback(*args), 23 | samplerate=samplerate) 24 | 25 | self.channels = channels 26 | 27 | if channel_side == 'r': 28 | self.channel_side = 0 29 | elif channel_side == 'l': 30 | self.channel_side = 1 31 | else: self.channel_side = -1 32 | 33 | def _callback(self, outdata, frames, time, status): 34 | '''Callback function for the output stream.''' 35 | # Print any error messages we receive 36 | if status: 37 | print(status, file=sys.stderr) 38 | 39 | # Get and use the sinewave's next batch of data 40 | data = self.sinewave_generator.next_data(frames) 41 | outdata[:] = data.reshape(-1, 1) 42 | 43 | # Output on the given channel 44 | if self.channel_side != -1 and self.channels == 2: 45 | outdata[:, self.channel_side] = 0.0 46 | 47 | 48 | 49 | 50 | def play(self): 51 | '''Plays the sinewave (in a separate thread). Changes in frequency or amplitude will transition smoothly.''' 52 | self.output_stream.start() 53 | 54 | def stop(self): 55 | '''If the sinewave is playing, stops the sinewave.''' 56 | self.output_stream.stop() 57 | 58 | def set_frequency(self, frequency): 59 | '''Sets the goal frequency of the sinewave, which will be smoothly transitioned to.''' 60 | self.sinewave_generator.set_frequency(frequency) 61 | 62 | def set_pitch(self, pitch): 63 | '''Sets the goal pitch of the sinewave (relative to middle C), 64 | which will be smoothly transitioned to.''' 65 | self.sinewave_generator.set_pitch(pitch) 66 | 67 | def set_volume(self, volume): 68 | '''Sets the goal volume (in decibels, relative to medium volume) of the sinewave''' 69 | self.sinewave_generator.set_decibels(volume) 70 | -------------------------------------------------------------------------------- /pysinewave/sinewave_generator.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from pysinewave import utilities 4 | 5 | class SineWaveGenerator: 6 | '''Generates continuous sine wave data that smoothly transitions between pitches and volumes. 7 | (For simplicity, use SineWave instead. 8 | SineWaveGenerator is included to allow for alternative uses of generated sinewave data.)''' 9 | 10 | def __init__(self, pitch, pitch_per_second=12, decibels=1, decibels_per_second=1, 11 | samplerate=utilities.DEFAULT_SAMPLE_RATE): 12 | self.frequency = utilities.pitch_to_frequency(pitch) 13 | self.phase = 0 14 | self.amplitude = utilities.decibels_to_amplitude_ratio(decibels) 15 | 16 | self.pitch_per_second = pitch_per_second 17 | self.decibels_per_second = decibels_per_second 18 | self.goal_frequency = self.frequency 19 | self.goal_amplitude = self.amplitude 20 | self.samplerate = samplerate 21 | 22 | def new_frequency_array(self, time_array): 23 | '''Calcululate the frequency values for the next chunk of data.''' 24 | dir = utilities.direction(self.frequency, self.goal_frequency) 25 | new_frequency = self.frequency * utilities.interval_to_frequency_ratio( 26 | dir * self.pitch_per_second * time_array) 27 | return utilities.bounded_by_end(new_frequency, self.frequency, self.goal_frequency) 28 | 29 | def new_amplitude_array(self, time_array): 30 | '''Calcululate the amplitude values for the next chunk of data.''' 31 | dir = utilities.direction(self.amplitude, self.goal_amplitude) 32 | new_amplitude = self.amplitude * utilities.decibels_to_amplitude_ratio( 33 | dir * self.decibels_per_second * time_array) 34 | return utilities.bounded_by_end(new_amplitude, self.amplitude, self.goal_amplitude) 35 | 36 | def new_phase_array(self, new_frequency_array, delta_time): 37 | '''Calcululate the phase values for the next chunk of data, given frequency values''' 38 | return self.phase + np.cumsum(new_frequency_array * delta_time) 39 | 40 | def set_frequency(self, frequency): 41 | '''Set the goal frequency that the sinewave will gradually shift towards.''' 42 | self.goal_frequency = frequency 43 | 44 | def set_pitch(self, pitch): 45 | '''Set the goal pitch that the sinewave will gradually shift towards.''' 46 | self.goal_frequency = utilities.pitch_to_frequency(pitch) 47 | 48 | def set_amplitude(self, amplitude): 49 | '''Set the amplitude that the sinewave will gradually shift towards.''' 50 | self.goal_amplitude = amplitude 51 | 52 | def set_decibels(self, decibels): 53 | '''Set the amplitude (in decibels) that the sinewave will gradually shift towards.''' 54 | self.goal_amplitude = utilities.decibels_to_amplitude_ratio(decibels) 55 | 56 | def next_data(self, frames): 57 | '''Get the next pressure array for the given number of frames''' 58 | 59 | # Convert frame information to time information 60 | time_array = utilities.frames_to_time_array(0, frames, self.samplerate) 61 | delta_time = time_array[1] - time_array[0] 62 | 63 | # Calculate the frequencies of this batch of data 64 | new_frequency_array = self.new_frequency_array(time_array) 65 | 66 | # Calculate the phases 67 | new_phase_array = self.new_phase_array(new_frequency_array, delta_time) 68 | 69 | # Calculate the amplitudes 70 | new_amplitude_array = self.new_amplitude_array(time_array) 71 | 72 | # Create the sinewave array 73 | sinewave_array = new_amplitude_array * np.sin(2*np.pi*new_phase_array) 74 | 75 | # Update frequency and amplitude 76 | self.frequency = new_frequency_array[-1] 77 | self.amplitude = new_amplitude_array[-1] 78 | 79 | # Update phase (getting rid of extra cycles, so we don't eventually have an overflow error) 80 | self.phase = new_phase_array[-1] % 1 81 | 82 | #print('Frequency: {0} Phase: {1} Amplitude: {2}'.format(self.frequency, self.phase, self.amplitude)) 83 | 84 | return sinewave_array -------------------------------------------------------------------------------- /pysinewave/tests/test_utilities.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import math 3 | 4 | from pysinewave import utilities 5 | 6 | # def test_bounded(): 7 | # # Out-of-bounds values snap to closest bound 8 | # assert utilities.bounded(6, -2, 3) == 3 9 | # assert utilities.bounded(-10, -2, 3) == -2 10 | # # Regardless of order 11 | # assert utilities.bounded(6, 3, -2) == 3 12 | # # Inside-bounds values remain unchanged 13 | # assert utilities.bounded(1, 3, -2) == 1 14 | 15 | def test_bounded_by_end(): 16 | # Bounds by end and only end, when end > start 17 | assert (utilities.bounded_by_end([1,2,3,4,5], 3, 4) == [1,2,3,4,4]).all() 18 | # Bounds by end and only end, when end < start 19 | assert (utilities.bounded_by_end([1,2,3,4,5], 3, 2) == [2,2,3,4,5]).all() 20 | 21 | def test_pitch_to_frequency(): 22 | # 0 is middle C 23 | assert utilities.pitch_to_frequency(0) == utilities.MIDDLE_C_FREQUENCY 24 | # 9 is middle A (440) 25 | assert math.isclose(utilities.pitch_to_frequency(9), 440, rel_tol=0.1) -------------------------------------------------------------------------------- /pysinewave/utilities.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | # Default frames per second 4 | DEFAULT_SAMPLE_RATE = 44100 5 | 6 | # Frequency of a middle C 7 | MIDDLE_C_FREQUENCY = 261.625565 8 | 9 | def direction(start, end): 10 | '''Returns 1 if end > start, and -1 if end < start.''' 11 | return 1 if end > start else -1 12 | 13 | def bounded_by_end(value, start, end): 14 | '''Returns value if value is closer to start than end is, otherwise returns end.''' 15 | if start < end: 16 | return np.minimum(value, end) 17 | else: 18 | return np.maximum(value, end) 19 | 20 | def frames_to_time(frames, framerate): 21 | '''Convert frame count to time (using framerate).''' 22 | return frames / framerate 23 | 24 | def frames_to_time_array(start_frame, frames, framerate): 25 | '''Convert frame information into a time array.''' 26 | # Convert frame info to time info 27 | start_time = frames_to_time(start_frame, framerate) 28 | end_time = frames_to_time(start_frame + frames, framerate) 29 | 30 | # Create time array with one entry for each frame 31 | time_array = np.linspace(start_time, end_time, frames, endpoint=False) 32 | return time_array 33 | 34 | def sinewave(frequency, time): 35 | '''Create a sinewave array for a sinewave of given constant frequency.''' 36 | return np.sin(2 * np.pi * time * frequency) 37 | 38 | def interval_to_frequency_ratio(interval): 39 | '''The frequency of the given pitch (in Hz), relative to middle C''' 40 | return 2**(interval/12) 41 | 42 | def pitch_to_frequency(pitch): 43 | '''The frequency of the given pitch (in Hz), relative to middle C''' 44 | return MIDDLE_C_FREQUENCY * 2**(pitch/12) 45 | 46 | def decibels_to_amplitude_ratio(decibels): 47 | '''The ratio between two amplitudes given a decibel change''' 48 | return 2**(decibels / 10) -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | astroid==2.3.3 2 | attrs==19.3.0 3 | cffi==1.13.2 4 | isort==4.3.21 5 | lazy-object-proxy==1.4.3 6 | mccabe==0.6.1 7 | more-itertools==8.0.2 8 | numpy==1.18.0 9 | packaging==19.2 10 | pluggy==0.13.1 11 | py==1.10.0 12 | pycparser==2.19 13 | pylint==2.4.4 14 | pyparsing==2.4.5 15 | pytest==5.3.2 16 | rope==0.14.0 17 | six==1.13.0 18 | sounddevice==0.3.14 19 | wcwidth==0.1.7 20 | wrapt==1.11.2 21 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | with open("README.md", "r") as fh: 4 | long_description = fh.read() 5 | 6 | setuptools.setup( 7 | name="pysinewave", 8 | version="0.0.7", 9 | author="David Davini", 10 | author_email="daviddavini@g.ucla.edu", 11 | description="Generate and play sine waves in real time, that can make smooth, continuous transitions in pitch and volume.", 12 | long_description=long_description, 13 | long_description_content_type="text/markdown", 14 | url="https://github.com/daviddavini/pysinewave", 15 | project_urls={ 16 | 'Source': 'https://github.com/daviddavini/pysinewave', 17 | 'Tracker': 'https://github.com/daviddavini/pysinewave/issues', 18 | }, 19 | packages=setuptools.find_packages(), 20 | classifiers=[ 21 | "Programming Language :: Python :: 3", 22 | "License :: OSI Approved :: MIT License", 23 | "Operating System :: OS Independent", 24 | ], 25 | install_requires=[ 26 | 'numpy', 27 | 'sounddevice', 28 | ], 29 | python_requires='>=3.6', 30 | ) --------------------------------------------------------------------------------