# graph2sound

This is a Python library for adding some auditory feedback to your graphs. Alternatively, this library has capabilities such as creating smooth variable-frequency waveforms or playing simple MIDI-tones.

## Installation
Use `pip install graph2sound` to install the module.\
Alternatively, download `graph2sound.py` and save it in your working directory.


## Dependencies
`numpy` for array creation and modification.\
`scipy` for signal modification and interpolation.\
`sounddevice` is required for playback. However, as the wav + samplerate can be extracted with `graph2wav`, it is not strictly needed.

## Usage
Depending on what you want to do, g2s may be called as a single function. These functions actually chain some subroutines together to assemble and play the final tone.\
The general syntax is as follows:
```
import graph2sound as g2s
g2s.graph2sound(y[,x],[t=,r=,s=,l=,h=,w=,f=])
```

### Demo
Test g2s by `g2s.graph2sound()`. Plays a demo sound.

### Single y series
At the very least, g2s requires a single data series consisting of two points or more.
```
y = [i**2 for i in range(10)]
g2s.graph2sound(y)
```
This will play a tone which increases in frequency quadratically. Every point in the series is played for an equal amount of time (linear time mapping).


### Single x y series
If you have an associated time series, for example a result from an ODE solver, you may also map your data points to time. Note: the lengths of the two series must be equal.
```
import numpy as np
t = np.logspace(0.1,1,100)
y = np.sin(t)
g2s.graph2sound(y,t)
```
This will play a sound which steadily increases in frequency, as opposed to a logarithmic increase in frequency.

### Two-dimensional y series
If you have a series of y data which is 2D, g2s will automatically convert all rows to wavs and adds them.
```
y = [[i*j for i in range(5)] for j in range(5)]
g2s.graph2sound(y)
```
This will play the normalized summed waveforms, row major.

### Keyword use and more examples
```
import pandas as pd
d = {'x':[0,0.1,0.2,3],'y':[0,0,0,1]}
df = pd.DataFrame(d)
g2s.graph2sound(df['y'],df['x'],t=1.5,l=440,h=880,w='sawtooth')
```
Plays from a DataFrame. Has a 1.5 second duration, the lowest/highest tones are 440 and 880 and the synth uses a sawtooth wave.

```
from scipy import signal
import numpy as np
x = np.linspace(0,20)
y = signal.square(x)
g2s.graph2sound(y,x,w='square')
```
This is what a square square wave sounds like.

### Keywords
`g2s.graph2sound` takes multiple optional keywords:\
`t = float,int` Default: 2 seconds. Determines the length of the tone played.\
`r = int` Default: 44100. Sample rate. Determines the samples per second which are fed into the audio library.\
`s = int` Default: 100*t. The number of segments the tone is divided in. Essentially: how many frequency changes the tone has.\
`l = float` Default: 130.813 Hz. C3. This is the lowest tone the y series will be mapped to.\
`h = float` Default: 2093.005 Hz. C7. This is the highest tone the y series will be mapped to.

`w =`\
`sine` Outputs a sine waveform (default).\
`square` Outputs a square waveform.\
`sawtooth` Outputs a sawtooth waveform.\
`f =`\
`fifth` Adds a perfect fifth overtone.\
`yaranaika` HARMONY\
`MATLAB` plays the sound like ye olde days.

### Low-level functions
Read the docs (TODO) to use the underlying functions.\
`wave,samplerate = g2s.graph2wave` takes in the arguments mentioned above and converts it to the required waveform. Assembles it into a ready-to-play array. Returns the waveform and the samplerate used.\
`g2s.wave2sound(wave,samplerate)` takes the arguments of the previous function and plays them back.\
`y,x = g2s.argskwargcleanup(*args,**kwargs)` verifies the x/y data format to it can be used in the rest of the functions.\
`f_list = g2s.freqlist(y,x,frequency_low,frequency_high,segments)` creates a frequency table which is required to synthesize the desired tone.\
`wave,samplerate = g2s.synth(time,samplerate,segments,frequency_list,**kwargs)` is the meat and potatoes of this module. Given information on how the samplerate and the segments look like, synthesizes a continuous waveform with the desired samplerate.\
`freq = g2s.midi2freq(mnn)` takes in a [midi note number](https://www.cs.cmu.edu/~music/cmsip/readings/Standard-MIDI-file-format-updated.pdf) (not from MIDI.org, needs login) and returns the frequency. A4 = 69 = 440 Hz.\
`mnn = g2s.freq2midi(freq)` takes in a frequency and returns the midi note number as specified above.\
`mnn = g2s.midi2midi(mnn,delta_freq=0)` changes the midi number by a given frequency. Does not output ints or check if the number is a real note or not.\
`freq = g2s.freq2freq(freq,delta_mnn=0)` changes the frequency by a midi note interval.



## Support
Shit's broken? Merge requests are open.


## License
MIT


## Why?
In one of my bachelors' courses (control theory), I was fucking around with MATLAB (hallowed be thy name) and at a certain point, I thought to myself:

> *"This graph needs sound"*\
> \- Crossed Omega, 2018

Obviously, this Lovecraftian horror of an idea took digital form rather quickly. After ample time, the first iteration was done - which sounded pretty horrible<sup>1</sup>. For the next three years, it remained untouched, left stewing, the entity itself anticipating a return. As I learnt Python, one of the first things I did, after giving the IT department a major headache, was rewrite graph2sound to be used as a Python library. Alas, my attempt to do so was not thwarted and thus stands before you an *academic shitpost*<sup>TM</sup> of genuine Dutch quality.\
<sup>1</sup>: You can actually listen to what it sounded like by passing the MATLAB filter keyword `f=MATLAB`.

## TODO
- ~~Add a small amount of filters to modify the waveforms~~
- ~~Rewrite the synth function, some of the arguments are redundant~~
- Add an LFO
- ~~Add a sound library checker~~
- ~~Add proper data sanitation~~
- ~~Add multi-dimensional sounds~~
- (Never ever) Rewrite this as a matplotlib backend

## Known bugs
- The synth function bugs out at some edge cases: for a function with the highest point at t=end, the final frequency is never played. Requires changes to the frequency list function.
- The data validation misses lists such as [1,[1,2,3]] as a check for raggedness. This development is a pain in the bum.
- The raggedness of arrays is still not handled perfectly, as ragged arrays cause all kinds of different errors.
- The returned datatype of the ...2midi functions is a float; this should be an int and checked for if the frequency is actually a midi note.
- The generated wav may start and end with a non-zero loudness. This can cause "popping", which could be fixed by ramping the amplitude up and down by either adding a ramp before and after or multiplying the beginning and ending of the wav with an exponential curve.
