Skip to content

Commit

Permalink
cython contributed configuration
Browse files Browse the repository at this point in the history
  • Loading branch information
stephen-huan authored and eshrh committed Aug 10, 2022
1 parent 0acc226 commit ca57285
Show file tree
Hide file tree
Showing 7 changed files with 580 additions and 0 deletions.
36 changes: 36 additions & 0 deletions contrib/cython/README.md
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.

55 changes: 55 additions & 0 deletions contrib/cython/src/audio-record
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,
)

24 changes: 24 additions & 0 deletions contrib/cython/src/cbackend.pxd
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

124 changes: 124 additions & 0 deletions contrib/cython/src/cbackend.pyx
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

Loading

0 comments on commit ca57285

Please sign in to comment.