0

我想用 Kivy 作为前端写一个音乐 DAW / 合成器 / 鼓机。

有没有办法用 Kivy 做低延迟音频?

(理想情况下它也可以在 Android 上编译和运行)

4

1 回答 1

0

我将在此处包含我的整个解决方案,以防有人需要再次在 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.hfromminiaudio需要在同一个目录中。

# 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配方和添加补丁,所以我只是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调用它才能调用它):

# 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)

要在 Windows 上构建它,只需pip install使用setup.py.

要在 Android 上构建它,您需要一台 Linux 机器pip install buildozer(我在 Windows Linux 子系统 2 中使用 Ubuntu - wsl2,并确保我在 linux 目录中对源代码进行了 git checkout,因为涉及到很多编译和来自 WSL 的 Windows 目录的 IO 非常慢)。

# 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

现在,您可以复制bin/yourapp.apk到 Windows 目录并adb install yourapp.apk从 CMD 运行,或者按照我的说明进行操作buildozer android debug deploy run在 WSL 中运行 React Native,模拟器直接在 Windows 中运行

于 2020-11-11T09:48:43.067 回答