如何解决Kivy低延迟音频也在Android上
我想写一个以Kivy为前端的音乐DAW /合成器/鼓机。
有没有办法用Kivy做低延迟音频?
(理想情况下,它还将在Android上编译并运行)
解决方法
如果在任何人都需要再次在Android或Windows / Linux / Mac上使用Kivy进行低延迟/实时音频播放或输入/记录的情况下,我将在这里包括整个解决方案:
在选择我所选择的路径之前,请注意:我现在正经历明显的按钮单击延迟,尤其是在Windows上。这可能是我的项目的热门,也可能是您的。在开始使用与Cython集成的C ++库进行Android编译之前,请测试您的输入延迟!
如果想了解为什么setup.py
和python-for-android
食谱中存在某些有趣的行,请搜索过去几天的StackOverflow历史记录。
我最终直接与miniaudio
一起使用Cython
:
# engine.py
import cython
from midi import Message
cimport miniaudio
# this is my synth in another Cython file,you'll need to supply your own
from synthunit cimport RingBuffer,SynthUnit,int16_t,uint8_t
cdef class Miniaudio:
cdef miniaudio.ma_device_config config
cdef miniaudio.ma_device device
def __init__(self,Synth synth):
cdef void* p_data
p_data = <void*>synth.get_synth_unit_address()
self.config = miniaudio.ma_device_config_init(miniaudio.ma_device_type.playback);
self.config.playback.format = miniaudio.ma_format.s16
self.config.playback.channels = 1
self.config.sampleRate = 0
self.config.dataCallback = cython.address(callback)
self.config.pUserData = p_data
if miniaudio.ma_device_init(NULL,cython.address(self.config),cython.address(self.device)) != miniaudio.ma_result.MA_SUCCESS:
raise RuntimeError("Error initializing miniaudio")
SynthUnit.Init(self.device.sampleRate)
def __enter__(self):
miniaudio.ma_device_start(cython.address(self.device))
def __exit__(self,type,value,tb):
miniaudio.ma_device_uninit(cython.address(self.device))
cdef void callback(miniaudio.ma_device* p_device,void* p_output,const void* p_input,miniaudio.ma_uint32 frame_count) nogil:
# this function must be realtime (never ever block),hence the `nogil`
cdef SynthUnit* p_synth_unit
p_synth_unit = <SynthUnit*>p_device[0].pUserData
output = <int16_t*>p_output
p_synth_unit[0].GetSamples(frame_count,output)
# debug("row",0)
# debug("frame_count",frame_count)
# debug("freq mHz",int(1000 * p_synth_unit[0].freq))
cdef class Synth:
# wraps synth in an object that can be used from Python code,but can provice raw pointer
cdef RingBuffer ring_buffer
cdef SynthUnit* p_synth_unit
def __cinit__(self):
self.ring_buffer = RingBuffer()
self.p_synth_unit = new SynthUnit(cython.address(self.ring_buffer))
def __dealloc__(self):
del self.p_synth_unit
cdef SynthUnit* get_synth_unit_address(self):
return self.p_synth_unit
cpdef send_midi(self,midi):
raw = b''.join(Message(midi,channel=1).bytes_content)
self.ring_buffer.Write(raw,len(raw))
# can't do debug prints from a realtime function,but can write to a buffer:
cdef int d_index = 0
ctypedef long long addr
cdef addr[1024] d_data
cdef (char*)[1024] d_label
cdef void debug(char* label,addr x) nogil:
global d_index
if d_index < sizeof(d_data) * sizeof(d_data[0]):
d_label[d_index] = label
d_data[d_index] = x
d_index += 1
def get_debug_data():
result = []
row = None
for i in range(d_index):
if d_label[i] == b"row":
result.append(row)
row = []
else:
row.append((d_label[i],d_data[i]))
result.append(row)
return result
# miniaudio.pxd
cdef extern from "miniaudio_define.h":
pass # needed to do a #define that miniaudio.h expects,just put it in another C header
cdef extern from "miniaudio.h":
ctypedef unsigned int ma_uint32
cdef enum ma_result:
MA_SUCCESS = 0
cdef enum ma_device_type:
playback "ma_device_type_playback" = 1
capture "ma_device_type_capture" = 2
duplex "ma_device_type_duplex" = playback | capture
loopback "ma_device_type_loopback" = 4
cdef enum ma_format:
unknown "ma_format_unknown" = 0
u8 "ma_format_u8" = 1
s16 "ma_format_s16" = 2
s24 "ma_format_s24" = 3
s32 "ma_format_s32" = 4
f32 "ma_format_f32" = 5
ctypedef struct ma_device_id:
pass
ctypedef struct ma_device_config_playback:
const ma_device_id* pDeviceID
ma_format format
ma_uint32 channels
ctypedef void (* ma_device_callback_proc)(ma_device* pDevice,void* pOutput,const void* pInput,ma_uint32 frameCount)
ctypedef struct ma_device_config:
ma_uint32 sampleRate
ma_uint32 periodSizeInMilliseconds
ma_device_config_playback playback
ma_device_callback_proc dataCallback
void* pUserData
ctypedef struct ma_device:
ma_uint32 sampleRate
void* pUserData
ma_context* pContext
ctypedef struct ma_context:
pass
ma_device_config ma_device_config_init(ma_device_type deviceType)
ma_result ma_device_init(ma_context* pContext,const ma_device_config* pConfig,ma_device* pDevice)
ma_result ma_device_start(ma_device* pDevice)
void ma_device_uninit(ma_device* pDevice)
// minidaudio_define.h
#define MA_NO_DECODING
#define MA_NO_ENCODING
#define MINIAUDIO_IMPLEMENTATION
和miniaudio.h
中的miniaudio
必须位于同一目录中。
# setup.py
from setuptools import setup,Extension
from Cython.Build import cythonize
setup(
name = 'engine',version = '0.1',ext_modules = cythonize([Extension("engine",["engine.pyx"] + ['synth/' + p for p in [
'synth_unit.cc','util.cc'
]],include_path = ['synth/'],language = 'c++',)])
)
由于pymidi
在Android上由于import serial
不起作用而崩溃,并且由于我还不知道编写python-for-android
食谱和添加补丁,因此我只添加了{{1 }}对我的根目录没有任何作用:
serial.py
最后是"""
Override pySerial because it doesn't work on Android.
TODO: Use https://source.android.com/devices/audio/midi to implement MIDI support for Android
"""
Serial = lambda *args,**kwargs: None
(main.py
必须调用它):
python-for-android
要在Windows上进行构建,只需将# main.py
class MyApp(App):
# a Kivy app
...
if __name__ == '__main__':
synth = engine.Synth()
with engine.Miniaudio(synth):
MyApp(synth).run()
print('Goodbye') # for some strange reason without this print the program sometimes hangs on close
#data = engine.get_debug_data()
#for x in data: print(x)
放在pip install
目录下。
要在Android上进行构建,您需要一台具有setup.py
的Linux机器(我在Windows Linux Subsystem 2-{{1}}中使用了Ubuntu,并确保对源进行git checkout在linux目录中,因为其中涉及大量编译,并且WSL中Windows目录的IO非常慢)。
pip install buildozer
wsl2
# python-for-android/recipes/engine/__init__.py
from pythonforandroid.recipe import IncludedFilesBehaviour,CppCompiledComponentsPythonRecipe
import os
import sys
class SynthRecipe(IncludedFilesBehaviour,CppCompiledComponentsPythonRecipe):
version = 'stable'
src_filename = "../../../engine"
name = 'engine'
depends = ['setuptools']
call_hostpython_via_targetpython = False
install_in_hostpython = True
def get_recipe_env(self,arch):
env = super().get_recipe_env(arch)
env['LDFLAGS'] += ' -lc++_shared'
return env
recipe = SynthRecipe()
$ buildozer init
现在,您可以将# in buildozer.spec change:
requirements = python3,kivy,cython,py-midi,phase-engine
# ...
p4a.local_recipes = ./python-for-android/recipes/
复制到Windows目录并从CMD运行$ buildozer android debug
,也可以按照我的指示进行操作,以便bin/yourapp.apk
可以正常工作:Running React Native in WSL with the emulator running directly in Windows
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。