Kivy低延迟音频(同样在Android上)
我想写一个以Kivy为前端的音乐DAW/合成器/鼓机 有没有办法用Kivy制作低延迟音频Kivy低延迟音频(同样在Android上),android,audio,kivy,buildozer,python-for-android,Android,Audio,Kivy,Buildozer,Python For Android,我想写一个以Kivy为前端的音乐DAW/合成器/鼓机 有没有办法用Kivy制作低延迟音频 (理想情况下,它也可以在Android上编译和运行)我将在这里介绍我的整个解决方案,以防任何人需要在Android或Windows/Linux/Mac上使用Kivy进行低延迟/实时音频播放或输入/录制: 在你走我选择的道路之前,请注意:我现在正经历着巨大的按钮点击延迟,尤其是在Windows上。这可能是我的项目的一个障碍,也可能是你的。在开始使用Android的Cython集成C++库进行战斗之前测试输入延
(理想情况下,它也可以在Android上编译和运行)我将在这里介绍我的整个解决方案,以防任何人需要在Android或Windows/Linux/Mac上使用Kivy进行低延迟/实时音频播放或输入/录制: 在你走我选择的道路之前,请注意:我现在正经历着巨大的按钮点击延迟,尤其是在Windows上。这可能是我的项目的一个障碍,也可能是你的。在开始使用Android的Cython集成C++库进行战斗之前测试输入延迟! 如果您想了解为什么
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
的miniaudio.h
需要位于同一目录中
# 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++',
)])
)
由于Android上的pymidi
崩溃是因为import serial
不起作用,而且我还不知道如何为Android编写python
配方和添加补丁,所以我只添加了一个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
(必须调用它才能调用pythonforandroid
):
要在Windows上构建它,只需使用setup.py
安装目录
要在Android上构建它,您需要一台带有pip install buildozer
的Linux机器(我在Windows Linux子系统2-wsl2
中使用了Ubuntu,并确保我在Linux目录中对源代码进行了git签出,因为涉及到大量编译,并且WSL的Windows目录IO非常慢)
现在,您可以将bin/yourapp.apk
复制到Windows目录并从CMD运行adb install yourapp.apk
,也可以按照我的说明在此处运行buildozer android debug deploy run
:
# 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++',
)])
)
"""
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
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)
# 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/
$ buildozer android debug