使用ctypes将音频数据从Python传递到C

时间:2019-07-02 03:56:26

标签: python ctypes

我有一个C ++库,该库对音频数据进行分析,并提供一个C API。 C API函数之一采用const int16_t*指向数据的指针并返回分析结果。

我正在尝试为此API建立一个Python接口,并且大多数接口都在工作,但是我在获取ctypes指针用作该函数的参数时遇到了麻烦。由于C端的指针指向const,所以我觉得应该可以对任何连续的数据进行良好的处理。但是,以下方法不起作用:

import ctypes
import wave

_native_lib = ctypes.cdll.LoadLibrary('libsound.so')
_native_function = _native_lib.process_sound_data
_native_function.argtypes = [ctypes.POINTER(ctypes.c_int16),
                             ctypes.c_size_t]
_native_function.restype = ctypes.c_int

wav_path = 'hello.wav'

with wave.open(wav_path, mode='rb') as wav_file:
    wav_bytes = wav_file.readframes(wav_file.getnframes())

data_start = ctypes.POINTER(ctypes.c_int16).from_buffer(wav_bytes) # ERROR: data is immutable
_native_function(data_start, len(wav_bytes)//2)

手动将wav_bytes复制到bytearray可以构造指针,但会导致本机代码段错误,表明接收到的地址是错误的(它通过单元测试,并从C ++读取数据)。通过正确的地址解决此问题从技术上可以解决问题,但我觉得有更好的方法。

当然可以只获取某些数据的地址,并保证它是正确的格式并且不会被更改吗?我不想不必将我所有以Python方式存储的音频数据深深复制为ctypes格式,因为如果我能找到指向它们的指针,大概字节就在某个地方!

理想情况下,我希望能够做这样的事情

data_start = cast_to(address_of(data[0]), c_int16_pointer)
_native_function(data_start, len(data))

然后可以与具有[0]len的任何内容一起使用。有没有办法在ctypes中做这样的事情?如果不是,是否有技术原因导致它不可能实现?还有其他我应该使用的东西吗?

2 个答案:

答案 0 :(得分:1)

这应该为您工作。将array用于可写缓冲区,并创建引用该缓冲区的ctypes数组。

data = array.array('h',wav_bytes)
addr,size = data.buffer_info()
arr = (c_short * size).from_address(addr)
_native_function(arr,size)

或者,要跳过将wav_bytes的副本复制到data数组中,可以使用argtypes中的指针类型。 ctypes知道如何将字节字符串转换为c_char_p。指针只是一个地址,因此_native_function将收到该地址,但在内部将其用作int*

_native_function.argtypes = c_char_p,c_size_t
_native_function(wav_bytes,len(wav_bytes) // 2)

解决“底层缓冲区不可写”错误的另一种方法是利用c_char_p,它允许使用不可变的字节字符串,然后将其显式转换为所需的指针类型:

_native_function.argtypes = POINTER(c_short),c_size_t
p = cast(c_char_p(wav_bytes),POINTER(c_short))
_native_function(p,len(wav_bytes) // 2)

在后一种情况下,您必须确保不实际写入缓冲区,因为它会破坏保存数据的不变的Python对象。

答案 1 :(得分:0)

我环顾了CPython Bug跟踪器,看是否以前曾出现过这种错误,似乎是raised as an issue in 2011。我同意发布者的观点,认为这是一个严重的错误设计,但当时的开发人员似乎并没有这么做。

Eryk Sun对该线程的评论表明,实际上有可能直接使用ctypes.cast。这是评论的一部分:

  

cast呼叫ctypes._cast(obj, obj, typ)_cast是ctypes函数指针,定义如下:

   _cast = PYFUNCTYPE(py_object, 
                      c_void_p, py_object, py_object)(_cast_addr)
     

由于cast进行了FFI调用,将第一个arg转换为c_void_p,因此您可以直接将bytes强制转换为指针类型:

   >>> from ctypes import *
   >>> data = b'123\x00abc'

   >>> ptr = cast(data, c_void_p)

对于我来说尚不清楚,这是标准实际上是必需的,还是仅仅是CPython实现的细节,但是以下内容在CPython中对我有用:

import ctypes
data = b'imagine this string is 16-bit sound data'
data_ptr = ctypes.cast(data, ctypes.POINTER(ctypes.c_int16))

documentation on cast说:

  

ctypes.cast(obj, type)

     

此函数类似于C中的强制转换运算符。它返回类型新的实例,该实例指向与obj相同的内存块。 type必须是指针类型,而obj必须是可以解释为指针的对象。

因此,看来CPython认为bytes'可以解释为指针'。在我看来,这似乎有些可疑,但是这些现代的隐藏指针的语言有一种弄乱我直觉的方式。