From a99eabcad9d9925196618d01349f290cf46ef379 Mon Sep 17 00:00:00 2001 From: Ben Niemann Date: Sun, 10 Jul 2016 20:22:18 +0200 Subject: [PATCH] Basics of libcsound binding. --- bin/noisicaä | 1 + bin/noisipg | 1 + bin/runtests | 118 +----------------------------- bin/runtests.py | 118 ++++++++++++++++++++++++++++++ noisicaa/audioproc/csound.pyx | 110 ++++++++++++++++++++++++++++ noisicaa/audioproc/csound.pyxbld | 14 ++++ noisicaa/audioproc/csound_test.py | 22 ++++++ 7 files changed, 269 insertions(+), 115 deletions(-) create mode 100755 bin/runtests.py create mode 100644 noisicaa/audioproc/csound.pyx create mode 100644 noisicaa/audioproc/csound.pyxbld create mode 100644 noisicaa/audioproc/csound_test.py diff --git a/bin/noisicaä b/bin/noisicaä index 5c4086b0..926e1cbb 100755 --- a/bin/noisicaä +++ b/bin/noisicaä @@ -2,4 +2,5 @@ LIBDIR=$(readlink -f "$(dirname "$0")/..") export PYTHONPATH="$LIBDIR:$PYTHONPATH" +export LD_LIBRARY_PATH=${VIRTUAL_ENV}/lib exec python3 -m noisicaa.editor_main "$@" diff --git a/bin/noisipg b/bin/noisipg index 2f2c2b86..cff40944 100755 --- a/bin/noisipg +++ b/bin/noisipg @@ -2,4 +2,5 @@ LIBDIR=$(readlink -f "$(dirname "$0")/..") export PYTHONPATH="$LIBDIR:$PYTHONPATH" +export LD_LIBRARY_PATH=${VIRTUAL_ENV}/lib exec python3 -m noisicaa.audio_playground "$@" diff --git a/bin/runtests b/bin/runtests index dba472a3..7cf0d088 100755 --- a/bin/runtests +++ b/bin/runtests @@ -1,116 +1,4 @@ -#!/usr/bin/env python3 +#!/bin/bash -import argparse -import fnmatch -import logging -import os -import os.path -import sys -import unittest - -import coverage - -import pyximport -pyximport.install() - -LIBDIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) -sys.path.insert(0, LIBDIR) - -def main(argv): - parser = argparse.ArgumentParser() - parser.add_argument('selectors', type=str, nargs='*') - parser.add_argument('--debug', action='store_true', default=False) - parser.add_argument( - '--log-level', - choices=['debug', 'info', 'warning', 'error', 'critical'], - default='error', - help="Minimum level for log messages written to STDERR.") - parser.add_argument('--nocoverage', action='store_true', default=False) - args = parser.parse_args(argv[1:]) - - logging.basicConfig() - logging.getLogger().setLevel({ - 'debug': logging.DEBUG, - 'info': logging.INFO, - 'warning': logging.WARNING, - 'error': logging.ERROR, - 'critical': logging.CRITICAL, - }[args.log_level if not args.debug else 'debug']) - - loader = unittest.defaultTestLoader - suite = unittest.TestSuite() - - if not args.nocoverage: - cov = coverage.Coverage( - source=['noisicaa'], - omit='*_*test.py', - config_file=False) - cov.set_option("run:branch", True) - cov.start() - - for dirpath, dirnames, filenames in os.walk( - os.path.join(LIBDIR, 'noisicaa')): - if '__pycache__' in dirnames: - dirnames.remove('__pycache__') - - for filename in filenames: - if not fnmatch.fnmatch(filename, '*.py'): - continue - - modpath = os.path.join(dirpath, filename) - assert modpath.startswith(LIBDIR + '/') - modpath = modpath[len(LIBDIR)+1:-3] - modname = modpath.replace('/', '.') - logging.info("Loading module %s...", modname) - __import__(modname) - - if not fnmatch.fnmatch(filename, '*_*test.py'): - continue - - if args.selectors: - matched = False - for selector in args.selectors: - if modname.startswith(selector): - matched = True - if not matched: - continue - - modsuite = loader.loadTestsFromName(modname) - suite.addTest(modsuite) - - runner = unittest.TextTestRunner(verbosity=2) - result = runner.run(suite) - - if not args.nocoverage: - cov.stop() - cov_data = cov.get_data() - total_coverage = cov.html_report( - directory='/tmp/noisicaä.coverage') - - file_coverages = [] - for path in sorted(cov_data.measured_files()): - _, statements, _, missing, _ = cov.analysis2(path) - try: - file_coverage = 1.0 - 1.0 * len(missing) / len(statements) - except ZeroDivisionError: - file_coverage = 1.0 - file_coverages.append( - (os.path.relpath( - path, os.path.abspath(os.path.dirname(__file__))), - file_coverage)) - file_coverages = sorted(file_coverages, key=lambda f: f[1]) - file_coverages = filter(lambda f: f[1] < 0.8, file_coverages) - file_coverages = list(file_coverages) - - print() - print("Total coverage: %.1f%%" % total_coverage) - for path, file_coverage in file_coverages[:5]: - print("% 3.1f%% %s" % (100 * file_coverage, path)) - print("Coverage report: file:///tmp/noisicaä.coverage/index.html") - print() - - return 0 if result.wasSuccessful() else 1 - - -if __name__ == '__main__': - sys.exit(main(sys.argv)) +export LD_LIBRARY_PATH=${VIRTUAL_ENV}/lib +exec python3 $(readlink -f "$(dirname "$0")/runtests.py") "$@" diff --git a/bin/runtests.py b/bin/runtests.py new file mode 100755 index 00000000..9a5d04bc --- /dev/null +++ b/bin/runtests.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python3 + +import argparse +import fnmatch +import logging +import os +import os.path +import sys +import unittest + +import coverage + +import pyximport +pyximport.install() + +LIBDIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) +sys.path.insert(0, LIBDIR) + +os.environ['LD_LIBRARY_PATH'] = os.path.join(os.getenv('VIRTUAL_ENV'), 'lib') + +def main(argv): + parser = argparse.ArgumentParser() + parser.add_argument('selectors', type=str, nargs='*') + parser.add_argument('--debug', action='store_true', default=False) + parser.add_argument( + '--log-level', + choices=['debug', 'info', 'warning', 'error', 'critical'], + default='error', + help="Minimum level for log messages written to STDERR.") + parser.add_argument('--nocoverage', action='store_true', default=False) + args = parser.parse_args(argv[1:]) + + logging.basicConfig() + logging.getLogger().setLevel({ + 'debug': logging.DEBUG, + 'info': logging.INFO, + 'warning': logging.WARNING, + 'error': logging.ERROR, + 'critical': logging.CRITICAL, + }[args.log_level if not args.debug else 'debug']) + + loader = unittest.defaultTestLoader + suite = unittest.TestSuite() + + if not args.nocoverage: + cov = coverage.Coverage( + source=['noisicaa'], + omit='*_*test.py', + config_file=False) + cov.set_option("run:branch", True) + cov.start() + + for dirpath, dirnames, filenames in os.walk( + os.path.join(LIBDIR, 'noisicaa')): + if '__pycache__' in dirnames: + dirnames.remove('__pycache__') + + for filename in filenames: + if not fnmatch.fnmatch(filename, '*.py'): + continue + + modpath = os.path.join(dirpath, filename) + assert modpath.startswith(LIBDIR + '/') + modpath = modpath[len(LIBDIR)+1:-3] + modname = modpath.replace('/', '.') + logging.info("Loading module %s...", modname) + __import__(modname) + + if not fnmatch.fnmatch(filename, '*_*test.py'): + continue + + if args.selectors: + matched = False + for selector in args.selectors: + if modname.startswith(selector): + matched = True + if not matched: + continue + + modsuite = loader.loadTestsFromName(modname) + suite.addTest(modsuite) + + runner = unittest.TextTestRunner(verbosity=2) + result = runner.run(suite) + + if not args.nocoverage: + cov.stop() + cov_data = cov.get_data() + total_coverage = cov.html_report( + directory='/tmp/noisicaä.coverage') + + file_coverages = [] + for path in sorted(cov_data.measured_files()): + _, statements, _, missing, _ = cov.analysis2(path) + try: + file_coverage = 1.0 - 1.0 * len(missing) / len(statements) + except ZeroDivisionError: + file_coverage = 1.0 + file_coverages.append( + (os.path.relpath( + path, os.path.abspath(os.path.dirname(__file__))), + file_coverage)) + file_coverages = sorted(file_coverages, key=lambda f: f[1]) + file_coverages = filter(lambda f: f[1] < 0.8, file_coverages) + file_coverages = list(file_coverages) + + print() + print("Total coverage: %.1f%%" % total_coverage) + for path, file_coverage in file_coverages[:5]: + print("% 3.1f%% %s" % (100 * file_coverage, path)) + print("Coverage report: file:///tmp/noisicaä.coverage/index.html") + print() + + return 0 if result.wasSuccessful() else 1 + + +if __name__ == '__main__': + sys.exit(main(sys.argv)) diff --git a/noisicaa/audioproc/csound.pyx b/noisicaa/audioproc/csound.pyx new file mode 100644 index 00000000..e20eff6e --- /dev/null +++ b/noisicaa/audioproc/csound.pyx @@ -0,0 +1,110 @@ +import logging +import os + +from libc cimport stdio +from libc cimport string + +### DECLARATIONS ########################################################## + +cdef extern from "stdarg.h" nogil: + ctypedef int va_list + +cdef extern from "stdio.h" nogil: + int vsnprintf(char *str, size_t size, const char *format, va_list ap) + +cdef extern from "csound/csound.h" nogil: + ctypedef int CSOUND; + + int csoundGetVersion() + void csoundSetDefaultMessageCallback( + void (*csoundMessageCallback_)(CSOUND*, + int attr, + const char* format, + va_list)) + + void csoundInitialize(int) + void* csoundCreate(void_p) + void csoundDestroy(void*) + + +### LOGGING ############################################################### + +# standard message +CSOUNDMSG_DEFAULT = 0x0000 +# error message (initerror, perferror, etc.) +CSOUNDMSG_ERROR = 0x1000 +# orchestra opcodes (e.g. printks) +CSOUNDMSG_ORCH = 0x2000 +# for progress display and heartbeat characters +CSOUNDMSG_REALTIME = 0x3000 +# warning messages +CSOUNDMSG_WARNING = 0x4000 + +CSOUNDMSG_TYPE_MASK = 0x7000 + +_logger = logging.getLogger('csound') +_logbuf = bytearray() + +cdef void _message_callback( + CSOUND* csnd, int attr, const char* fmt, va_list args) nogil: + cdef char buf[10240] + cdef int l + l = vsnprintf(buf, sizeof(buf), fmt, args) + + with gil: + try: + _logbuf.extend(buf[:l]) + + while True: + eol = _logbuf.find(b'\n') + if eol == -1: + break + line = _logbuf[0:eol] + del _logbuf[0:eol+1] + + line = line.decode('utf-8', 'replace') + line = line.expandtabs(tabsize=8) + + if attr & CSOUNDMSG_TYPE_MASK == CSOUNDMSG_ERROR: + _logger.error('%s', line) + elif attr & CSOUNDMSG_TYPE_MASK == CSOUNDMSG_WARNING: + _logger.warning('%s', line) + else: + _logger.info('%s', line) + except Exception as exc: + _logger.exception("Exception in csound message callback:") + os._exit(1) + +csoundSetDefaultMessageCallback(_message_callback) + + +### GLOBAL INIT ########################################################### + +CSOUNDINIT_NO_SIGNAL_HANDLER = 1 +CSOUNDINIT_NO_ATEXIT = 2 + +csoundInitialize(CSOUNDINIT_NO_SIGNAL_HANDLER | CSOUNDINIT_NO_ATEXIT) + + +### CLIENT CODE ########################################################### + +__version__ = '%d.%02d.%d' % ( + csoundGetVersion() // 1000, + (csoundGetVersion() // 10) % 100, + csoundGetVersion() % 10) + + +cdef class CSound(object): + cdef void* csnd + + def __cinit__(self): + self.csnd = csoundCreate(None) + + def __dealloc__(self): + if self.csnd != NULL: + self.close() + + def close(self): + assert self.csnd != NULL + csoundDestroy(self.csnd) + self.csnd = NULL diff --git a/noisicaa/audioproc/csound.pyxbld b/noisicaa/audioproc/csound.pyxbld new file mode 100644 index 00000000..9289991a --- /dev/null +++ b/noisicaa/audioproc/csound.pyxbld @@ -0,0 +1,14 @@ +# -*- mode: python -*- + +import os +import os.path + +def make_ext(modname, pyxfilename): + from distutils.extension import Extension + return Extension( + name = modname, + sources=[pyxfilename], + libraries=['csound64'], + library_dirs=[os.path.join(os.getenv('VIRTUAL_ENV'), 'lib')], + include_dirs=[os.path.join(os.getenv('VIRTUAL_ENV'), 'include')], + ) diff --git a/noisicaa/audioproc/csound_test.py b/noisicaa/audioproc/csound_test.py new file mode 100644 index 00000000..4757c1e9 --- /dev/null +++ b/noisicaa/audioproc/csound_test.py @@ -0,0 +1,22 @@ +#!/usr/bin/python3 + +import unittest + +if __name__ == '__main__': + import pyximport + pyximport.install() + +from . import csound + + +class CSoundTest(unittest.TestCase): + def test_version(self): + self.assertEqual(csound.__version__, '6.07.0') + + def test_constructor(self): + csnd = csound.CSound() + + csnd.close() + +if __name__ == '__main__': + unittest.main()