【问题标题】:Passing audio data from Python to C with ctypes使用 ctypes 将音频数据从 Python 传递到 C
【发布时间】:2019-07-02 03:56:26
【问题描述】:

我有一个对音频数据进行分析的 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 中做这样的事情?如果没有,是否有技术原因导致它不可能,还有其他我应该使用的东西吗?

【问题讨论】:

  • _native_function 指定 argtypesrestypestackoverflow.com/questions/52268294/…stackoverflow.com/questions/53182796/….
  • @CristiFati 我在代码加载_native_function 中进行了编辑。我的问题不是如何声明这个函数,而是如何将 Python 数据转换为适合它的参数。
  • 您对段错误的评论很有趣。您在 Windows 中看到了吗?你在 macOS 或 Linux 下试过吗?我觉得 question I asked 可能与您看到的内容相关联。
  • @MatthewWalker 不幸的是,我只在 Linux 上进行了测试,所以我不知道在 Windows 上会发生什么。实际上,我认为我也从未在 Linux 上修复过那个狡猾的副本,因为我最终只是将其转换为。说实话,整个 ctypes API 有点乱。而且恐怕我不知道您如何调试您的问题,而不是使用 -g 编译您自己的 CPython 并在其上运行 GDB。

标签: python ctypes


【解决方案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 对象。

【讨论】:

  • 从查看文档看来,这似乎将数据深度复制到了一个数组中。我同意它应该可以工作,但我正在寻找一种将这些现有数据传递到 C 函数而不复制所有数据的方法(或解释为什么从根本上不可能这样做)。
  • @BaryonsforBreakfast。有一种方法。请参阅上面的更新。
  • @BaryonsforBreakfast 也添加了另一个选项。
  • 我刚刚有机会测试第三种方法,它似乎确实有效(显然应该如此)。它不像我想要的那样灵活和安全,但只要 ctypes 没有const,它可能是最好的。谢谢!
  • 对于第三个例子,c_char_p 不是assume the string is NULL terminated 吗?因此,对于可能包含 NULL 的音频数据之类的,这不会潜在地造成问题吗?此外,音频数据不以 NULL 结尾是合理的——这将取决于音频本身。你还觉得c_char_p 是这里的一个选项吗?
【解决方案2】:

我查看了 CPython 错误跟踪器,看看之前是否出现过这种情况,它似乎是 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 进行了将第一个arg 转换为c_void_p 的FFI 调用,因此您可以直接将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 '可以解释为指针'。这对我来说似乎很可疑,但是这些现代的指针隐藏语言有一种方式会扰乱我的直觉。

【讨论】:

  • 但是,至少在 Python 3.8 下,似乎无法强制转换 memoryview。如果我们继续上面的示例,mv = memoryview(data); data_ptr = ctypes.cast(mv, ctypes.POINTER(ctypes.c_int16)) 会生成错误ctypes.ArgumentError: argument 1: <class 'TypeError'>: wrong type。解决此问题的任何想法?
  • @MatthewWalker 我以前从未使用过memoryview,但查看文档似乎听起来像一个简单的char* 指针和长度,他们指向的数据实际上不必是连续的,因此它实际上更像是一对 C++ 迭代器。我猜你不能转换它,因为(a)数据可能不连续[尽管他们可以检查并引发异常],并且(b)没有人费心为它编写转换。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-04-14
  • 1970-01-01
  • 1970-01-01
  • 2014-05-02
  • 1970-01-01
相关资源
最近更新 更多