Package: taped
tapedPython's serene audio accessorTo install:pip install tapedA quick (audio) peepIn a nutshell:fromtapedimportLiveWflive_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 acquisitionBut 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 thatwith...thing), to automatically "clean up" when finished using.input_device_index: If you don't specify any arguments when making aLiveWf, you'll get defaults. You might want to check them out in case they're not what you want. One crucial argument is theinput_device_index, which specifies what the audio source actually is. You can get a list of choices by using thelist_recording_device_index_namesfunction:fromtapedimportlist_recording_device_index_nameslist_recording_device_index_names()[(0, 'MacBook Pro Microphone'), (2, 'ZoomAudioDevice')]fromtapedimportLiveWf&#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!withLiveWf(input_device_index)aslive_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)fromtapedimportdisp_wfdisp_wf(chk,sr=live_wf.sr)# will sound faster than normal because (remember) we down-sampled.Note, you can usefind_a_default_input_device_indexto find an input device index for you automatically.
That's what happens if you don't specify any inputs toLiveWf().
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".fromtapedimportfind_a_default_input_device_indexinput_device_index=find_a_default_input_device_index()So Basically...Gives you access to your microphone as an iterator of numerical samples.>>> 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 microphonefromtapedimportLiveWf,disp_wffromitertoolsimportislicedefrecord_and_display_audio_from_microphone(n_samples=10000,sample_rate=22050):withLiveWf(sr=sample_rate)aslive_audio_stream:wf=list(islice(live_audio_stream,n_samples))returndisp_wf(wf,sample_rate)record_and_display_audio_from_microphone()Record and save audio from microphonefromtapedimportLiveWf,disp_wfimportsoundfileassf# pip install soundfile (or get your waveform_to_file function elsewhere)defrecord_and_save_audio_from_microphone(filepath='tmp.wav',n_samples=10000,sample_rate=22050):withLiveWf(0,sr=sample_rate)aslive_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 soundwf,sr=sf.read('myexample.wav')disp_wf(wf,sr)A few more detailstapeduses a layered approach.TheLiveWfclass 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 theBufferItemsitems give you.WfChunks: Provides numerical waveform chunks; by default in the format ofnumpy.arrayint16integers.LiveWf: Gives you access to a fixed size buffer of the recent history of audio, in waveform format. Essentially, theWfChunkschained 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.fromcontextlibimportsuppresstry:fromhumimportdisp_wfexceptModuleNotFoundError:importmatplotlib.pylabaspltdisp_wf=plt.plotLet's get aninput_device_indexto use throughout our demo.fromtapedimportfind_a_default_input_device_indexinput_device_index=find_a_default_input_device_index()BufferItemsBufferItemsgives you a stream of 5-tuples containing sensor bytes, along with other information (timestamp etc.)
thatstream2py, which wrapsPyAudio(itself a wrapper ofPortAudio) gives us.If you're okay with the high level interfaces thattapedoffers, you may want to skip thisBufferItemssection.
But if you want (or need) to peep under (the first level of) the hood, here's whatBufferItemsis about.fromtaped.baseimportBufferItemswithBufferItems(input_device_index)asbuffer_items:item=next(buffer_items)print(f"item is a{type(item).__name__}(a namedtuple) with{len(item)}elements")fori,xinenumerate(item):ifisinstance(x,bytes):print(f"{i}:{item._fields[i]}:{len(x)}bytes:{x[:4]}...")else:print(f"{i}:{item._fields[i]}:{x}")item is a BufferItemOutput (a namedtuple) with 5 elements
0: timestamp: 1608336556178995
1: bytes: 8192 bytes: b'\t\x00\x18\x00'...
2: frame_count: 4096
3: time_info: {'input_buffer_adc_time': 135079.42883468725, 'current_time': 135079.60533177, 'output_buffer_dac_time': 0.0}
4: status_flags: 0fromtimeimportsleepfromcollectionsimportnamedtuplefrompprintimportpprintwithBufferItems(input_device_index)asbuffer_items:it=iter(buffer_items)# note the iter(buffer_items) instead of just buffer_items!item=next(it)sleep(2)item2=next(it)data_names=['timestamp','bytes','frame_count','time_info','status_flags']defdisplay_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}See that the three kind of timestamps that we get are different,
but all around4096 / 44100 = 0.09287..., the chunk size, in seconds.assertbuffer_items.chk_size==4096assertbuffer_items.sr==44100print("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}See that we have different bytes!assertitem.bytes!=item2.bytesBut add we usedit = buffer_itemsdirectly instead ofit = iter(buffer_items), we would have gotten the same bytes.Indeed, usingiter(...)ensures that we "move forward" in our iteration, where as doing anext(buffer_items)would just give us the first chunk of the queue (that is, the oldest one).
Until the buffer is full, that oldest chunk is always the same first one.
Once the buffer is full, thenext(buffer_items)will give us something different (at the rate of incoming chunks).We can see this by making the buffer size (stream_buffer_size_s, whose default is 60 seconds) smaller.
Study the following code to see what's happening.importtimedeftake_a_nap(nap_time):print(f"After a{nap_time}seconds nap...")time.sleep(nap_time)d=dict()withBufferItems(stream_buffer_size_s=2)ass:d[0]=next(s)take_a_nap(1)d[1]=next(s)print(f"... chunk timestamp is{(d[1].timestamp-d[0].timestamp)/1e6}later\n")take_a_nap(3)d[2]=next(s)print(f"... chunk timestamp is{(d[2].timestamp-d[1].timestamp)/1e6}later\n")take_a_nap(1)d[3]=next(s)print(f"... chunk timestamp is{(d[3].timestamp-d[2].timestamp)/1e6}later\n")Found MacBook Pro Microphone. Will use it as the default input device. It's index is 1
After a 1 seconds nap...
... chunk timestamp is 0.0 later

After a 3 seconds nap...
... chunk timestamp is 2.229115 later

After a 1 seconds nap...
... chunk timestamp is 1.021678 laterByteChunksIf you just want the bytes of the sensor, use this.fromtaped.baseimportByteChunkswithByteChunks(input_device_index)asbyte_chks:byte_chk=next(byte_chks)assertisinstance(byte_chk,bytes)# now we're just getting byteslen(byte_chk)8192WfChunksIf you want to consume your waveform chunks numpy arrays instead of bytes, use this.fromtaped.baseimportWfChunkswithWfChunks(input_device_index)aswf_chks:chk=next(wf_chks)assertisinstance(chk,np.ndarray)# it's a numpy arrayassertisinstance(chk[0],np.int16)# ... of int16 integerslen(chk)4096LiveWfAnd 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.fromtaped.baseimportLiveWffromitertoolsimportislicewithLiveWf(input_device_index)aslive_wf:sample=next(live_wf)# get one samplechk=list(islice(live_wf,0,20000))# get 20K samplesassertisinstance(sample,np.int16)# a sample is an int16assertlen(chk)==20000# chk is an array of 20K samplesdisp_wf(chk)You can also access thelist(islice(..., start, stop, step))samples through the[...]brackets interface.withLiveWf(input_device_index)aslive_wf:chk=live_wf[10000:54100]# skip the first 10000, and get 44100 samples after thatdisp_wf(chk)withLiveWf(input_device_index)aslive_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!