Metadata-Version: 2.1
Name: taped
Version: 0.0.4
Summary: Python's serene audio accessor
Home-page: https://github.com/otosense/taped
Author: OtoSense
License: apache-2.0
Keywords: audio,microphone,sound,stream2py
Platform: any
Description-Content-Type: text/markdown
Requires-Dist: stream2py
Requires-Dist: creek
Requires-Dist: ipython
Requires-Dist: matplotlib
Requires-Dist: PyAudio
Requires-Dist: soundfile


# taped
Python's serene audio accessor


To install:	```pip install taped```


# A quick (audio) peep

## In a nutshell:

```python
from taped import LiveWf
live_wf = LiveWf()  # make a live audio waveform object (using defaults for every thing)
live_wf.start()  # start "recording"
wf_chunk = live_wf[:100]  # do stuff (here, grab the first 100 (numerical waveform) samples)
... # do other stuff (save audio, display, pipe into some ML pipeline...)
live_wf.stop()  # stop the live_wf acquisition
```

But obviously, there's more to it. For a quick peep we'll mention just two:
- **context manager**: `LiveWf`, like most stream providing objects, is best used as a context manager (it's that `with...` thing), to automatically "clean up" when finished using.
- **input_device_index**: If you don't specify any arguments when making a `LiveWf`, you'll get defaults. You might want to check them out in case they're not what you want. One crucial argument is the `input_device_index`, which specifies what the audio source actually is. You can get a list of choices by using the `list_recording_device_index_names` function:


```python
from taped import list_recording_device_index_names

list_recording_device_index_names()
```



    [(0, 'MacBook Pro Microphone'), (2, 'ZoomAudioDevice')]




```python
from taped import LiveWf

&#35; you can specify as an integer, a string, or a (int, string) tuple. 
&#35;Bare in mind that both integer index (and sometimes names) may change from one session to another.
input_device_index = 'MacBook Pro Microphone' 

&#35;note how we use a context manager here!
with LiveWf(input_device_index) as live_wf:  # could have also used integer index 0, or both index and name!
    # now live_wf acts (sort of) like a numpy array of live audio waveform
    # skip the first 10000, samples, then and get 110000 samples after that, taking every other sample (i.e. downsampling)
    chk = live_wf[10_000:110_000:2]  

len(chk), type(chk)
```



    (22050, list)




```python
from taped import disp_wf
disp_wf(chk)  # assumes sample rate is 44100
```



![image](https://user-images.githubusercontent.com/1906276/102552725-81a00a80-4076-11eb-9aba-2ecb8b71e36f.png)




Note, you can use `find_a_default_input_device_index` to find an input device index for you automatically. 
That's what happens if you don't specify any inputs to `LiveWf()`. 
But don't trust that it will work every time. It's behavior may change, but right now, it just looks for the first name with word `"microphone"` in the list of names, and if that's not found, the first containing `"mic"`.

```python
from taped import find_a_default_input_device_index
input_device_index = find_a_default_input_device_index()
```

## So Basically...

Gives you access to your microphone as an iterator of numerical samples.

```pydocstring
>>> from itertools import islice
>>> from taped import LiveWf
>>>
>>> with LiveWf() as live_wf:
...     first_sample = next(live_wf)  # get a sample
...     second_sample = next(live_wf)  # get the next sample
...     ten_samples = list(islice(live_wf, 7))  # get the next 7 samples, using itertools.islice
...     a_3_6_slice = live_wf[3:6] # skip 3 samples and get 3 more (so up to 6), using [.] instead of islice
...     downsampled = live_wf[0:10:2]  # take every other sample (i.e. down-sampling) using [.]
>>> first_sample
-323
>>> second_sample
-1022
>>> ten_samples
[-1343, -1547, -1687, -1651, -1623, -1511, -1449]
>>> a_3_6_slice
[-1323, -1322, -1274]
>>> downsampled
[-1263, -1272, -1220, -1192, -1168]
```

From there, the sky is the limit.

For instance...

## Record and display audio from a microphone

```python
from taped import LiveWf, disp_wf
from itertools import islice

def record_and_display_audio_from_microphone(n_samples=10000, sample_rate=22050):
    with LiveWf(sr=sample_rate) as live_audio_stream:
        wf = list(islice(live_audio_stream, n_samples))
    return disp_wf(wf, sample_rate)

record_and_display_audio_from_microphone()
```

![image](https://user-images.githubusercontent.com/1906276/101562916-289cec00-397d-11eb-8a40-d3a7345e40da.png)


## Record and save audio from microphone

```python
from taped import LiveWf, disp_wf
import soundfile as sf  # pip install soundfile (or get your waveform_to_file function elsewhere)

def record_and_save_audio_from_microphone(filepath='tmp.wav', n_samples=10000, sample_rate=22050):
    with LiveWf(0, sr=sample_rate) as live_audio_stream:
        sf.write(filepath, 
                 data=list(islice(live_audio_stream, n_samples)), 
                 samplerate=sample_rate)

record_and_save_audio_from_microphone('myexample.wav')

# now read that file and display the sound
wf, sr = sf.read('myexample.wav')
disp_wf(wf, sr)
```

![image](https://user-images.githubusercontent.com/1906276/101563806-d1981680-397e-11eb-9f1e-fc35b9b1cc4a.png)


# A few more details

`taped` uses a layered approach. 

The `LiveWf` class you know (and already love) is actually the forth of the following stack of layers:

- `BufferItems`: Provides the items from an audio sensor (also called a mic!); namely the bytes, but also other useful information, such as timestamps (system and sensor).
- `ByteChunks`: Provides chunks of bytes from the mic. Essentially, extracts the bytes that the `BufferItems` items give you.
- `WfChunks`: Provides numerical waveform chunks; by default in the format of `numpy.array` `int16` integers. 
- `LiveWf`: Gives you access to a fixed size buffer of the recent history of audio, in waveform format. Essentially, the `WfChunks` chained together in one continuous (but live/dynamic) array.

Defining a waveform "displayer": if you want to display audio as a spectrogram, and actually play it (in a jupyter notebook), `pip install hum`. 


```python
from contextlib import suppress

try:
    from hum import disp_wf
except ModuleNotFoundError:
    import matplotlib.pylab as plt
    disp_wf = plt.plot
```

Let's get an `input_device_index` to use throughout our demo.


```python
from taped import find_a_default_input_device_index

input_device_index = find_a_default_input_device_index()
```

## BufferItems

`BufferItems` gives you a stream of 5-tuples containing sensor bytes, along with other information (timestamp etc.)


```python
from taped.base import BufferItems

with BufferItems(input_device_index) as buffer_items:
    item = next(buffer_items)

for i, x in enumerate(item):
    if isinstance(x, bytes):
        print(f"Element {i}: {len(x)} bytes: {x[:4]}...")
    else:
        print(f"Element {i}: {x}")
```

    Element 0: 1608243559585274
    Element 1: 8192 bytes: b' \x00[\x00'...
    Element 2: 4096
    Element 3: {'input_buffer_adc_time': 92149.79537730424, 'current_time': 92149.974279211, 'output_buffer_dac_time': 0.0}
    Element 4: 0



```python
from time import sleep
from collections import namedtuple
from pprint import pprint

BufferItemOutput = namedtuple(typename='BufferItemOutput', 
                              field_names=['timestamp', 'bytes', 'frame_count', 'time_info', 'status_flags'])

with BufferItems(input_device_index) as buffer_items:
    it = iter(buffer_items)
    item = BufferItemOutput(*next(it))
    sleep(2)
    item2 = BufferItemOutput(*next(it))

data_names = ['timestamp', 'bytes', 'frame_count', 'time_info', 'status_flags']

def display_buffer_item(item):
    d = dict(zip(item._fields, item))
    d['bytes'] = f"{len(d['bytes'])} bytes: {d['bytes'][:4]}..."
    pprint(d)

print('\nitem') 
display_buffer_item(item)
print('\nitem2') 
display_buffer_item(item2)
```


    item
    {'bytes': "8192 bytes: b'\\x05\\x00\\x0c\\x00'...",
     'frame_count': 4096,
     'status_flags': 0,
     'time_info': {'current_time': 84806.95996904,
                   'input_buffer_adc_time': 84806.78105158823,
                   'output_buffer_dac_time': 0.0},
     'timestamp': 1608236216529112}

    item2
    {'bytes': "8192 bytes: b'`\\xffR\\xff'...",
     'frame_count': 4096,
     'status_flags': 0,
     'time_info': {'current_time': 84807.04517719701,
                   'input_buffer_adc_time': 84806.87393594165,
                   'output_buffer_dac_time': 0.0},
     'timestamp': 1608236216621991}



```python
assert item.bytes != item2.bytes
```


```python
print("differences...")
dict(
    timestamp=item2.timestamp - item.timestamp, 
    input_buffer_adc_time = item2.time_info['input_buffer_adc_time'] - item.time_info['input_buffer_adc_time'], 
    current_time = item2.time_info['current_time'] - item.time_info['current_time'],
)

```

    differences...





    {'timestamp': 92879,
     'input_buffer_adc_time': 0.09288435342023149,
     'current_time': 0.0852081570046721}



## ByteChunks

If you just want the bytes of the sensor, use this.


```python
from taped.base import ByteChunks


with ByteChunks(input_device_index) as byte_chks:
    byte_chk = next(byte_chks)


assert isinstance(byte_chk, bytes)  # now we're just getting bytes
len(byte_chk)
```




    8192



## WfChunks

If you want to consume your waveform chunks numpy arrays instead of bytes, use this.


```python
from taped.base import WfChunks


with WfChunks(input_device_index) as wf_chks:
    chk = next(wf_chks)


assert isinstance(chk, np.ndarray)  # it's a numpy array
assert isinstance(chk[0], np.int16) # ... of int16 integers
len(chk)
```




    4096



## LiveWf

And finally, if you'd like to imagine you had a single waveform, as an array (populated continuously with live data from your sensor), use this.


```python
from taped.base import LiveWf
from itertools import islice

with LiveWf(input_device_index) as live_wf:
    sample = next(live_wf)  # get one sample
    chk = list(islice(live_wf, 0, 20000))  # get 20K samples

assert isinstance(sample, np.int16)  # a sample is an int16
assert len(chk) == 20000  # chk is an array of 20K samples
```


```python
disp_wf(chk)
```




![image](https://user-images.githubusercontent.com/1906276/102552830-aeecb880-4076-11eb-8b3e-f02d2b4d42b5.png)




You can also access the `list(islice(..., start, stop, step))` samples through the `[...]` brackets interface.


```python
with LiveWf(input_device_index) as live_wf:
    chk = live_wf[10000:54100]  # skip the first 10000, and get 44100 samples after that

disp_wf(chk)
```



![image](https://user-images.githubusercontent.com/1906276/102552877-c461e280-4076-11eb-86fb-4e5ba338edcb.png)



```python
with LiveWf(input_device_index) as live_wf:
    chk = live_wf[0:44100:2]  # get samples 0 through 44100, but only every other sample (so, downsampling)

disp_wf(chk)  # if you listen to it with the sample sample rate, it will sound accelerated!
```


![image](https://user-images.githubusercontent.com/1906276/102552941-e6f3fb80-4076-11eb-81ab-d079a1f74a4b.png)




