├── test ├── __init__.py ├── meta_tests.py ├── wrapper_tests.py ├── helpers.py ├── dsp_tests.py └── ui_tests.py ├── test_synth.dsp ├── .gitignore ├── test_examples.py ├── dattorro_notch_cut_regalia.dsp ├── LICENSE ├── FAUSTPy ├── __init__.py ├── python_meta.py ├── __main__.py ├── python_dsp.py ├── wrapper.py └── python_ui.py ├── setup.py └── README.md /test/__init__.py: -------------------------------------------------------------------------------- 1 | # nothing needs to be done here 2 | -------------------------------------------------------------------------------- /test_synth.dsp: -------------------------------------------------------------------------------- 1 | declare name "Simple test synth; produces garbage"; 2 | declare version "0.0"; 3 | declare author "Marc Joliet"; 4 | declare license "MIT"; 5 | declare copyright "(c)Marc Joliet 2013"; 6 | 7 | // produces the stream 1,0,1,0,... 8 | process = _~+(1):%(2); 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # object and backup files 2 | *.[oa] 3 | *.so 4 | *.py[co] 5 | *~ 6 | 7 | # generated by cffi 8 | __pycache__/ 9 | yacctab.py 10 | lextab.py 11 | 12 | # C/C++ code 13 | *.cpp 14 | *.c 15 | *.h 16 | 17 | # distutils directories: 18 | *.egg-info/ 19 | build/ 20 | dist/ 21 | 22 | # vim swap files 23 | *.swp 24 | -------------------------------------------------------------------------------- /test_examples.py: -------------------------------------------------------------------------------- 1 | import os 2 | import glob 3 | import argparse 4 | import FAUSTPy 5 | 6 | parser = argparse.ArgumentParser() 7 | parser.add_argument('-p', '--path', 8 | dest="examples_path", 9 | default="/usr/share/faust-*/examples", 10 | help="The path to the FAUST examples.") 11 | args = parser.parse_args() 12 | 13 | fs = 48e3 14 | 15 | for f in glob.glob(os.sep.join([args.examples_path, "*.dsp"])): 16 | print(f) 17 | dsp = FAUSTPy.FAUST(f, int(fs), "double") 18 | -------------------------------------------------------------------------------- /dattorro_notch_cut_regalia.dsp: -------------------------------------------------------------------------------- 1 | declare name "Dattoro notch filter and resonator (Regalia)"; 2 | declare version "0.1"; 3 | declare author "Marc Joliet"; 4 | declare license "MIT"; 5 | declare copyright "(c)Marc Joliet 2013"; 6 | 7 | import("filter.lib"); 8 | import("math.lib"); 9 | 10 | // user inputs 11 | fc = hslider("Center Freq. [unit:Hz]", 5e3, 30, 20e3, 1); 12 | k = hslider("Gain", 0.0, 0.0, 5.0, 1e-3); 13 | q = hslider("Q", 1, 1, 10, 1e-3); 14 | 15 | wc = 2*PI*fc/SR; 16 | 17 | // the allpass is constructed from last to first coefficient; use the normalised 18 | // ladder form for increased robustness 19 | //ap(beta, gamma) = allpassnn(2, (gamma, beta)); 20 | ap(beta, gamma) = allpassn(2, (gamma, beta)); 21 | 22 | notch_resonator_regalia(k, fc) = H with { 23 | beta = (1-tan(wc/(2*q)))/(1+tan(wc/(2*q))); 24 | gamma = neg(cos(wc)); 25 | A = ap(beta, gamma); 26 | H = _ : *(0.5) <: *(1+k),(A:*(1-k)) :> _; 27 | }; 28 | 29 | process = par(i,2,notch_resonator_regalia(k,fc)); 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | "FAUSTPy" Copyright (c) 2013 Marc Joliet . 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /FAUSTPy/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | A set of classes used to dynamically wrap FAUST DSP programs in Python. 5 | 6 | This package defines three types: 7 | - PythonUI is an implementation of the UIGlue C struct. 8 | - PythonMeta is an implementation of the MetaGlue C struct. 9 | - PythonDSP wraps the DSP struct. 10 | - FAUST integrates the other two, sets up the CFFI environment (defines the 11 | data types and API) and compiles the FAUST program. This is the class you 12 | most likely want to use. 13 | """ 14 | 15 | from . wrapper import FAUST 16 | from . python_ui import PythonUI, Param 17 | from . python_meta import PythonMeta 18 | from . python_dsp import PythonDSP 19 | 20 | # TODO: see which meta-data is still relevant. pydoc definitely uses "author", 21 | # "credits" and "version" (and "date"), should the rest be removed? 22 | __author__ = "Marc Joliet" 23 | __copyright__ = "Copyright 2013, Marc Joliet" 24 | __credits__ = "Marc Joliet" 25 | __license__ = "MIT" 26 | __version__ = "0.1" 27 | __maintainer__ = "Marc Joliet" 28 | __email__ = "marcec@gmx.de" 29 | __status__ = "Prototype" 30 | 31 | __all__ = ["FAUST", "PythonUI", "PythonMeta", "PythonDSP", "Param", "wrapper"] 32 | -------------------------------------------------------------------------------- /test/meta_tests.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | import cffi 4 | from . helpers import init_ffi, empty 5 | from FAUSTPy import PythonMeta 6 | 7 | ################################# 8 | # test PythonMeta 9 | ################################# 10 | 11 | 12 | def tearDownModule(): 13 | cffi.verifier.cleanup_tmpdir( 14 | tmpdir=os.sep.join([os.path.dirname(__file__), "__pycache__"]) 15 | ) 16 | 17 | 18 | class test_faustmeta(unittest.TestCase): 19 | 20 | def setUp(self): 21 | 22 | self.obj = empty() 23 | self.ffi, self.C = init_ffi() 24 | 25 | # grab the C object from the PythonMeta instance 26 | self.meta = PythonMeta(self.ffi, self.obj) 27 | 28 | def test_attributes(self): 29 | "Verify presence of various attributes." 30 | 31 | self.assertTrue(hasattr(self.meta, "meta")) 32 | self.assertTrue(hasattr(self.obj, "metadata")) 33 | 34 | def test_declare(self): 35 | "Test the declare() C callback." 36 | 37 | c_meta = self.meta.meta 38 | 39 | c_meta.declare(c_meta.mInterface, b"foo", b"bar") 40 | self.assertDictEqual(self.obj.metadata, {b"foo": b"bar"}) 41 | 42 | c_meta.declare(c_meta.mInterface, b"baz", b"biz") 43 | self.assertDictEqual(self.obj.metadata, {b"foo": b"bar", 44 | b"baz": b"biz"}) 45 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import setup 3 | 4 | 5 | def read(fname): 6 | return open(os.path.join(os.path.dirname(__file__), fname)).read() 7 | 8 | 9 | setup( 10 | name='FAUSTPy', 11 | version='0.1', 12 | url='https://github.com/marcecj/faust_python', 13 | download_url='https://github.com/marcecj/faust_python', 14 | license='MIT', 15 | author='Marc Joliet', 16 | author_email='marcec@gmx.de', 17 | description='FAUSTPy is a Python wrapper for the FAUST DSP language.', 18 | packages=['FAUSTPy'], 19 | test_suite="test", 20 | long_description=read('README.md'), 21 | platforms='any', 22 | 23 | # the ctypes field was added before NumPy 1.0, so any version is OK 24 | # TODO: do I need requires, too (it makes the --requires option work)? 25 | requires=["cffi", "numpy"], 26 | install_requires=["cffi", "numpy"], 27 | provides=["FAUSTPy"], 28 | 29 | classifiers=[ 30 | 'Development Status :: 3 - Alpha', 31 | 'License :: OSI Approved :: MIT License', 32 | 'Operating System :: POSIX :: Linux', 33 | 'Programming Language :: Python', 34 | 'Programming Language :: Python :: 2.7', 35 | 'Programming Language :: Python :: 3', 36 | 'Programming Language :: Python :: 3.2', 37 | 'Topic :: Multimedia :: Sound/Audio' 38 | 'Topic :: Software Development :: Libraries :: Python Modules', 39 | ], 40 | ) 41 | -------------------------------------------------------------------------------- /FAUSTPy/python_meta.py: -------------------------------------------------------------------------------- 1 | class PythonMeta(object): 2 | """ 3 | Stores DSP meta-data in a metadata attribute of another object, 4 | specifically a FAUST wrapper object. 5 | 6 | In FAUST, a DSP may specify meta-data of itself and libraries it uses, 7 | which it does through the declare() method of a Meta object. The 8 | PythonMeta class implements such a Meta object. It creates a C callback to 9 | its declare() method and stores it in a Meta struct, which can be passed to 10 | the metadatamydsp() function of a FAUST DSP object. 11 | """ 12 | 13 | def __init__(self, ffi, obj=None): 14 | """ 15 | Initialise a PythonMeta object. 16 | 17 | Parameters: 18 | ----------- 19 | 20 | ffi : cffi.FFI 21 | The CFFI instance that holds all the data type declarations. 22 | obj : object (optional) 23 | The Python object in which the meta-data is to be stored. If None 24 | (the default) the PythonMeta instance manipulates itself. 25 | """ 26 | 27 | if obj: 28 | self.__obj = obj 29 | else: 30 | self.__obj = self 31 | 32 | self.__obj.metadata = {} 33 | 34 | def declare(mInterface, key, value): 35 | self.declare(ffi.string(key), ffi.string(value)) 36 | 37 | self.__declare_c = ffi.callback("void(void*, char*, char*)", declare) 38 | 39 | meta = ffi.new("MetaGlue*") 40 | meta.declare = self.__declare_c 41 | meta.mInterface = ffi.NULL # we don't use this anyway 42 | 43 | self.__meta = meta 44 | 45 | meta = property( 46 | fget=lambda x: x.__meta, 47 | doc="The Meta struct that calls back to its parent object." 48 | ) 49 | 50 | def declare(self, key, value): 51 | 52 | self.__obj.metadata[key] = value 53 | -------------------------------------------------------------------------------- /test/wrapper_tests.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | import cffi 4 | import numpy as np 5 | from FAUSTPy import FAUST 6 | 7 | ################################# 8 | # test FAUST 9 | ################################# 10 | 11 | 12 | def tearDownClass(): 13 | cffi.verifier.cleanup_tmpdir( 14 | tmpdir=os.sep.join([os.path.dirname(__file__), "__pycache__"]) 15 | ) 16 | 17 | 18 | class test_faustwrapper_init(unittest.TestCase): 19 | 20 | def test_init(self): 21 | """Test initialisation of FAUST objects.""" 22 | 23 | FAUST("dattorro_notch_cut_regalia.dsp", 48000) 24 | FAUST("dattorro_notch_cut_regalia.dsp", 48000, "float") 25 | FAUST("dattorro_notch_cut_regalia.dsp", 48000, "double") 26 | FAUST("dattorro_notch_cut_regalia.dsp", 48000, "long double") 27 | 28 | def test_init_inline_code(self): 29 | """Test initialisation of FAUST objects with inline FAUST code.""" 30 | 31 | dsp = FAUST(b"process=*(0.5);", 48000) 32 | dsp = FAUST(b"process=*(0.5);", 48000, "float") 33 | dsp = FAUST(b"process=*(0.5);", 48000, "double") 34 | dsp = FAUST(b"process=*(0.5);", 48000, "long double") 35 | 36 | def test_init_wrong_args(self): 37 | """Test initialisation of FAUST objects with bad arguments.""" 38 | 39 | self.assertRaises(ValueError, FAUST, "dattorro_notch_cut_regalia.dsp", 40 | 48000, "l double") 41 | 42 | 43 | class test_faustwrapper(unittest.TestCase): 44 | 45 | def setUp(self): 46 | 47 | self.dsp1 = FAUST("dattorro_notch_cut_regalia.dsp", 48000) 48 | 49 | dsp_code = b""" 50 | declare name "Inline Test"; 51 | declare author "Some Guy"; 52 | 53 | process = *(0.5); 54 | """ 55 | self.dsp2 = FAUST(dsp_code, 48000) 56 | 57 | def test_attributes(self): 58 | "Verify presence of various attributes." 59 | 60 | self.assertTrue(hasattr(self.dsp1, "dsp")) 61 | self.assertTrue(hasattr(self.dsp1.dsp, "ui")) 62 | self.assertTrue(hasattr(self.dsp1.dsp, "metadata")) 63 | 64 | self.assertTrue(hasattr(self.dsp2, "dsp")) 65 | self.assertTrue(hasattr(self.dsp2.dsp, "ui")) 66 | self.assertTrue(hasattr(self.dsp2.dsp, "metadata")) 67 | 68 | self.assertDictEqual(self.dsp2.dsp.metadata, 69 | {b"name": b"Inline Test", 70 | b"author": b"Some Guy"}) 71 | 72 | def test_compute(self): 73 | """Test the compute() method.""" 74 | 75 | audio = np.zeros((self.dsp2.dsp.num_in, 48e3), 76 | dtype=self.dsp2.dsp.dtype) 77 | audio[0, 0] = 1 78 | 79 | out = self.dsp2.compute(audio) 80 | 81 | self.assertEqual(out[0, 0], audio[0, 0]*0.5) 82 | -------------------------------------------------------------------------------- /test/helpers.py: -------------------------------------------------------------------------------- 1 | import cffi 2 | from tempfile import NamedTemporaryFile 3 | from string import Template 4 | from subprocess import check_call 5 | 6 | 7 | class empty(object): 8 | pass 9 | 10 | 11 | def init_ffi(faust_dsp="dattorro_notch_cut_regalia.dsp", 12 | faust_float="float"): 13 | 14 | ffi = cffi.FFI() 15 | 16 | cdefs = "typedef {0} FAUSTFLOAT;".format(faust_float) + """ 17 | 18 | typedef struct { 19 | void *mInterface; 20 | void (*declare)(void* interface, const char* key, const char* value); 21 | } MetaGlue; 22 | 23 | typedef struct { 24 | // widget layouts 25 | void (*openVerticalBox)(void*, const char* label); 26 | void (*openHorizontalBox)(void*, const char* label); 27 | void (*openTabBox)(void*, const char* label); 28 | void (*declare)(void*, FAUSTFLOAT*, char*, char*); 29 | // passive widgets 30 | void (*addNumDisplay)(void*, const char* label, FAUSTFLOAT* zone, int p); 31 | void (*addTextDisplay)(void*, const char* label, FAUSTFLOAT* zone, const char* names[], FAUSTFLOAT min, FAUSTFLOAT max); 32 | void (*addHorizontalBargraph)(void*, const char* label, FAUSTFLOAT* zone, FAUSTFLOAT min, FAUSTFLOAT max); 33 | void (*addVerticalBargraph)(void*, const char* label, FAUSTFLOAT* zone, FAUSTFLOAT min, FAUSTFLOAT max); 34 | // active widgets 35 | void (*addHorizontalSlider)(void*, const char* label, FAUSTFLOAT* zone, FAUSTFLOAT init, FAUSTFLOAT min, FAUSTFLOAT max, FAUSTFLOAT step); 36 | void (*addVerticalSlider)(void*, const char* label, FAUSTFLOAT* zone, FAUSTFLOAT init, FAUSTFLOAT min, FAUSTFLOAT max, FAUSTFLOAT step); 37 | void (*addButton)(void*, const char* label, FAUSTFLOAT* zone); 38 | void (*addToggleButton)(void*, const char* label, FAUSTFLOAT* zone); 39 | void (*addCheckButton)(void*, const char* label, FAUSTFLOAT* zone); 40 | void (*addNumEntry)(void*, const char* label, FAUSTFLOAT* zone, FAUSTFLOAT init, FAUSTFLOAT min, FAUSTFLOAT max, FAUSTFLOAT step); 41 | void (*closeBox)(void*); 42 | void* uiInterface; 43 | } UIGlue; 44 | 45 | typedef struct {...;} mydsp; 46 | 47 | mydsp *newmydsp(); 48 | void deletemydsp(mydsp*); 49 | void metadatamydsp(MetaGlue* m); 50 | int getSampleRatemydsp(mydsp* dsp); 51 | int getNumInputsmydsp(mydsp* dsp); 52 | int getNumOutputsmydsp(mydsp* dsp); 53 | int getInputRatemydsp(mydsp* dsp, int channel); 54 | int getOutputRatemydsp(mydsp* dsp, int channel); 55 | void classInitmydsp(int samplingFreq); 56 | void instanceInitmydsp(mydsp* dsp, int samplingFreq); 57 | void initmydsp(mydsp* dsp, int samplingFreq); 58 | void buildUserInterfacemydsp(mydsp* dsp, UIGlue* interface); 59 | void computemydsp(mydsp* dsp, int count, FAUSTFLOAT** inputs, FAUSTFLOAT** outputs); 60 | """ 61 | ffi.cdef(cdefs) 62 | 63 | with NamedTemporaryFile(suffix=".c") as f: 64 | 65 | faust_args = ["-lang", "c", "-o", f.name, faust_dsp] 66 | 67 | if faust_float == "float": 68 | faust_args = ["-single"] + faust_args 69 | elif faust_float == "double": 70 | faust_args = ["-double"] + faust_args 71 | elif faust_float == "long double": 72 | faust_args = ["-quad"] + faust_args 73 | 74 | check_call(["faust"] + faust_args) 75 | 76 | # compile the code 77 | C = ffi.verify( 78 | Template(""" 79 | #define FAUSTFLOAT ${FAUSTFLOAT} 80 | 81 | // helper function definitions 82 | FAUSTFLOAT min(FAUSTFLOAT x, FAUSTFLOAT y) { return x < y ? x : y;}; 83 | FAUSTFLOAT max(FAUSTFLOAT x, FAUSTFLOAT y) { return x > y ? x : y;}; 84 | 85 | // the MetaGlue struct that will be wrapped 86 | typedef struct { 87 | void *mInterface; 88 | void (*declare)(void* interface, const char* key, const char* value); 89 | } MetaGlue; 90 | 91 | // the UIGlue struct that will be wrapped 92 | typedef struct { 93 | // widget layouts 94 | void (*openVerticalBox)(void*, const char* label); 95 | void (*openHorizontalBox)(void*, const char* label); 96 | void (*openTabBox)(void*, const char* label); 97 | void (*declare)(void*, FAUSTFLOAT*, char*, char*); 98 | // passive widgets 99 | void (*addNumDisplay)(void*, const char* label, FAUSTFLOAT* zone, int p); 100 | void (*addTextDisplay)(void*, const char* label, FAUSTFLOAT* zone, const char* names[], FAUSTFLOAT min, FAUSTFLOAT max); 101 | void (*addHorizontalBargraph)(void*, const char* label, FAUSTFLOAT* zone, FAUSTFLOAT min, FAUSTFLOAT max); 102 | void (*addVerticalBargraph)(void*, const char* label, FAUSTFLOAT* zone, FAUSTFLOAT min, FAUSTFLOAT max); 103 | // active widgets 104 | void (*addHorizontalSlider)(void*, const char* label, FAUSTFLOAT* zone, FAUSTFLOAT init, FAUSTFLOAT min, FAUSTFLOAT max, FAUSTFLOAT step); 105 | void (*addVerticalSlider)(void*, const char* label, FAUSTFLOAT* zone, FAUSTFLOAT init, FAUSTFLOAT min, FAUSTFLOAT max, FAUSTFLOAT step); 106 | void (*addButton)(void*, const char* label, FAUSTFLOAT* zone); 107 | void (*addToggleButton)(void*, const char* label, FAUSTFLOAT* zone); 108 | void (*addCheckButton)(void*, const char* label, FAUSTFLOAT* zone); 109 | void (*addNumEntry)(void*, const char* label, FAUSTFLOAT* zone, FAUSTFLOAT init, FAUSTFLOAT min, FAUSTFLOAT max, FAUSTFLOAT step); 110 | void (*closeBox)(void*); 111 | void* uiInterface; 112 | } UIGlue; 113 | 114 | ${FAUSTC} 115 | """).substitute( 116 | FAUSTFLOAT=faust_float, 117 | FAUSTC=f.read().decode() 118 | ), 119 | extra_compile_args=["-std=c99", "-march=native", "-O3"], 120 | ) 121 | 122 | return ffi, C 123 | -------------------------------------------------------------------------------- /test/dsp_tests.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | import cffi 4 | import numpy as np 5 | from . helpers import init_ffi 6 | from FAUSTPy import PythonDSP 7 | 8 | ################################# 9 | # test PythonDSP 10 | ################################# 11 | 12 | 13 | def tearDownModule(): 14 | cffi.verifier.cleanup_tmpdir( 15 | tmpdir=os.sep.join([os.path.dirname(__file__), "__pycache__"]) 16 | ) 17 | 18 | 19 | class test_faustdsp_init(unittest.TestCase): 20 | 21 | def setUp(self): 22 | 23 | self.ffi, self.C = zip(*[init_ffi(faust_float=ff) for ff in 24 | ("float", "double", "long double")]) 25 | 26 | def test_init_different_fs(self): 27 | """ 28 | Test initialisation of PythonDSP objects with different values for the 29 | sampling rate. 30 | """ 31 | 32 | # just try various sampling rates 33 | PythonDSP(self.C[0], self.ffi[0], 32000) 34 | PythonDSP(self.C[0], self.ffi[0], 44100) 35 | PythonDSP(self.C[0], self.ffi[0], 48000) 36 | self.assertRaises(ValueError, PythonDSP, self.C[0], self.ffi[0], 0) 37 | self.assertRaises(ValueError, PythonDSP, self.C[0], self.ffi[0], -1) 38 | 39 | def test_init_different_faustfloats(self): 40 | """ 41 | Test initialisation of PythonDSP objects with different values of 42 | FAUSTFLOAT. 43 | """ 44 | 45 | # this should not do anything 46 | PythonDSP(self.C[0], self.ffi[0], 48000) 47 | PythonDSP(self.C[1], self.ffi[1], 48000) 48 | PythonDSP(self.C[2], self.ffi[2], 48000) 49 | 50 | def test_init_bad_ffi_combos(self): 51 | """ 52 | Test initialisation and .compute() of PythonDSP objects with 53 | incompatible compinations of FFILibrary and FFI objects. 54 | """ 55 | 56 | # pairs of incompatible FFILibrary and FFI objects 57 | ffis = [(self.C[0], self.ffi[1]), 58 | (self.C[0], self.ffi[2]), 59 | (self.C[1], self.ffi[0]), 60 | (self.C[1], self.ffi[2]), 61 | (self.C[2], self.ffi[0]), 62 | (self.C[2], self.ffi[1])] 63 | 64 | # the init itself won't fail, but the type checking in later 65 | # compute()'s will; this is due to the fact that you cannot tell 66 | # whether a FFILibrary object come from a given FFI object or not 67 | for C, ffi in ffis: 68 | dsp = PythonDSP(C, ffi, 48000) 69 | audio = np.zeros((dsp.num_in, 48e3), dtype=dsp.dtype) 70 | audio[:, 0] = 1 71 | self.assertRaises(TypeError, dsp.compute, audio) 72 | 73 | 74 | class test_faustdsp(unittest.TestCase): 75 | 76 | def setUp(self): 77 | 78 | self.ffi1, self.C1 = init_ffi() 79 | self.ffi2, self.C2 = init_ffi(faust_dsp="test_synth.dsp") 80 | 81 | self.dsp = PythonDSP(self.C1, self.ffi1, 48000) 82 | self.synth = PythonDSP(self.C2, self.ffi2, 48000) 83 | 84 | def tearDown(self): 85 | 86 | # TODO: for some reason, this prevents strange errors along the line of 87 | # 88 | # "Exception TypeError: "initializer for ctype 'struct $mydsp *' must 89 | # be a pointer to same type, not cdata 'struct $mydsp *'" in > ignored" 92 | # 93 | # Find out why! 94 | del self.dsp 95 | del self.synth 96 | 97 | def test_attributes(self): 98 | "Verify presence of various attributes." 99 | 100 | self.assertTrue(hasattr(self.dsp, "dsp")) 101 | self.assertTrue(hasattr(self.dsp, "fs")) 102 | self.assertTrue(hasattr(self.dsp, "num_in")) 103 | self.assertTrue(hasattr(self.dsp, "num_out")) 104 | self.assertTrue(hasattr(self.dsp, "faustfloat")) 105 | self.assertTrue(hasattr(self.dsp, "dtype")) 106 | 107 | def test_compute(self): 108 | "Test the compute() method." 109 | 110 | audio = np.zeros((self.dsp.num_in, 48e3), dtype=self.dsp.dtype) 111 | audio[:, 0] = 1 112 | out = self.dsp.compute(audio) 113 | 114 | def test_compute_empty_input(self): 115 | "Test the compute() method with zero input samples." 116 | 117 | audio = np.zeros((self.dsp.num_in, 0), dtype=self.dsp.dtype) 118 | out = self.dsp.compute(audio) 119 | self.assertEqual(out.size, 0) 120 | 121 | def test_compute_bad_dtype(self): 122 | "Test the compute() method with inputs of incorrect dtype." 123 | 124 | for dtype in ("float64", "float128"): 125 | audio = np.zeros((self.dsp.num_in, 48e3), dtype=dtype) 126 | audio[:, 0] = 1 127 | self.assertRaises(ValueError, self.dsp.compute, audio) 128 | 129 | def test_compute_synth(self): 130 | "Test the compute() for synthesizer effects." 131 | 132 | count = 128 133 | ref = np.zeros((self.synth.num_out, count), dtype=self.synth.dtype) 134 | ref[:, ::2] = 1 135 | out = self.synth.compute(count) 136 | self.assertTrue(np.all(ref == out)) 137 | 138 | def test_compute_synth_zero_count(self): 139 | "Test the compute() for synthesizer effects with zero output samples." 140 | 141 | out = self.synth.compute(0) 142 | self.assertEqual(out.size, 0) 143 | 144 | def test_compute_synth_neg_count(self): 145 | """ 146 | Test the compute() for synthesizer effects with negative output 147 | samples. 148 | """ 149 | 150 | self.assertRaises(ValueError, self.synth.compute, -1) 151 | -------------------------------------------------------------------------------- /FAUSTPy/__main__.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import numpy as np 3 | import matplotlib.pyplot as plt 4 | from FAUSTPy import * 5 | 6 | ####################################################### 7 | # set up command line arguments 8 | ####################################################### 9 | 10 | parser = argparse.ArgumentParser() 11 | parser.add_argument('-f', '--faustfloat', 12 | dest="faustfloat", 13 | default="float", 14 | help="The value of FAUSTFLOAT.") 15 | parser.add_argument('-p', '--path', 16 | dest="faust_path", 17 | default="", 18 | help="The path to the FAUST compiler.") 19 | parser.add_argument('-c', '--cflags', 20 | dest="cflags", 21 | default=[], 22 | type=str.split, 23 | help="Extra compiler flags") 24 | parser.add_argument('-s', '--fs', 25 | dest="fs", 26 | default=48000, 27 | type=int, 28 | help="The sampling frequency") 29 | args = parser.parse_args() 30 | 31 | ####################################################### 32 | # initialise the FAUST object and get the default parameters 33 | ####################################################### 34 | 35 | wrapper.FAUST_PATH = args.faust_path 36 | 37 | dattorro = FAUST("dattorro_notch_cut_regalia.dsp", args.fs, args.faustfloat, 38 | extra_compile_args=args.cflags) 39 | 40 | def_Q = dattorro.dsp.ui.p_Q 41 | def_Gain = dattorro.dsp.ui.p_Gain 42 | def_Freq = dattorro.dsp.ui.p_Center_Freq 43 | 44 | ####################################################### 45 | # plot the frequency response with the default settings 46 | ####################################################### 47 | 48 | audio = np.zeros((dattorro.dsp.num_in, args.fs), dtype=dattorro.dsp.dtype) 49 | audio[:, 0] = 1 50 | 51 | out = dattorro.compute(audio) 52 | 53 | print(audio) 54 | print(out) 55 | 56 | spec = np.fft.fft(out)[:, :args.fs/2] 57 | 58 | fig = plt.figure() 59 | p = fig.add_subplot( 60 | 1, 1, 1, 61 | title="Frequency response with the default settings\n" 62 | "(Q={}, F={:.2f} Hz, G={:.0f} dB FS)".format( 63 | def_Q.zone, def_Freq.zone, 20*np.log10(def_Gain.zone+1e-8) 64 | ), 65 | xlabel="Frequency in Hz (log)", 66 | ylabel="Magnitude in dB FS", 67 | xscale="log" 68 | ) 69 | p.plot(20*np.log10(np.absolute(spec.T)+1e-8)) 70 | p.legend(("Left channel", "Right channel"), loc="best") 71 | 72 | ####################################################### 73 | # plot the frequency response with varying Q 74 | ####################################################### 75 | 76 | Q = np.linspace(def_Q.min, def_Q.max, 10) 77 | 78 | dattorro.dsp.ui.p_Center_Freq = 1e2 79 | dattorro.dsp.ui.p_Gain = 10**(-0.5) # -10 dB 80 | 81 | cur_G = dattorro.dsp.ui.p_Gain.zone 82 | cur_F = dattorro.dsp.ui.p_Center_Freq.zone 83 | 84 | fig = plt.figure() 85 | p = fig.add_subplot( 86 | 1, 1, 1, 87 | title="Frequency response " 88 | "(G={:.0f} dB FS, F={} Hz)".format(20*np.log10(cur_G+1e-8), cur_F), 89 | xlabel="Frequency in Hz (log)", 90 | ylabel="Magnitude in dB FS", 91 | xscale="log" 92 | ) 93 | 94 | for q in Q: 95 | dattorro.dsp.ui.p_Q = q 96 | out = dattorro.compute(audio) 97 | spec = np.fft.fft(out)[0, :args.fs/2] 98 | 99 | p.plot(20*np.log10(np.absolute(spec.T)+1e-8), 100 | label="Q={}".format(q)) 101 | 102 | p.legend(loc="best") 103 | 104 | ####################################################### 105 | # plot the frequency response with varying gain 106 | ####################################################### 107 | 108 | # start at -60 dB because the minimum is at an extremely low -160 dB 109 | G = np.logspace(-3, np.log10(def_Gain.max), 10) 110 | 111 | dattorro.dsp.ui.p_Q = 2 112 | 113 | cur_Q = dattorro.dsp.ui.p_Q.zone 114 | cur_F = dattorro.dsp.ui.p_Center_Freq.zone 115 | 116 | fig = plt.figure() 117 | p = fig.add_subplot( 118 | 1, 1, 1, 119 | title="Frequency response (Q={}, F={} Hz)".format(cur_Q, cur_F), 120 | xlabel="Frequency in Hz (log)", 121 | ylabel="Magnitude in dB FS", 122 | xscale="log" 123 | ) 124 | 125 | for g in G: 126 | dattorro.dsp.ui.p_Gain = g 127 | out = dattorro.compute(audio) 128 | spec = np.fft.fft(out)[0, :args.fs/2] 129 | 130 | p.plot(20*np.log10(np.absolute(spec.T)+1e-8), 131 | label="G={:.3g} dB FS".format(20*np.log10(g+1e-8))) 132 | 133 | p.legend(loc="best") 134 | 135 | ########################################################### 136 | # plot the frequency response with varying center frequency 137 | ########################################################### 138 | 139 | F = np.logspace(np.log10(def_Freq.min), np.log10(def_Freq.max), 10) 140 | 141 | dattorro.dsp.ui.p_Q = def_Q.default 142 | dattorro.dsp.ui.p_Gain = 10**(-0.5) # -10 dB 143 | 144 | cur_Q = dattorro.dsp.ui.p_Q.zone 145 | cur_G = dattorro.dsp.ui.p_Gain.zone 146 | 147 | fig = plt.figure() 148 | p = fig.add_subplot( 149 | 1, 1, 1, 150 | title="Frequency response " 151 | "(Q={}, G={:.0f} dB FS)".format(cur_Q, 20*np.log10(cur_G+1e-8)), 152 | xlabel="Frequency in Hz (log)", 153 | ylabel="Magnitude in dB FS", 154 | xscale="log" 155 | ) 156 | 157 | for f in F: 158 | dattorro.dsp.ui.p_Center_Freq = f 159 | out = dattorro.compute(audio) 160 | spec = np.fft.fft(out)[0, :args.fs/2] 161 | 162 | p.plot(20*np.log10(np.absolute(spec.T)+1e-8), 163 | label="F={:.2f} Hz".format(f)) 164 | 165 | p.legend(loc="best") 166 | 167 | ################ 168 | # show the plots 169 | ################ 170 | 171 | plt.show() 172 | 173 | print("everything passes!") 174 | -------------------------------------------------------------------------------- /FAUSTPy/python_dsp.py: -------------------------------------------------------------------------------- 1 | from numpy import atleast_2d, ndarray, float32, float64, float128 2 | 3 | 4 | class PythonDSP(object): 5 | """A FAUST DSP wrapper. 6 | 7 | This class is more low-level than the FAUST class. It can be viewed as an 8 | abstraction that sits directly on top of the FAUST DSP struct. 9 | """ 10 | 11 | def __init__(self, C, ffi, fs): 12 | """Initialise a PythonDSP object. 13 | 14 | To instantiate this object, you create a cffi.FFI object that contains 15 | all required declarations (check the FAUSTPy.FAUST code for an 16 | example). Then you compile the code via ffi.verfiy(), which creates an 17 | FFILibrary object. Both of these are then passed to this constructor 18 | along with the other parameters specified below. 19 | 20 | Parameters: 21 | ----------- 22 | 23 | C : cffi.FFILibrary 24 | The FFILibrary that represents the compiled code. 25 | ffi : cffi.FFI 26 | The CFFI instance that holds all the data type declarations. 27 | fs : int 28 | The sampling rate the FAUST DSP should be initialised with. 29 | """ 30 | 31 | self.__C = C 32 | self.__ffi = ffi 33 | self.__faust_float = ffi.getctype("FAUSTFLOAT") 34 | self.__dsp = ffi.gc(C.newmydsp(), C.deletemydsp) 35 | self.metadata = {} 36 | 37 | if fs <= 0: 38 | raise ValueError("The sampling rate must have a positive value.") 39 | 40 | if self.__faust_float == "float": 41 | self.__dtype = float32 42 | elif self.__faust_float == "double": 43 | self.__dtype = float64 44 | elif self.__faust_float == "long double": 45 | self.__dtype = float128 46 | 47 | # calls both classInitmydsp() and instanceInitmydsp() 48 | C.initmydsp(self.__dsp, int(fs)) 49 | 50 | # allocate the input and output pointers so that they are not 51 | # allocated/deallocated at every call to compute() 52 | # TODO: can the number of inputs/outputs change at run time? 53 | self.__input_p = self.__ffi.new("FAUSTFLOAT*[]", self.num_in) 54 | self.__output_p = self.__ffi.new("FAUSTFLOAT*[]", self.num_out) 55 | 56 | dsp = property(fget=lambda x: x.__dsp, 57 | doc="The DSP struct that calls back to its parent object.") 58 | 59 | dtype = property(fget=lambda x: x.__dtype, 60 | doc="A dtype corresponding to the value of FAUSTFLOAT.") 61 | 62 | faustfloat = property(fget=lambda x: x.__faust_float, 63 | doc="The value of FAUSTFLOAT for this DSP.") 64 | 65 | fs = property(fget=lambda s: s.__C.getSampleRatemydsp(s.__dsp), 66 | doc="The sampling rate of the DSP.") 67 | 68 | num_in = property(fget=lambda s: s.__C.getNumInputsmydsp(s.__dsp), 69 | doc="The number of input channels.") 70 | 71 | num_out = property(fget=lambda s: s.__C.getNumOutputsmydsp(s.__dsp), 72 | doc="The number of output channels.") 73 | 74 | def compute(self, audio): 75 | """ 76 | Process an ndarray with the FAUST DSP. 77 | 78 | Parameters: 79 | ----------- 80 | 81 | The first argument depends on the type of DSP (synthesizer or effect): 82 | 83 | audio : numpy.ndarray 84 | If the DSP is an effect (i.e., it processes input data and produces 85 | output), the first argument is an audio signal to process. 86 | 87 | or 88 | 89 | count : int 90 | If the DSP is a synthesizer (i.e., it has zero inputs and produces 91 | output), the first argument is the number of output samples to 92 | produce 93 | 94 | Returns: 95 | -------- 96 | 97 | out : numpy.ndarray 98 | The output of the DSP. 99 | 100 | Notes: 101 | ------ 102 | 103 | This function uses the buffer protocol to avoid copying the input data. 104 | """ 105 | 106 | if self.num_in > 0: 107 | # returns a view, so very little overhead 108 | audio = atleast_2d(audio) 109 | 110 | # Verify that audio.dtype == self.dtype, because a) Python 111 | # SEGFAULTs when audio.dtype < self.dtype and b) the computation is 112 | # garbage when audio.dtype > self.dtype. 113 | if audio.dtype != self.__dtype: 114 | raise ValueError("audio.dtype must be {}".format(self.__dtype)) 115 | 116 | count = audio.shape[1] # number of samples 117 | num_in = self.num_in # number of input channels 118 | 119 | # set up the input pointers 120 | for i in range(num_in): 121 | self.__input_p[i] = self.__ffi.cast('FAUSTFLOAT *', 122 | audio[i].ctypes.data) 123 | else: 124 | # special case for synthesizers: the input argument is the number 125 | # of samples 126 | count = audio 127 | 128 | num_out = self.num_out # number of output channels 129 | 130 | # initialise the output array 131 | output = ndarray((num_out, count), dtype=self.__dtype) 132 | 133 | # set up the output pointers 134 | for i in range(num_out): 135 | self.__output_p[i] = self.__ffi.cast('FAUSTFLOAT *', 136 | output[i].ctypes.data) 137 | 138 | # call the DSP 139 | self.__C.computemydsp(self.__dsp, count, self.__input_p, 140 | self.__output_p) 141 | 142 | return output 143 | 144 | # TODO: Run some more serious tests to check whether compute2() is worth 145 | # keeping, because with the bundled DSP the run-time is about 83 us for 146 | # 2x64 samples versus about 90 us for compute(), so only about 7 us 147 | # difference. 148 | def compute2(self, audio): 149 | """ 150 | Process an ndarray with the FAUST DSP, like compute(), but without any 151 | safety checks. NOTE: compute2() can crash Python if "audio" is an 152 | incompatible NumPy array! 153 | 154 | This function is only useful if the DSP is an effect since the checks 155 | not made here do not apply to synthesizers. 156 | 157 | Parameters: 158 | ----------- 159 | 160 | audio : numpy.ndarray 161 | The audio signal to process. 162 | 163 | Returns: 164 | -------- 165 | 166 | out : numpy.ndarray 167 | The output of the DSP. 168 | 169 | Notes: 170 | ------ 171 | 172 | This function uses the buffer protocol to avoid copying the input data. 173 | """ 174 | 175 | count = audio.shape[1] # number of samples 176 | num_in = self.num_in # number of input channels 177 | num_out = self.num_out # number of output channels 178 | 179 | # initialise the output array 180 | output = ndarray((num_out, count), dtype=audio.dtype) 181 | 182 | # set up the output pointers 183 | for i in range(num_out): 184 | self.__output_p[i] = self.__ffi.cast('FAUSTFLOAT *', 185 | output[i].ctypes.data) 186 | 187 | # set up the input pointers 188 | for i in range(num_in): 189 | self.__input_p[i] = self.__ffi.cast('FAUSTFLOAT *', 190 | audio[i].ctypes.data) 191 | 192 | # call the DSP 193 | self.__C.computemydsp(self.__dsp, count, self.__input_p, 194 | self.__output_p) 195 | 196 | return output 197 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FAUSTPy 2 | Marc Joliet 3 | 4 | A FAUST wrapper for Python. 5 | 6 | ## Introduction 7 | 8 | FAUSTPy is a Python wrapper for the [FAUST](http://faust.grame.fr/) DSP 9 | language. It is implemented using the [CFFI](https://cffi.readthedocs.org/) and 10 | hence creates the wrapper dynamically at run-time. 11 | 12 | ## Installation 13 | 14 | FAUSTPy has the following requirements: 15 | 16 | - [FAUST](http://faust.grame.fr/), specifically the FAUST2 branch, because 17 | FAUSTPy requires the C backend. 18 | - [CFFI](https://cffi.readthedocs.org/), tested with version 0.6. 19 | - A C compiler; the default CFLAGS assume a GCC compatible one. 20 | - [NumPy](http://numpy.scipy.org/), tested with version 1.6. 21 | 22 | FAUSTPy works with Python 2.7 and 3.2+. 23 | 24 | You can install FAUSTPy via the provided setup.py script by running 25 | 26 | sudo python setup.py install 27 | 28 | or 29 | 30 | python setup.py install --user 31 | 32 | Although you may want to verify that everything works beforehand by running the 33 | test suite first: 34 | 35 | python setup.py test 36 | 37 | ## Useage 38 | 39 | Using FAUSTPy is fairly simple, the main class is FAUSTPy.FAUST, which takes 40 | care of the dirty work. A typical example: 41 | 42 | dsp = FAUSTPy.FAUST("faust_file.dsp", fs) 43 | 44 | This will create a wrapper that initialises the FAUST DSP with the sampling rate 45 | `fs` and with `FAUSTFLOAT` set to the default value of `float` (the default 46 | precision that is set by the FAUST compiler). Note that this 47 | 48 | 1. compiles the FAUST DSP to C, 49 | 2. compiles and links the C code, and 50 | 3. initialises the C objects, 51 | 52 | all of which happens in the background, thanks to the CFFI. Furthermore, this 53 | wrapper class 54 | 55 | 1. initialises the UI as a `ui` attribute of the DSP, and 56 | 2. stores the meta-data declared by the DSP as a `metadata` attribute. 57 | 58 | To better match the [NumPy](http://numpy.scipy.org/) default of `double`, you 59 | can overload the `faust_float` argument: 60 | 61 | dsp = FAUSTPy.FAUST("faust_file.dsp", fs, "double") 62 | 63 | To process an array, simply call: 64 | 65 | # dsp.dsp is a PythonDSP object wrapped by the FAUST object 66 | audio = numpy.zeros((dsp.dsp.num_in, count)) 67 | audio[:,0] = 1 68 | out = dsp.compute(audio) 69 | 70 | Here the array `audio` is initialised to the number of inputs of the DSP and 71 | `count` samples; each channel consists of a Kronecker delta, so `out` contains 72 | the impulse response of the DSP. In general `audio` is allowed to have more 73 | channels (rows) than the DSP, in which case the first `dsp.dsp.num_in` channels 74 | are processed, but not less. 75 | 76 | You can also pass in-line FAUST code as the first argument, which will be 77 | written to a temporary file and compiled by FAUST as usual. In Python 3: 78 | 79 | dsp = FAUSTPy.FAUST(b"process = _:*(0.5);", fs) 80 | 81 | Finally, below is a simple IPython example (using Python 2) that shows what a 82 | FAUST object might look like. It is based on the DSP 83 | `dattorro_notch_cut_regalia.dsp` included in this repository. 84 | 85 | In [1]: import FAUSTPy 86 | 87 | In [2]: import numpy as np 88 | 89 | In [3]: fs = 48000 90 | 91 | In [4]: dattorro = FAUSTPy.FAUST("dattorro_notch_cut_regalia.dsp", fs, "double") 92 | 93 | In [5]: dattorro. 94 | dattorro.compute dattorro.dsp dattorro.FAUST_PATH 95 | dattorro.compute2 dattorro.FAUST_FLAGS 96 | 97 | In [5]: dattorro.dsp. 98 | dattorro.dsp.compute dattorro.dsp.faustfloat dattorro.dsp.num_out 99 | dattorro.dsp.compute2 dattorro.dsp.fs dattorro.dsp.ui 100 | dattorro.dsp.dsp dattorro.dsp.metadata 101 | dattorro.dsp.dtype dattorro.dsp.num_in 102 | 103 | In [5]: dattorro.dsp.metadata 104 | Out[5]: 105 | {'author': 'Marc Joliet', 106 | 'copyright': '(c)Marc Joliet 2013', 107 | 'filter.lib/author': 'Julius O. Smith (jos at ccrma.stanford.edu)', 108 | 'filter.lib/copyright': 'Julius O. Smith III', 109 | 'filter.lib/license': 'STK-4.3', 110 | 'filter.lib/name': 'Faust Filter Library', 111 | 'filter.lib/reference': 'https://ccrma.stanford.edu/~jos/filters/', 112 | 'filter.lib/version': '1.29', 113 | 'license': 'MIT', 114 | 'math.lib/author': 'GRAME', 115 | 'math.lib/copyright': 'GRAME', 116 | 'math.lib/license': 'LGPL with exception', 117 | 'math.lib/name': 'Math Library', 118 | 'math.lib/version': '1.0', 119 | 'music.lib/author': 'GRAME', 120 | 'music.lib/copyright': 'GRAME', 121 | 'music.lib/license': 'LGPL with exception', 122 | 'music.lib/name': 'Music Library', 123 | 'music.lib/version': '1.0', 124 | 'name': 'Dattoro notch filter and resonator (Regalia)', 125 | 'version': '0.1'} 126 | 127 | In [6]: dattorro.dsp.fs 128 | Out[6]: 48000 129 | 130 | In [7]: dattorro.dsp.num_in 131 | Out[7]: 2 132 | 133 | In [8]: dattorro.dsp.num_out 134 | Out[8]: 2 135 | 136 | In [9]: dattorro.dsp.ui. 137 | dattorro.dsp.ui.label dattorro.dsp.ui.metadata dattorro.dsp.ui.p_Gain 138 | dattorro.dsp.ui.layout dattorro.dsp.ui.p_Center_Freq dattorro.dsp.ui.p_Q 139 | 140 | In [9]: dattorro.dsp.ui.label 141 | Out[9]: 'dattorro_notch_cut_regalia' 142 | 143 | In [10]: dattorro.dsp.ui.layout 144 | Out[10]: 'vertical' 145 | 146 | In [11]: dattorro.dsp.ui.p_Center_Freq 147 | Out[11]: 148 | 149 | In [12]: dattorro.dsp.ui.p_Center_Freq. 150 | dattorro.dsp.ui.p_Center_Freq.default dattorro.dsp.ui.p_Center_Freq.min 151 | dattorro.dsp.ui.p_Center_Freq.label dattorro.dsp.ui.p_Center_Freq.step 152 | dattorro.dsp.ui.p_Center_Freq.max dattorro.dsp.ui.p_Center_Freq.type 153 | dattorro.dsp.ui.p_Center_Freq.metadata dattorro.dsp.ui.p_Center_Freq.zone 154 | 155 | In [12]: dattorro.dsp.ui.p_Center_Freq.label 156 | Out[12]: 'Center Freq.' 157 | 158 | In [13]: dattorro.dsp.ui.p_Center_Freq.metadata 159 | Out[13]: {'unit': 'Hz'} 160 | 161 | In [14]: dattorro.dsp.ui.p_Center_Freq.type 162 | Out[14]: 'HorizontalSlider' 163 | 164 | In [15]: audio = np.zeros((dattorro.dsp.num_in,fs), dtype=dattorro.dsp.dtype) 165 | 166 | In [16]: audio[:,0] = 1 167 | 168 | In [17]: audio 169 | Out[17]: 170 | array([[ 1., 0., 0., ..., 0., 0., 0.], 171 | [ 1., 0., 0., ..., 0., 0., 0.]]) 172 | 173 | In [18]: dattorro.compute(audio) 174 | Out[18]: 175 | array([[ 0.74657288, -0.30020767, 0.0227801 , ..., 0. , 176 | 0. , 0. ], 177 | [ 0.74657288, -0.30020767, 0.0227801 , ..., 0. , 178 | 0. , 0. ]]) 179 | 180 | For more details, see the built-in documentation (aka `pydoc FAUSTPy`) and - if 181 | you are so inclined - the source code. 182 | 183 | ## Demo script 184 | 185 | The `__main__.py` of the FAUST package contains a small demo application which 186 | plots some magnitude frequency responses of the example FAUST DSP. You can 187 | execute it by executing 188 | 189 | PYTHONPATH=. python FAUSTPy 190 | 191 | in the source directory. This will display four plots: 192 | 193 | - the magnitude frequency response of the FAUST DSP at default settings, 194 | - the magnitude frequency response with varying Q, 195 | - the magnitude frequency response with varying gain, and 196 | - the magnitude frequency response with varying center frequency. 197 | 198 | ## TODO 199 | 200 | - finish the UIGlue wrapper 201 | - finish the test suite 202 | - finish the unit tests 203 | - add functional tests so that you can test how everything works together 204 | (perhaps use "UITester.dsp" and maybe one other DSP from the examples) 205 | -------------------------------------------------------------------------------- /test/ui_tests.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | import cffi 4 | from . helpers import init_ffi, empty 5 | from FAUSTPy import PythonUI 6 | 7 | ################################# 8 | # test PythonUI 9 | ################################# 10 | 11 | 12 | def tearDownModule(): 13 | cffi.verifier.cleanup_tmpdir( 14 | tmpdir=os.sep.join([os.path.dirname(__file__), "__pycache__"]) 15 | ) 16 | 17 | 18 | class test_faustui(unittest.TestCase): 19 | 20 | def setUp(self): 21 | 22 | self.obj = empty() 23 | self.ffi, self.C = init_ffi() 24 | 25 | # grab the C object from the PythonUI instance 26 | self.ui = PythonUI(self.ffi, self.obj) 27 | 28 | def test_attributes(self): 29 | "Verify presence of various attributes." 30 | 31 | self.assertTrue(hasattr(self.ui, "ui")) 32 | 33 | def test_declare_group(self): 34 | "Test declaration of group meta-data." 35 | 36 | c_ui = self.ui.ui 37 | 38 | c_ui.declare(c_ui.uiInterface, self.ffi.NULL, b"key1", b"val1") 39 | c_ui.openTabBox(c_ui.uiInterface, b"box1") 40 | c_ui.declare(c_ui.uiInterface, self.ffi.NULL, b"key1", b"val1") 41 | c_ui.declare(c_ui.uiInterface, self.ffi.NULL, b"key2", b"val2") 42 | c_ui.openTabBox(c_ui.uiInterface, b"box2") 43 | c_ui.closeBox(c_ui.uiInterface) 44 | c_ui.declare(c_ui.uiInterface, self.ffi.NULL, b"key2", b"val2") 45 | c_ui.declare(c_ui.uiInterface, self.ffi.NULL, b"key3", b"val3") 46 | c_ui.openTabBox(c_ui.uiInterface, b"box3") 47 | c_ui.closeBox(c_ui.uiInterface) 48 | c_ui.closeBox(c_ui.uiInterface) 49 | 50 | self.assertTrue(hasattr(self.obj.b_box1, "metadata")) 51 | self.assertTrue(hasattr(self.obj.b_box1.b_box2, "metadata")) 52 | self.assertTrue(hasattr(self.obj.b_box1.b_box3, "metadata")) 53 | 54 | self.assertDictEqual(self.obj.b_box1.metadata, 55 | {b"key1": b"val1"}) 56 | self.assertDictEqual(self.obj.b_box1.b_box2.metadata, 57 | {b"key1": b"val1", b"key2": b"val2"}) 58 | self.assertDictEqual(self.obj.b_box1.b_box3.metadata, 59 | {b"key2": b"val2", b"key3": b"val3"}) 60 | 61 | def test_declare_parameter(self): 62 | "Test declaration of parameter meta-data." 63 | 64 | c_ui = self.ui.ui 65 | 66 | param1 = self.ffi.new("FAUSTFLOAT*", 0.0) 67 | param2 = self.ffi.new("FAUSTFLOAT*", 0.5) 68 | param3 = self.ffi.new("FAUSTFLOAT*", 1.0) 69 | 70 | c_ui.declare(c_ui.uiInterface, param1, b"key1", b"val1") 71 | c_ui.addVerticalSlider(c_ui.uiInterface, b"slider1", param1, 0.0, 0.0, 72 | 2.0, 0.1) 73 | c_ui.declare(c_ui.uiInterface, param2, b"key1", b"val1") 74 | c_ui.declare(c_ui.uiInterface, param2, b"key2", b"val2") 75 | c_ui.addHorizontalSlider(c_ui.uiInterface, b"slider2", param2, 0.0, 76 | 0.0, 2.0, 0.1) 77 | c_ui.declare(c_ui.uiInterface, param3, b"key2", b"val2") 78 | c_ui.declare(c_ui.uiInterface, param3, b"key3", b"val3") 79 | c_ui.addNumEntry(c_ui.uiInterface, b"numentry", param3, 0.0, 0.0, 2.0, 80 | 0.1) 81 | 82 | # closeBox triggers assignment of parameter meta-data 83 | c_ui.closeBox(c_ui.uiInterface) 84 | 85 | self.assertTrue(hasattr(self.obj.p_slider1, "metadata")) 86 | self.assertTrue(hasattr(self.obj.p_slider2, "metadata")) 87 | self.assertTrue(hasattr(self.obj.p_numentry, "metadata")) 88 | 89 | self.assertDictEqual(self.obj.p_slider1.metadata, 90 | {b"key1": b"val1"}) 91 | self.assertDictEqual(self.obj.p_slider2.metadata, 92 | {b"key1": b"val1", b"key2": b"val2"}) 93 | self.assertDictEqual(self.obj.p_numentry.metadata, 94 | {b"key2": b"val2", b"key3": b"val3"}) 95 | 96 | def test_openVerticalBox(self): 97 | "Test the openVerticalBox C callback." 98 | 99 | c_ui = self.ui.ui 100 | 101 | c_ui.openVerticalBox(c_ui.uiInterface, b"box") 102 | c_ui.closeBox(c_ui.uiInterface) 103 | 104 | self.assertTrue(hasattr(self.obj, "b_box")) 105 | self.assertEqual(self.obj.b_box.layout, "vertical") 106 | self.assertEqual(self.obj.b_box.label, b"box") 107 | 108 | def test_openHorizontalBox(self): 109 | "Test the openHorizontalBox C callback." 110 | 111 | c_ui = self.ui.ui 112 | 113 | c_ui.openHorizontalBox(c_ui.uiInterface, b"box") 114 | c_ui.closeBox(c_ui.uiInterface) 115 | 116 | self.assertTrue(hasattr(self.obj, "b_box")) 117 | self.assertEqual(self.obj.b_box.layout, "horizontal") 118 | self.assertEqual(self.obj.b_box.label, b"box") 119 | 120 | def test_openTabBox(self): 121 | "Test the openTabBox C callback." 122 | 123 | c_ui = self.ui.ui 124 | 125 | c_ui.openTabBox(c_ui.uiInterface, b"box") 126 | c_ui.closeBox(c_ui.uiInterface) 127 | 128 | self.assertTrue(hasattr(self.obj, "b_box")) 129 | self.assertEqual(self.obj.b_box.layout, "tab") 130 | self.assertEqual(self.obj.b_box.label, b"box") 131 | 132 | def test_closeBox(self): 133 | "Test the closeBox C callback." 134 | 135 | c_ui = self.ui.ui 136 | 137 | c_ui.openVerticalBox(c_ui.uiInterface, b"box1") 138 | c_ui.openHorizontalBox(c_ui.uiInterface, b"box2") 139 | c_ui.closeBox(c_ui.uiInterface) 140 | c_ui.openTabBox(c_ui.uiInterface, b"box3") 141 | c_ui.closeBox(c_ui.uiInterface) 142 | c_ui.closeBox(c_ui.uiInterface) 143 | c_ui.openTabBox(c_ui.uiInterface, b"box4") 144 | c_ui.closeBox(c_ui.uiInterface) 145 | 146 | self.assertTrue(hasattr(self.obj, "b_box1")) 147 | self.assertTrue(hasattr(self.obj.b_box1, "b_box2")) 148 | self.assertTrue(hasattr(self.obj.b_box1, "b_box3")) 149 | self.assertTrue(hasattr(self.obj, "b_box4")) 150 | 151 | def test_addHorizontalSlider(self): 152 | "Test the addHorizontalSlider C callback." 153 | 154 | c_ui = self.ui.ui 155 | 156 | param = self.ffi.new("FAUSTFLOAT*", 1.0) 157 | self.assertEqual(param[0], 1.0) 158 | 159 | c_ui.addHorizontalSlider(c_ui.uiInterface, b"slider", param, 0.0, 0.0, 160 | 2.0, 0.1) 161 | self.assertTrue(hasattr(self.obj, "p_slider")) 162 | self.assertEqual(self.obj.p_slider.label, b"slider") 163 | self.assertEqual(self.obj.p_slider.zone, 0.0) 164 | self.assertEqual(self.obj.p_slider.min, 0.0) 165 | self.assertEqual(self.obj.p_slider.max, 2.0) 166 | self.assertAlmostEqual(self.obj.p_slider.step, 0.1, 8) 167 | self.assertEqual(self.obj.p_slider.default, 0.0) 168 | self.assertEqual(self.obj.p_slider.metadata, {}) 169 | self.assertEqual(self.obj.p_slider.type, "HorizontalSlider") 170 | 171 | self.obj.p_slider.zone = 0.5 172 | self.assertEqual(self.obj.p_slider.zone, param[0]) 173 | 174 | def test_addVerticalSlider(self): 175 | "Test the addVerticalSlider C callback." 176 | 177 | c_ui = self.ui.ui 178 | 179 | param = self.ffi.new("FAUSTFLOAT*", 1.0) 180 | self.assertEqual(param[0], 1.0) 181 | 182 | c_ui.addVerticalSlider(c_ui.uiInterface, b"slider", param, 0.0, 0.0, 183 | 2.0, 0.1) 184 | self.assertTrue(hasattr(self.obj, "p_slider")) 185 | self.assertEqual(self.obj.p_slider.label, b"slider") 186 | self.assertEqual(self.obj.p_slider.zone, 0.0) 187 | self.assertEqual(self.obj.p_slider.min, 0.0) 188 | self.assertEqual(self.obj.p_slider.max, 2.0) 189 | self.assertAlmostEqual(self.obj.p_slider.step, 0.1, 8) 190 | self.assertEqual(self.obj.p_slider.default, 0.0) 191 | self.assertEqual(self.obj.p_slider.metadata, {}) 192 | self.assertEqual(self.obj.p_slider.type, "VerticalSlider") 193 | 194 | self.obj.p_slider.zone = 0.5 195 | self.assertEqual(self.obj.p_slider.zone, param[0]) 196 | 197 | def test_addNumEntry(self): 198 | "Test the addNumEntry C callback." 199 | 200 | c_ui = self.ui.ui 201 | 202 | param = self.ffi.new("FAUSTFLOAT*", 1.0) 203 | self.assertEqual(param[0], 1.0) 204 | 205 | c_ui.addNumEntry(c_ui.uiInterface, b"numentry", param, 0.0, 0.0, 2.0, 206 | 0.1) 207 | self.assertTrue(hasattr(self.obj, "p_numentry")) 208 | self.assertEqual(self.obj.p_numentry.label, b"numentry") 209 | self.assertEqual(self.obj.p_numentry.zone, 0.0) 210 | self.assertEqual(self.obj.p_numentry.min, 0.0) 211 | self.assertEqual(self.obj.p_numentry.max, 2.0) 212 | self.assertAlmostEqual(self.obj.p_numentry.step, 0.1, 8) 213 | self.assertEqual(self.obj.p_numentry.default, 0.0) 214 | self.assertEqual(self.obj.p_numentry.metadata, {}) 215 | self.assertEqual(self.obj.p_numentry.type, "NumEntry") 216 | 217 | self.obj.p_numentry.zone = 0.5 218 | self.assertEqual(self.obj.p_numentry.zone, param[0]) 219 | 220 | def test_addButton(self): 221 | "Test the addButton C callback." 222 | 223 | c_ui = self.ui.ui 224 | 225 | param = self.ffi.new("FAUSTFLOAT*", 1.0) 226 | c_ui.addButton(c_ui.uiInterface, b"button", param) 227 | self.assertTrue(hasattr(self.obj, "p_button")) 228 | self.assertEqual(self.obj.p_button.label, b"button") 229 | self.assertEqual(self.obj.p_button.zone, 0.0) 230 | self.assertEqual(self.obj.p_button.min, 0.0) 231 | self.assertEqual(self.obj.p_button.max, 1.0) 232 | self.assertEqual(self.obj.p_button.step, 1) 233 | self.assertEqual(self.obj.p_button.default, 0.0) 234 | self.assertEqual(self.obj.p_button.metadata, {}) 235 | self.assertEqual(self.obj.p_button.type, "Button") 236 | 237 | self.obj.p_button.zone = 1 238 | self.assertEqual(self.obj.p_button.zone, param[0]) 239 | 240 | def test_addToggleButton(self): 241 | "Test the addToggleButton C callback." 242 | 243 | c_ui = self.ui.ui 244 | 245 | param = self.ffi.new("FAUSTFLOAT*", 1.0) 246 | c_ui.addToggleButton(c_ui.uiInterface, b"button", param) 247 | self.assertTrue(hasattr(self.obj, "p_button")) 248 | self.assertEqual(self.obj.p_button.label, b"button") 249 | self.assertEqual(self.obj.p_button.zone, 0.0) 250 | self.assertEqual(self.obj.p_button.min, 0.0) 251 | self.assertEqual(self.obj.p_button.max, 1.0) 252 | self.assertEqual(self.obj.p_button.step, 1) 253 | self.assertEqual(self.obj.p_button.default, 0.0) 254 | self.assertEqual(self.obj.p_button.metadata, {}) 255 | self.assertEqual(self.obj.p_button.type, "ToggleButton") 256 | 257 | self.obj.p_button.zone = 1 258 | self.assertEqual(self.obj.p_button.zone, param[0]) 259 | 260 | def test_addCheckButton(self): 261 | "Test the addCheckButton C callback." 262 | 263 | c_ui = self.ui.ui 264 | 265 | param = self.ffi.new("FAUSTFLOAT*", 1.0) 266 | c_ui.addCheckButton(c_ui.uiInterface, b"button", param) 267 | self.assertTrue(hasattr(self.obj, "p_button")) 268 | self.assertEqual(self.obj.p_button.label, b"button") 269 | self.assertEqual(self.obj.p_button.zone, 0.0) 270 | self.assertEqual(self.obj.p_button.min, 0.0) 271 | self.assertEqual(self.obj.p_button.max, 1.0) 272 | self.assertEqual(self.obj.p_button.step, 1) 273 | self.assertEqual(self.obj.p_button.default, 0.0) 274 | self.assertEqual(self.obj.p_button.metadata, {}) 275 | self.assertEqual(self.obj.p_button.type, "CheckButton") 276 | 277 | self.obj.p_button.zone = 1 278 | self.assertEqual(self.obj.p_button.zone, param[0]) 279 | -------------------------------------------------------------------------------- /FAUSTPy/wrapper.py: -------------------------------------------------------------------------------- 1 | import cffi 2 | import os 3 | from subprocess import check_output 4 | from tempfile import NamedTemporaryFile 5 | from string import Template 6 | from . import python_ui, python_meta, python_dsp 7 | 8 | FAUST_PATH = "" 9 | FAUSTFLOATS = frozenset(("float", "double", "long double")) 10 | 11 | 12 | class FAUST(object): 13 | """Wraps a FAUST DSP using the CFFI. The DSP file is compiled to C, which 14 | is then compiled and linked to the running Python interpreter by the CFFI. 15 | It exposes the compute() function of the DSP along with some other 16 | attributes (see below). 17 | """ 18 | 19 | def __init__(self, faust_dsp, fs, 20 | faust_float="float", 21 | faust_flags=[], 22 | dsp_class=python_dsp.PythonDSP, 23 | ui_class=python_ui.PythonUI, 24 | meta_class=python_meta.PythonMeta, 25 | **kwargs): 26 | """ 27 | Initialise a FAUST object. 28 | 29 | Parameters: 30 | ----------- 31 | 32 | faust_dsp : string / bytes 33 | This can be either the path to a FAUST DSP file (which should end 34 | in ".dsp") or a string of FAUST code. Note that in Python 3 a code 35 | string must be of type "bytes". 36 | fs : int 37 | The sampling rate the FAUST DSP should be initialised with. 38 | faust_float : string (optional) 39 | The value of the FAUSTFLOAT type. This is used internally by FAUST 40 | to generalise to different precisions. Possible values are "float", 41 | "double" or "long double". 42 | faust_flags : list of strings (optional) 43 | A list of additional flags to pass to the FAUST compiler, which are 44 | appended to "-lang c" (since FAUSTPy requires the FAUST C backend). 45 | 46 | And in case you want to write your own DSP/UI/Meta class (for whatever 47 | reason), you can override any of the following arguments: 48 | 49 | dsp_class : PythonDSP-like (optional) 50 | The constructor of a DSP wrapper. 51 | ui_class : PythonUI-like (optional) 52 | The constructor of a UIGlue wrapper. 53 | meta_class : PythonMeta-like (optional) 54 | The constructor of a MetaGlue wrapper. 55 | 56 | You may also pass additional keyword arguments, which will get passed 57 | directly to cffi.FFI.verify(). This lets you override the compiler 58 | flags, for example. 59 | 60 | Notes: 61 | ------ 62 | 63 | You can override the C compiler (at least on Unix-like systems) by 64 | overriding the ``CC`` and ``LDSHARED`` environment variables, which you 65 | can verify by viewing the output of the test suite when called via 66 | setup.py, for example using clang:: 67 | 68 | CC=clang LDSHARED="clang -pthreads -shared" python setup.py test 69 | 70 | The default compiler flags are "-std=c99 -march=native -O3". The 71 | reasons for this are: 72 | 73 | - compilation happens at run time, so -march=native should be safe, 74 | - FAUST programs usually profit from -O3, especially since it activates 75 | auto-vectorisation, and 76 | - since additional flags are appended to this default, you *can* 77 | override it in situations where it is unsuitable. 78 | """ 79 | 80 | if faust_float not in FAUSTFLOATS: 81 | raise ValueError("Invalid value for faust_float!") 82 | 83 | self.FAUST_PATH = FAUST_PATH 84 | self.FAUST_FLAGS = ["-lang", "c"] + faust_flags 85 | self.is_inline = False 86 | 87 | # compile the FAUST DSP to C and compile it with the CFFI 88 | with NamedTemporaryFile(suffix=".dsp") as dsp_file: 89 | 90 | # Two things: 91 | # 92 | # 1.) In Python 3, in-line code *has* to be a byte array, so if 93 | # faust_dsp is a string the test is short-circuited and we assume 94 | # that it represents a file name. 95 | # 96 | # 2.) In Python 2, string literals are all byte arrays, so also 97 | # check whether the string ends with ".dsp", in which case we 98 | # assume that it represents a file name, otherwise it must be a 99 | # code block. 100 | if type(faust_dsp) is bytes and not faust_dsp.endswith(b".dsp"): 101 | dsp_file.write(faust_dsp) 102 | 103 | # make sure the data is immediately written to disc 104 | dsp_file.flush() 105 | 106 | faust_dsp = dsp_file.name 107 | 108 | self.is_inline = True 109 | 110 | c_code = self.__compile_faust(faust_dsp, faust_float) 111 | self.__ffi, self.__C = self.__gen_ffi( 112 | c_code, faust_float, faust_dsp, **kwargs 113 | ) 114 | 115 | # initialise the DSP object 116 | self.__dsp = dsp_class(self.__C, self.__ffi, fs) 117 | 118 | # set up the UI 119 | if ui_class: 120 | UI = ui_class(self.__ffi, self.__dsp) 121 | self.__C.buildUserInterfacemydsp(self.__dsp.dsp, UI.ui) 122 | 123 | # get the meta-data of the DSP 124 | if meta_class: 125 | Meta = meta_class(self.__ffi, self.__dsp) 126 | self.__C.metadatamydsp(Meta.meta) 127 | 128 | # add shortcuts to the compute* functions 129 | self.compute = self.__dsp.compute 130 | self.compute2 = self.__dsp.compute2 131 | 132 | # expose some internal attributes as properties 133 | dsp = property(fget=lambda x: x.__dsp, 134 | doc="The internal PythonDSP object.") 135 | 136 | def __compile_faust(self, dsp_fname, faust_float): 137 | 138 | if faust_float == "float": 139 | self.FAUST_FLAGS.append("-single") 140 | elif faust_float == "double": 141 | self.FAUST_FLAGS.append("-double") 142 | elif faust_float == "long double": 143 | self.FAUST_FLAGS.append("-quad") 144 | 145 | if self.FAUST_PATH: 146 | faust_cmd = os.sep.join([self.FAUST_PATH, "faust"]) 147 | else: 148 | faust_cmd = "faust" 149 | 150 | faust_args = self.FAUST_FLAGS + [dsp_fname] 151 | 152 | return check_output([faust_cmd] + faust_args).decode() 153 | 154 | def __gen_ffi(self, c_code, faust_float, dsp_fname, **kwargs): 155 | 156 | # define the ffi object 157 | ffi = cffi.FFI() 158 | 159 | # if the DSP is from an inline code string we replace the "label" 160 | # argument to the first call to open*Box() (which is always the DSP 161 | # file base name sans suffix) with something predictable so that the 162 | # caching mechanism of the CFFI still works, but make it somewhat 163 | # unusual to reduce the likelihood of a name clash 164 | if self.is_inline: 165 | fname = os.path.basename(dsp_fname).rpartition('.')[0] 166 | c_code = c_code.replace(fname, "123first_box") 167 | 168 | c_flags = ["-std=c99", "-march=native", "-O3"] 169 | kwargs["extra_compile_args"] = c_flags + \ 170 | kwargs.get("extra_compile_args", []) 171 | 172 | # declare various types and functions 173 | # 174 | # These declarations need to be here -- independently of the code in 175 | # the ffi.verify() call below -- so that the CFFI knows the contents of 176 | # the data structures and the available functions. 177 | cdefs = "typedef {0} FAUSTFLOAT;".format(faust_float) + """ 178 | 179 | typedef struct { 180 | void *mInterface; 181 | void (*declare)(void* interface, const char* key, const char* value); 182 | } MetaGlue; 183 | 184 | typedef struct { 185 | // widget layouts 186 | void (*openVerticalBox)(void*, const char* label); 187 | void (*openHorizontalBox)(void*, const char* label); 188 | void (*openTabBox)(void*, const char* label); 189 | void (*declare)(void*, FAUSTFLOAT*, char*, char*); 190 | // passive widgets 191 | void (*addNumDisplay)(void*, const char* label, FAUSTFLOAT* zone, int p); 192 | void (*addTextDisplay)(void*, const char* label, FAUSTFLOAT* zone, const char* names[], FAUSTFLOAT min, FAUSTFLOAT max); 193 | void (*addHorizontalBargraph)(void*, const char* label, FAUSTFLOAT* zone, FAUSTFLOAT min, FAUSTFLOAT max); 194 | void (*addVerticalBargraph)(void*, const char* label, FAUSTFLOAT* zone, FAUSTFLOAT min, FAUSTFLOAT max); 195 | // active widgets 196 | void (*addHorizontalSlider)(void*, const char* label, FAUSTFLOAT* zone, FAUSTFLOAT init, FAUSTFLOAT min, FAUSTFLOAT max, FAUSTFLOAT step); 197 | void (*addVerticalSlider)(void*, const char* label, FAUSTFLOAT* zone, FAUSTFLOAT init, FAUSTFLOAT min, FAUSTFLOAT max, FAUSTFLOAT step); 198 | void (*addButton)(void*, const char* label, FAUSTFLOAT* zone); 199 | void (*addToggleButton)(void*, const char* label, FAUSTFLOAT* zone); 200 | void (*addCheckButton)(void*, const char* label, FAUSTFLOAT* zone); 201 | void (*addNumEntry)(void*, const char* label, FAUSTFLOAT* zone, FAUSTFLOAT init, FAUSTFLOAT min, FAUSTFLOAT max, FAUSTFLOAT step); 202 | void (*closeBox)(void*); 203 | void* uiInterface; 204 | } UIGlue; 205 | 206 | typedef struct {...;} mydsp; 207 | 208 | mydsp *newmydsp(); 209 | void deletemydsp(mydsp*); 210 | void metadatamydsp(MetaGlue* m); 211 | int getSampleRatemydsp(mydsp* dsp); 212 | int getNumInputsmydsp(mydsp* dsp); 213 | int getNumOutputsmydsp(mydsp* dsp); 214 | int getInputRatemydsp(mydsp* dsp, int channel); 215 | int getOutputRatemydsp(mydsp* dsp, int channel); 216 | void classInitmydsp(int samplingFreq); 217 | void instanceInitmydsp(mydsp* dsp, int samplingFreq); 218 | void initmydsp(mydsp* dsp, int samplingFreq); 219 | void buildUserInterfacemydsp(mydsp* dsp, UIGlue* interface); 220 | void computemydsp(mydsp* dsp, int count, FAUSTFLOAT** inputs, FAUSTFLOAT** outputs); 221 | """ 222 | ffi.cdef(cdefs) 223 | 224 | # compile the code 225 | C = ffi.verify( 226 | Template(""" 227 | #define FAUSTFLOAT ${FAUSTFLOAT} 228 | 229 | // helper function definitions 230 | FAUSTFLOAT min(FAUSTFLOAT x, FAUSTFLOAT y) { return x < y ? x : y;}; 231 | FAUSTFLOAT max(FAUSTFLOAT x, FAUSTFLOAT y) { return x > y ? x : y;}; 232 | 233 | // the MetaGlue struct that will be wrapped 234 | typedef struct { 235 | void *mInterface; 236 | void (*declare)(void* interface, const char* key, const char* value); 237 | } MetaGlue; 238 | 239 | // the UIGlue struct that will be wrapped 240 | typedef struct { 241 | // widget layouts 242 | void (*openVerticalBox)(void*, const char* label); 243 | void (*openHorizontalBox)(void*, const char* label); 244 | void (*openTabBox)(void*, const char* label); 245 | void (*declare)(void*, FAUSTFLOAT*, char*, char*); 246 | // passive widgets 247 | void (*addNumDisplay)(void*, const char* label, FAUSTFLOAT* zone, int p); 248 | void (*addTextDisplay)(void*, const char* label, FAUSTFLOAT* zone, const char* names[], FAUSTFLOAT min, FAUSTFLOAT max); 249 | void (*addHorizontalBargraph)(void*, const char* label, FAUSTFLOAT* zone, FAUSTFLOAT min, FAUSTFLOAT max); 250 | void (*addVerticalBargraph)(void*, const char* label, FAUSTFLOAT* zone, FAUSTFLOAT min, FAUSTFLOAT max); 251 | // active widgets 252 | void (*addHorizontalSlider)(void*, const char* label, FAUSTFLOAT* zone, FAUSTFLOAT init, FAUSTFLOAT min, FAUSTFLOAT max, FAUSTFLOAT step); 253 | void (*addVerticalSlider)(void*, const char* label, FAUSTFLOAT* zone, FAUSTFLOAT init, FAUSTFLOAT min, FAUSTFLOAT max, FAUSTFLOAT step); 254 | void (*addButton)(void*, const char* label, FAUSTFLOAT* zone); 255 | void (*addToggleButton)(void*, const char* label, FAUSTFLOAT* zone); 256 | void (*addCheckButton)(void*, const char* label, FAUSTFLOAT* zone); 257 | void (*addNumEntry)(void*, const char* label, FAUSTFLOAT* zone, FAUSTFLOAT init, FAUSTFLOAT min, FAUSTFLOAT max, FAUSTFLOAT step); 258 | void (*closeBox)(void*); 259 | void* uiInterface; 260 | } UIGlue; 261 | 262 | ${FAUSTC} 263 | """).substitute(FAUSTFLOAT=faust_float, FAUSTC=c_code), 264 | **kwargs 265 | ) 266 | 267 | return ffi, C 268 | -------------------------------------------------------------------------------- /FAUSTPy/python_ui.py: -------------------------------------------------------------------------------- 1 | # a string consisting of characters that are valid identifiers in both 2 | # Python 2 and Python 3 3 | import string 4 | valid_ident = string.ascii_letters + string.digits + "_" 5 | 6 | 7 | def str_to_identifier(s): 8 | """Convert a "bytes" to a valid (in Python 2 and 3) identifier.""" 9 | 10 | # convert str/bytes to unicode string 11 | s = s.decode() 12 | 13 | def filter_chars(s): 14 | for c in s: 15 | # periods are used for abbreviations and look ugly when converted 16 | # to underscore, so filter them out completely 17 | if c == ".": 18 | yield "" 19 | elif c in valid_ident or c == "_": 20 | yield c 21 | else: 22 | yield "_" 23 | 24 | if s[0] in string.digits: 25 | s = "_"+s 26 | 27 | return ''.join(filter_chars(s)) 28 | 29 | 30 | class Param(object): 31 | """A UI parameter object. 32 | 33 | This objects represents a FAUST UI input. It makes sure to enforce the 34 | constraints specified by the minimum, maximum and step size. 35 | 36 | This object implements the descriptor protocol: reading it works just like 37 | normal objects, but assignment is redirects to its "zone" attribute. 38 | """ 39 | 40 | def __init__(self, label, zone, init, min, max, step, param_type): 41 | """Initialise a Param object. 42 | 43 | Parameters: 44 | ----------- 45 | 46 | label : str 47 | The full label as specified in the FAUST DSP file. 48 | zone : cffi.CData 49 | Points to the FAUSTFLOAT object inside the DSP C object. 50 | init : float 51 | The initialisation value. 52 | min : float 53 | The minimum allowed value. 54 | max : float 55 | The maximum allowed value. 56 | step : float 57 | The step size of the parameter. 58 | param_type : str 59 | The parameter type (e.g., HorizontalSlider) 60 | """ 61 | 62 | # NOTE: _zone is a CData holding a float* 63 | self.label = label 64 | self._zone = zone 65 | self._zone[0] = init 66 | self.min = min 67 | self.max = max 68 | self.step = step 69 | self.type = param_type 70 | 71 | # extra attributes 72 | self.default = init 73 | self.metadata = {} 74 | self.__doc__ = "min={0}, max={1}, step={2}".format(min, max, step) 75 | 76 | def __zone_getter(self): 77 | return self._zone[0] 78 | 79 | def __zone_setter(self, x): 80 | if x >= self.max: 81 | self._zone[0] = self.max 82 | elif x <= self.min: 83 | self._zone[0] = self.min 84 | else: 85 | self._zone[0] = self.min + round((x-self.min)/self.step)*self.step 86 | 87 | zone = property(fget=__zone_getter, fset=__zone_setter, 88 | doc="Pointer to the value of the parameter.") 89 | 90 | def __set__(self, obj, value): 91 | 92 | self.zone = value 93 | 94 | 95 | class Box(object): 96 | def __init__(self, label, layout): 97 | self.label = label 98 | self.layout = layout 99 | self.metadata = {} 100 | 101 | def __setattr__(self, name, value): 102 | 103 | if name in self.__dict__ and hasattr(self.__dict__[name], "__set__"): 104 | self.__dict__[name].__set__(self, value) 105 | else: 106 | object.__setattr__(self, name, value) 107 | 108 | 109 | # TODO: implement the *Display() and *Bargraph() methods 110 | class PythonUI(object): 111 | """ 112 | Maps the UI elements of a FAUST DSP to attributes of another object, 113 | specifically a FAUST wrapper object. 114 | 115 | In FAUST, UI's are specified by the DSP object, which calls methods of a UI 116 | object to create them. The PythonUI class implements such a UI object. It 117 | creates C callbacks to its methods and stores then in a UI struct, which 118 | can then be passed to the buildUserInterface() function of a FAUST DSP 119 | object. 120 | 121 | The DSP object basically calls the methods of the PythonUI class from C via 122 | the callbacks in the UI struct and thus creates a hierarchical namespace of 123 | attributes which map back to the DSP's UI elements. 124 | 125 | Notes: 126 | ------ 127 | 128 | Box and Param attributes are prefixed with "b_" and "p_", respectively, in 129 | order to differentiate them from each other and from regular attributes. 130 | 131 | Boxes and parameters without a label are given a default name of "anon", 132 | where N is an integer (e.g., "p_anon1" for a label-less parameter). 133 | 134 | See also: 135 | --------- 136 | 137 | FAUSTPy.Param - wraps the UI input parameters. 138 | """ 139 | 140 | def __init__(self, ffi, obj=None): 141 | """ 142 | Initialise a PythonUI object. 143 | 144 | Parameters: 145 | ----------- 146 | 147 | ffi : cffi.FFI 148 | The CFFI instance that holds all the data type declarations. 149 | obj : object (optional) 150 | The Python object to which the UI elements are to be added. If 151 | None (the default) the PythonUI instance manipulates itself. 152 | """ 153 | 154 | if obj: 155 | self.__boxes = [obj] 156 | else: 157 | self.__boxes = [self] 158 | 159 | self.__num_anon_boxes = [0] 160 | self.__num_anon_params = [0] 161 | self.__metadata = [{}] 162 | self.__group_metadata = {} 163 | 164 | # define C callbacks that know the global PythonUI object 165 | @ffi.callback("void(void*, FAUSTFLOAT*, char*, char*)") 166 | def declare(mInterface, zone, key, value): 167 | self.declare(zone, ffi.string(key), ffi.string(value)) 168 | 169 | @ffi.callback("void(void*, char*)") 170 | def openVerticalBox(mInterface, label): 171 | self.openVerticalBox(ffi.string(label)) 172 | 173 | @ffi.callback("void(void*, char*)") 174 | def openHorizontalBox(mInterface, label): 175 | self.openHorizontalBox(ffi.string(label)) 176 | 177 | @ffi.callback("void(void*, char*)") 178 | def openTabBox(mInterface, label): 179 | self.openTabBox(ffi.string(label)) 180 | 181 | @ffi.callback("void(void*)") 182 | def closeBox(mInterface): 183 | self.closeBox() 184 | 185 | @ffi.callback("void(void*, char*, FAUSTFLOAT*, FAUSTFLOAT, FAUSTFLOAT, FAUSTFLOAT, FAUSTFLOAT)") 186 | def addHorizontalSlider(ignore, c_label, zone, init, min, max, step): 187 | label = ffi.string(c_label) 188 | self.addHorizontalSlider(label, zone, init, min, max, step) 189 | 190 | @ffi.callback("void(void*, char*, FAUSTFLOAT*, FAUSTFLOAT, FAUSTFLOAT, FAUSTFLOAT, FAUSTFLOAT)") 191 | def addVerticalSlider(ignore, c_label, zone, init, min, max, step): 192 | label = ffi.string(c_label) 193 | self.addVerticalSlider(label, zone, init, min, max, step) 194 | 195 | @ffi.callback("void(void*, char*, FAUSTFLOAT*, FAUSTFLOAT, FAUSTFLOAT, FAUSTFLOAT, FAUSTFLOAT)") 196 | def addNumEntry(ignore, c_label, zone, init, min, max, step): 197 | label = ffi.string(c_label) 198 | self.addNumEntry(label, zone, init, min, max, step) 199 | 200 | @ffi.callback("void(void*, char*, FAUSTFLOAT*)") 201 | def addButton(ignore, c_label, zone): 202 | self.addButton(ffi.string(c_label), zone) 203 | 204 | @ffi.callback("void(void*, char*, FAUSTFLOAT*)") 205 | def addToggleButton(ignore, c_label, zone): 206 | self.addToggleButton(ffi.string(c_label), zone) 207 | 208 | @ffi.callback("void(void*, char*, FAUSTFLOAT*)") 209 | def addCheckButton(ignore, c_label, zone): 210 | self.addCheckButton(ffi.string(c_label), zone) 211 | 212 | @ffi.callback("void(void*, char*, FAUSTFLOAT*, int)") 213 | def addNumDisplay(ignore, c_label, zone, p): 214 | self.addNumDisplay(ffi.string(c_label), zone, p) 215 | 216 | @ffi.callback("void(void*, char*, FAUSTFLOAT*, char*[], FAUSTFLOAT, FAUSTFLOAT)") 217 | def addTextDisplay(ignore, c_label, zone, names, min, max): 218 | self.addTextDisplay(ffi.string(c_label), zone, names, min, max) 219 | 220 | @ffi.callback("void(void*, char*, FAUSTFLOAT*, FAUSTFLOAT, FAUSTFLOAT)") 221 | def addHorizontalBargraph(ignore, c_label, zone, min, max): 222 | label = ffi.string(c_label) 223 | self.addHorizontalBargraph(label, zone, min, max) 224 | 225 | @ffi.callback("void(void*, char*, FAUSTFLOAT*, FAUSTFLOAT, FAUSTFLOAT)") 226 | def addVerticalBargraph(ignore, c_label, zone, min, max): 227 | label = ffi.string(c_label) 228 | self.addVerticalBargraph(label, zone, min, max) 229 | 230 | # create a UI object and store the above callbacks as it's function 231 | # pointers; also store the above functions in self so that they don't 232 | # get garbage collected 233 | ui = ffi.new("UIGlue*") 234 | ui.declare = self.__declare_c = declare 235 | ui.openVerticalBox = self.__openVerticalBox_c = openVerticalBox 236 | ui.openHorizontalBox = self.__openHorizontalBox_c = openHorizontalBox 237 | ui.openTabBox = self.__openTabBox_c = openTabBox 238 | ui.closeBox = self.__closeBox_c = closeBox 239 | ui.addHorizontalSlider = self.__addHorizontalSlider_c = addHorizontalSlider 240 | ui.addVerticalSlider = self.__addVerticalSlider_c = addVerticalSlider 241 | ui.addNumEntry = self.__addNumEntry_c = addNumEntry 242 | ui.addButton = self.__addButton_c = addButton 243 | ui.addToggleButton = self.__addToggleButton_c = addToggleButton 244 | ui.addCheckButton = self.__addCheckButton_c = addCheckButton 245 | ui.addNumDisplay = self.__addNumDisplay_c = addNumDisplay 246 | ui.addTextDisplay = self.__addTextDisplay_c = addTextDisplay 247 | ui.addHorizontalBargraph = self.__addHorizontalBargraph_c = addHorizontalBargraph 248 | ui.addVerticalBargraph = self.__addVerticalBargraph_c = addVerticalBargraph 249 | ui.uiInterface = ffi.NULL # we don't use this anyway 250 | 251 | self.__ui = ui 252 | self.__ffi = ffi 253 | 254 | ui = property(fget=lambda x: x.__ui, 255 | doc="The UI struct that calls back to its parent object.") 256 | 257 | def declare(self, zone, key, value): 258 | 259 | if zone == self.__ffi.NULL: 260 | # set group meta-data 261 | # 262 | # the group meta-data is stored temporarily here and is set during 263 | # the next openBox() 264 | self.__group_metadata[key] = value 265 | else: 266 | # store parameter meta-data 267 | # 268 | # since the only identifier we get is the zone (pointer to the 269 | # control value), we have to store this for now and assign it to 270 | # the corresponding parameter later in closeBox() 271 | if zone not in self.__metadata[-1]: 272 | self.__metadata[-1][zone] = {} 273 | self.__metadata[-1][zone][key] = value 274 | 275 | ########################## 276 | # stuff to do with boxes 277 | ########################## 278 | 279 | def openBox(self, label, layout): 280 | # If the label is an empty string, don't do anything, just stay in the 281 | # current Box 282 | if label: 283 | # special case the first box, which is always "0x00" (the ASCII 284 | # Null character), so that it has a consistent name 285 | if label.decode() == '0x00': 286 | sane_label = "ui" 287 | else: 288 | sane_label = "b_"+str_to_identifier(label) 289 | else: 290 | # if the label is empty, create a default label 291 | self.__num_anon_boxes[-1] += 1 292 | sane_label = "b_anon" + str(self.__num_anon_boxes[-1]) 293 | 294 | # create a new sub-Box and make it a child of the current Box 295 | box = Box(label, layout) 296 | setattr(self.__boxes[-1], sane_label, box) 297 | self.__boxes.append(box) 298 | 299 | # store the group meta-data in the newly opened box and reset 300 | # self.__group_metadata 301 | self.__boxes[-1].metadata.update(self.__group_metadata) 302 | self.__group_metadata = {} 303 | 304 | self.__num_anon_boxes.append(0) 305 | self.__num_anon_params.append(0) 306 | self.__metadata.append({}) 307 | 308 | def openVerticalBox(self, label): 309 | 310 | self.openBox(label, "vertical") 311 | 312 | def openHorizontalBox(self, label): 313 | 314 | self.openBox(label, "horizontal") 315 | 316 | def openTabBox(self, label): 317 | 318 | self.openBox(label, "tab") 319 | 320 | def closeBox(self): 321 | 322 | cur_metadata = self.__metadata.pop() 323 | 324 | # iterate over the objects in the current box and assign the meta-data 325 | # to the correct parameters 326 | for p in self.__boxes[-1].__dict__.values(): 327 | 328 | # TODO: add the Display class (or whatever it will be called) to 329 | # this list once *Display and *Bargraph are implemented 330 | if type(p) not in (Param,): 331 | continue 332 | 333 | # iterate over the meta-data that has accumulated in the current 334 | # box and assign it to its corresponding Param objects 335 | for zone, mdata in cur_metadata.items(): 336 | if p._zone == zone: 337 | p.metadata.update(mdata) 338 | 339 | self.__num_anon_boxes.pop() 340 | self.__num_anon_params.pop() 341 | 342 | # now pop the box off the stack 343 | self.__boxes.pop() 344 | 345 | ########################## 346 | # stuff to do with inputs 347 | ########################## 348 | 349 | def add_input(self, label, zone, init, min, max, step, param_type): 350 | 351 | if label: 352 | sane_label = str_to_identifier(label) 353 | else: 354 | # if the label is empty, create a default label 355 | self.__num_anon_params[-1] += 1 356 | sane_label = "anon" + str(self.__num_anon_params[-1]) 357 | 358 | setattr(self.__boxes[-1], "p_"+sane_label, 359 | Param(label, zone, init, min, max, step, param_type)) 360 | 361 | def addHorizontalSlider(self, label, zone, init, min, max, step): 362 | 363 | self.add_input(label, zone, init, min, max, step, "HorizontalSlider") 364 | 365 | def addVerticalSlider(self, label, zone, init, min, max, step): 366 | 367 | self.add_input(label, zone, init, min, max, step, "VerticalSlider") 368 | 369 | def addNumEntry(self, label, zone, init, min, max, step): 370 | 371 | self.add_input(label, zone, init, min, max, step, "NumEntry") 372 | 373 | def addButton(self, label, zone): 374 | 375 | self.add_input(label, zone, 0, 0, 1, 1, "Button") 376 | 377 | def addToggleButton(self, label, zone): 378 | 379 | self.add_input(label, zone, 0, 0, 1, 1, "ToggleButton") 380 | 381 | def addCheckButton(self, label, zone): 382 | 383 | self.add_input(label, zone, 0, 0, 1, 1, "CheckButton") 384 | 385 | def addNumDisplay(self, label, zone, p): 386 | pass 387 | 388 | def addTextDisplay(self, label, zone, names, min, max): 389 | pass 390 | 391 | def addHorizontalBargraph(self, label, zone, min, max): 392 | pass 393 | 394 | def addVerticalBargraph(self, label, zone, min, max): 395 | pass 396 | --------------------------------------------------------------------------------