-
Notifications
You must be signed in to change notification settings - Fork 12
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
0acc226
commit ca57285
Showing
7 changed files
with
580 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
# Recording Audio with PortAudio and Cython | ||
|
||
This is a drop-in replacement for [audio-record](../python/src/audio-record), | ||
replacing the Python library `sounddevice` which wraps | ||
PortAudio with a direct Cython interface to PortAudio. | ||
The rationale is that it might improve performance and stability. | ||
|
||
### Requirements | ||
|
||
The requirements are [Cython](https://cython.org/), | ||
[cysignals](https://github.com/sagemath/cysignals), | ||
[PortAudio](http://portaudio.com/) (for recording | ||
audio), and [pydub](https://github.com/jiaaro/pydub/) (for saving recordings). | ||
```console | ||
pip install cython cysignals pydub | ||
``` | ||
(Cython is packaged in Community as `cython`, | ||
cysignals is packaged in Community as `python-cysignals`, | ||
PortAudio is packaged in Community as `portaudio`, | ||
and pydub is packaged in the AUR as `python-pydub`). | ||
|
||
Once the dependencies have been installed, build the extension modules with | ||
```console | ||
python setup.py build_ext --inplace | ||
``` | ||
|
||
### Caveats | ||
|
||
The program only responds to `SIGINT` (2) | ||
and not `SIGTERM` (15) to stop recording. | ||
|
||
The program does not actually begin recording faster than its | ||
Python counterpart since Python interpreter overhead is minimal; | ||
the main startup costs are really in initializing PortAudio. | ||
That is, there'll still be a ~0.25 second delay before the recording starts. | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
#!/usr/bin/env python3 | ||
import argparse | ||
import crecord | ||
|
||
if __name__ == "__main__": | ||
parser = argparse.ArgumentParser(description="record audio") | ||
parser.add_argument("path", help="file path") | ||
parser.add_argument("-d", "--device", help="device name") | ||
parser.add_argument("-c", "--channels", type=int, default=2, | ||
help="number of audio channels") | ||
parser.add_argument("-s", "--samplerate", type=float, default=48000, | ||
help="audio sample rate (pass 0 for default rate)") | ||
parser.add_argument("-l", "--latency", default="high", | ||
help="audio latency") | ||
parser.add_argument("-b", "--bitrate", default="64k", | ||
help="audio bitrate") | ||
|
||
args = parser.parse_args() | ||
|
||
# device can either be string identifier or index | ||
try: | ||
device = int(args.device) | ||
except (ValueError, TypeError): | ||
device = args.device | ||
# use default sounddevice selection | ||
if device is not None and \ | ||
(len(device) == 0 or device.lower() == "none"): | ||
device = None | ||
|
||
# begin recording and let Cython handle interrupts | ||
try: | ||
crecord.record( | ||
args.samplerate, | ||
device, | ||
args.channels, | ||
args.latency, | ||
) | ||
except KeyboardInterrupt: | ||
samplerate, data = crecord.get_recording() | ||
|
||
# load data into pydub for exporting | ||
import pydub | ||
recording = pydub.AudioSegment( | ||
data=data, | ||
sample_width=4, | ||
frame_rate=samplerate, | ||
channels=args.channels, | ||
) | ||
# write to file (uses ffmpeg) | ||
recording.export( | ||
args.path, | ||
format=args.path.split(".")[-1], | ||
bitrate=args.bitrate, | ||
) | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
cdef void copy_array(const int *input_array, Node *node) nogil | ||
cdef (char *) to_bytes(LinkedList *arrays, size_t *output_size) nogil | ||
|
||
# linked list | ||
|
||
cdef struct Node: | ||
size_t size | ||
int *data | ||
Node *next_node | ||
|
||
cdef struct LinkedList: | ||
size_t size, node_size | ||
Py_ssize_t i, buffer | ||
Node *head | ||
Node *addr | ||
Node *tail | ||
bint cleanup | ||
|
||
cdef (LinkedList *) linkedlist_init(size_t node_size, Py_ssize_t buffer) nogil | ||
cdef void linkedlist_dealloc(LinkedList *self) nogil | ||
|
||
cdef void linkedlist_fill(LinkedList *self) nogil | ||
cdef (Node *) linkedlist_get(LinkedList *self, size_t size) nogil | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,124 @@ | ||
# cython: profile=False | ||
from libc.stdlib cimport malloc, free | ||
|
||
cdef (Node *) __new_node(size_t size) nogil: | ||
""" Dynamically allocate an Node struct with the given size. """ | ||
cdef Node *node = <Node *> malloc(sizeof(Node)) | ||
node.size = size | ||
node.data = <int *> malloc(size*sizeof(int)) | ||
node.next_node = NULL | ||
return node | ||
|
||
cdef void copy_array(const int *input_array, Node *node) nogil: | ||
""" Copy the input array's data into the node. """ | ||
cdef: | ||
size_t i | ||
|
||
for i in range(node.size): | ||
node.data[i] = input_array[i] | ||
|
||
cdef (char *) to_bytes(LinkedList *arrays, size_t *output_size) nogil: | ||
""" Serialize the list of 32-bit integer arrays as bytes. """ | ||
cdef: | ||
Py_ssize_t size, length, i, j, k | ||
int x | ||
Node *node | ||
char *output | ||
|
||
# get the total size | ||
size = 0 | ||
length = arrays.i | ||
node = arrays.head | ||
for i in range(length): | ||
size += node.size | ||
node = node.next_node | ||
# allocate contiguous data | ||
output_size[0] = size*sizeof(int) | ||
output = <char *> malloc(output_size[0]) | ||
k = 0 | ||
node = arrays.head | ||
for i in range(length): | ||
for j in range(<Py_ssize_t> node.size): | ||
x = node.data[j] | ||
output[k + 0] = (x >> 0) & 0xFF | ||
output[k + 1] = (x >> 8) & 0xFF | ||
output[k + 2] = (x >> 16) & 0xFF | ||
output[k + 3] = (x >> 24) & 0xFF | ||
k += 4 | ||
node = node.next_node | ||
|
||
return output | ||
|
||
# Singly linked list data structure with a bit of a lookahead buffer | ||
|
||
cdef (LinkedList *) linkedlist_init(size_t node_size, | ||
Py_ssize_t buffer) nogil: | ||
""" Dynamically allocate an LinkedList struct with the given size. """ | ||
cdef LinkedList *self = <LinkedList *> malloc(sizeof(LinkedList)) | ||
self.node_size = node_size | ||
self.buffer = buffer | ||
self.size = 0 | ||
self.i = -1 | ||
self.head = NULL | ||
self.addr = NULL | ||
self.tail = NULL | ||
|
||
linkedlist_fill(self) | ||
return self | ||
|
||
cdef void linkedlist_dealloc(LinkedList *self) nogil: | ||
""" Teardown to free associated explicitly allocated memory. """ | ||
cdef: | ||
Node *node | ||
Node *next_node | ||
|
||
node = self.head | ||
while node != NULL: | ||
next_node = node.next_node | ||
free(node.data) | ||
free(node) | ||
node = next_node | ||
free(self) | ||
|
||
cdef void linkedlist__add(LinkedList *self, size_t size) nogil: | ||
""" Add a node of the given size to the end of the linked list. """ | ||
cdef: | ||
Node *node | ||
|
||
node = __new_node(size) | ||
if self.size == 0: | ||
self.head = node | ||
self.addr = node | ||
else: | ||
self.tail.next_node = node | ||
self.tail = node | ||
self.size += 1 | ||
|
||
cdef void linkedlist_fill(LinkedList *self) nogil: | ||
""" Fill the buffer with uninitialized nodes. """ | ||
cdef: | ||
Py_ssize_t i | ||
|
||
for i in range(self.i + self.buffer - (<Py_ssize_t> self.size) + 1): | ||
linkedlist__add(self, self.node_size) | ||
|
||
cdef (Node *) linkedlist_get(LinkedList *self, size_t size) nogil: | ||
""" Get the node corresponding to the last currently empty position. """ | ||
cdef: | ||
Node *node | ||
|
||
node = self.addr | ||
# reached end of linked list, fallback to dynamically allocated memory | ||
if node == NULL: | ||
linkedlist__add(self, size) | ||
node = self.tail | ||
# not enough space, overwrite with dynamically allocated memory | ||
if node.size < size: | ||
node.data = <int *> malloc(size*sizeof(int)) | ||
|
||
# update the size with the actual size | ||
node.size = size | ||
self.addr = node.next_node | ||
self.i += 1 | ||
return node | ||
|
Oops, something went wrong.