diff --git a/3rdparty/typeshed/PyQt5/QtCore.pyi b/3rdparty/typeshed/PyQt5/QtCore.pyi index c87014ee..b53f4a0e 100644 --- a/3rdparty/typeshed/PyQt5/QtCore.pyi +++ b/3rdparty/typeshed/PyQt5/QtCore.pyi @@ -27,17 +27,20 @@ import sip import datetime # Support for new-style signals and slots. +class pyqtConnection: + pass + class pyqtSignal: def __init__(self, *types, name: str = ...) -> None: ... - def connect(self, slot: typing.Callable) -> None: ... - def disconnect(self, slot: typing.Optional[typing.Callable] = None) -> None: ... + def connect(self, slot: typing.Callable) -> pyqtConnection: ... + def disconnect(self, slot: typing.Optional[typing.Union[typing.Callable, pyqtConnection]] = None) -> None: ... def emit(self, *args: typing.Any) -> None: ... def __call__(self, *args: typing.Any) -> None: ... def __get__(self, instance: 'QObject', owner: typing.Type['QObject'] = None) -> 'pyqtBoundSignal': ... class pyqtBoundSignal: - def connect(self, slot: typing.Callable) -> None: ... - def disconnect(self, slot: typing.Optional[typing.Callable] = None) -> None: ... + def connect(self, slot: typing.Callable) -> pyqtConnection: ... + def disconnect(self, slot: typing.Optional[typing.Union[typing.Callable, pyqtConnection]] = None) -> None: ... def emit(self, *args: typing.Any) -> None: ... def __call__(self, *args: typing.Any) -> None: ... diff --git a/data/csound/CMakeLists.txt b/data/csound/CMakeLists.txt index d245376c..ed6e59a9 100644 --- a/data/csound/CMakeLists.txt +++ b/data/csound/CMakeLists.txt @@ -26,5 +26,7 @@ install_files( butterhp.csnd butterlp.csnd delay.csnd + lfo-arate.csnd + lfo-krate.csnd reverb.csnd ) diff --git a/data/csound/lfo-arate.csnd b/data/csound/lfo-arate.csnd new file mode 100644 index 00000000..637f1d2b --- /dev/null +++ b/data/csound/lfo-arate.csnd @@ -0,0 +1,55 @@ + + + + + +LFO (a-rate) + + + + + Frequency + + + + Amplitude + + + +0dbfs = 1.0 +ksmps = 32 +nchnls = 2 + +gaOut chnexport "out", 2 +gkFreq chnexport "freq", 1 +gkAmp chnexport "amp", 1 + +instr 1 + gaOut = lfo(gkAmp, gkFreq, 4) +endin + + + +i1 0 -1 + + diff --git a/data/csound/lfo-krate.csnd b/data/csound/lfo-krate.csnd new file mode 100644 index 00000000..950f52ce --- /dev/null +++ b/data/csound/lfo-krate.csnd @@ -0,0 +1,55 @@ + + + + + +LFO (k-rate) + + + + + Frequency + + + + Amplitude + + + +0dbfs = 1.0 +ksmps = 32 +nchnls = 2 + +gkOut chnexport "out", 2 +gkFreq chnexport "freq", 1 +gkAmp chnexport "amp", 1 + +instr 1 + gkOut = lfo(gkAmp, gkFreq, 4) +endin + + + +i1 0 -1 + + diff --git a/noisicaa/audioproc/__init__.py b/noisicaa/audioproc/__init__.py index d2324aee..c834bed6 100644 --- a/noisicaa/audioproc/__init__.py +++ b/noisicaa/audioproc/__init__.py @@ -28,6 +28,7 @@ from .audioproc_pb2 import ( DisconnectPorts, SetControlValue, SetPluginState, + SetNodePortProperties, ) from .audioproc_client import ( AbstractAudioProcClient, @@ -55,4 +56,5 @@ from .public import ( ProjectProperties, BackendSettings, HostParameters, + NodePortProperties, ) diff --git a/noisicaa/audioproc/audioproc.proto b/noisicaa/audioproc/audioproc.proto index 1185e550..4c013d51 100644 --- a/noisicaa/audioproc/audioproc.proto +++ b/noisicaa/audioproc/audioproc.proto @@ -27,6 +27,7 @@ import "noisicaa/node_db/node_description.proto"; import "noisicaa/audioproc/public/backend_settings.proto"; import "noisicaa/audioproc/public/control_value.proto"; import "noisicaa/audioproc/public/host_parameters.proto"; +import "noisicaa/audioproc/public/node_port_properties.proto"; import "noisicaa/audioproc/public/player_state.proto"; import "noisicaa/audioproc/public/plugin_state.proto"; import "noisicaa/audioproc/public/processor_message.proto"; @@ -71,6 +72,11 @@ message SetPluginState { required noisicaa.pb.PluginState state = 2; } +message SetNodePortProperties { + required string node_id = 1; + required noisicaa.pb.NodePortProperties port_properties = 2; +} + message Mutation { oneof type { AddNode add_node = 1; @@ -79,6 +85,7 @@ message Mutation { DisconnectPorts disconnect_ports = 4; SetControlValue set_control_value = 5; SetPluginState set_plugin_state = 6; + SetNodePortProperties set_node_port_properties = 7; } } diff --git a/noisicaa/audioproc/audioproc_client.py b/noisicaa/audioproc/audioproc_client.py index be30347d..0f314a91 100644 --- a/noisicaa/audioproc/audioproc_client.py +++ b/noisicaa/audioproc/audioproc_client.py @@ -131,6 +131,9 @@ class AbstractAudioProcClient(object): async def profile_audio_thread(self, duration: int) -> bytes: raise NotImplementedError + async def dump(self) -> None: + raise NotImplementedError + class AudioProcClient(AbstractAudioProcClient): def __init__(self, event_loop: asyncio.AbstractEventLoop, server: ipc.Server) -> None: @@ -353,3 +356,6 @@ class AudioProcClient(AbstractAudioProcClient): audioproc_pb2.UpdateProjectPropertiesRequest( realm=realm, properties=properties)) + + async def dump(self) -> None: + await self._stub.call('DUMP') diff --git a/noisicaa/audioproc/audioproc_process.py b/noisicaa/audioproc/audioproc_process.py index 89d3a654..2368a6b8 100644 --- a/noisicaa/audioproc/audioproc_process.py +++ b/noisicaa/audioproc/audioproc_process.py @@ -175,6 +175,9 @@ class AudioProcProcess(core.ProcessBase): self.__main_endpoint.add_handler( 'PROFILE_AUDIO_THREAD', self.__handle_profile_audio_thread, audioproc_pb2.ProfileAudioThreadRequest, audioproc_pb2.ProfileAudioThreadResponse) + self.__main_endpoint.add_handler( + 'DUMP', self.__handle_dump, + empty_message_pb2.EmptyMessage, empty_message_pb2.EmptyMessage) await self.server.add_endpoint(self.__main_endpoint) if self.shm_name is not None: @@ -341,6 +344,12 @@ class AudioProcProcess(core.ProcessBase): set_plugin_state.node_id, set_plugin_state.state) + elif mutation_type == 'set_node_port_properties': + set_node_port_properties = request.mutation.set_node_port_properties + node = graph.find_node(set_node_port_properties.node_id) + node.set_port_properties(set_node_port_properties.port_properties) + realm.update_spec() + else: raise ValueError(request.mutation) @@ -498,6 +507,17 @@ class AudioProcProcess(core.ProcessBase): response.svg = svg + async def __handle_dump( + self, + session: Session, + request: empty_message_pb2.EmptyMessage, + response: empty_message_pb2.EmptyMessage + ) -> None: + if self.__engine is not None: + logger.error("\n%s", self.__engine.dump()) + else: + logger.error("No engine.") + class AudioProcSubprocess(core.SubprocessMixin, AudioProcProcess): pass diff --git a/noisicaa/audioproc/engine/engine.pyx b/noisicaa/audioproc/engine/engine.pyx index 2db00bda..51d963c0 100644 --- a/noisicaa/audioproc/engine/engine.pyx +++ b/noisicaa/audioproc/engine/engine.pyx @@ -202,6 +202,12 @@ cdef class PyEngine(object): self.__engine_started = None + def dump(self): + out = "" + for _, realm in sorted(self.__realms.items()): + out += realm.dump() + return out + async def get_plugin_host(self): if self.__plugin_host is None: create_plugin_host_response = editor_main_pb2.CreateProcessResponse() diff --git a/noisicaa/audioproc/engine/graph.py b/noisicaa/audioproc/engine/graph.py index ff90c531..0cfa8055 100644 --- a/noisicaa/audioproc/engine/graph.py +++ b/noisicaa/audioproc/engine/graph.py @@ -30,6 +30,7 @@ from noisicaa import node_db from noisicaa import host_system as host_system_lib from noisicaa.core import ipc from noisicaa.core import session_data_pb2 +from noisicaa.audioproc.public import node_port_properties_pb2 from noisicaa.audioproc.public import processor_message_pb2 from . import control_value from . import processor as processor_lib @@ -239,6 +240,7 @@ class Node(object): self.outputs = {} # type: Dict[str, OutputPortMixin] self.__control_values = {} # type: Dict[str, control_value.PyControlValue] + self.__port_properties = {} # type: Dict[str, node_port_properties_pb2.NodePortProperties] if self.init_ports_from_description: self.init_ports() @@ -331,7 +333,7 @@ class Node(object): logger.info("%s: setup()", self.name) for port in self.ports: - if isinstance(port, KRateControlInputPort): + if isinstance(port, (KRateControlInputPort, ARateControlInputPort)): logger.info("Float control value '%s'", port.buf_name) cv = control_value.PyFloatControlValue( port.buf_name, port.description.float_value.default, 1) @@ -348,6 +350,16 @@ class Node(object): def set_session_value(self, key: str, value: session_data_pb2.SessionValue) -> None: pass + def get_port_properties(self, port_name: str) -> node_port_properties_pb2.NodePortProperties: + try: + return self.__port_properties[port_name] + except KeyError: + return node_port_properties_pb2.NodePortProperties(name=port_name) + + def set_port_properties( + self, port_properties: node_port_properties_pb2.NodePortProperties) -> None: + self.__port_properties[port_properties.name] = port_properties + @property def control_values(self) -> List[control_value.PyControlValue]: return [v for _, v in sorted(self.__control_values.items())] @@ -357,11 +369,21 @@ class Node(object): spec.append_control_value(cv) for port in self.ports: + port_properties = self.get_port_properties(port.name) + spec.append_buffer(port.buf_name, port.get_buf_type()) - if port.buf_name in self.__control_values: - spec.append_opcode( - 'FETCH_CONTROL_VALUE', self.__control_values[port.buf_name], port.buf_name) + if port.buf_name in self.__control_values and not port_properties.exposed: + if isinstance(port, KRateControlPortMixin): + spec.append_opcode( + 'FETCH_CONTROL_VALUE', + self.__control_values[port.buf_name], port.buf_name) + else: + assert isinstance(port, ARateControlPortMixin) + spec.append_opcode( + 'FETCH_CONTROL_VALUE_TO_AUDIO', + self.__control_values[port.buf_name], port.buf_name) + elif isinstance(port, InputPortMixin): spec.append_opcode('CLEAR', port.buf_name) for upstream_port in port.inputs: diff --git a/noisicaa/audioproc/engine/opcodes.cpp b/noisicaa/audioproc/engine/opcodes.cpp index 409496d6..0f25672e 100644 --- a/noisicaa/audioproc/engine/opcodes.cpp +++ b/noisicaa/audioproc/engine/opcodes.cpp @@ -105,6 +105,29 @@ Status run_FETCH_CONTROL_VALUE(BlockContext* ctxt, ProgramState* state, const ve } } +Status run_FETCH_CONTROL_VALUE_TO_AUDIO( + BlockContext* ctxt, ProgramState* state, const vector& args) { + int cv_idx = args[0].int_value(); + int buf_idx = args[1].int_value(); + ControlValue* cv = state->program->spec->get_control_value(cv_idx); + Buffer* buf = state->program->buffers[buf_idx].get(); + + switch (cv->type()) { + case ControlValueType::FloatCV: { + FloatControlValue* fcv = (FloatControlValue*)cv; + float* data = (float*)buf->data(); + for (uint32_t i = 0 ; i < state->host_system->block_size() ; ++i) { + *data++ = fcv->value(); + } + return Status::Ok(); + } + case ControlValueType::IntCV: + return ERROR_STATUS("IntControlValue not implemented yet."); + default: + return ERROR_STATUS("Invalid ControlValue type %d.", cv->type()); + } +} + Status run_POST_RMS(BlockContext* ctxt, ProgramState* state, const vector& args) { const string& node_id = args[0].string_value(); int port_index = args[1].int_value(); @@ -310,6 +333,7 @@ struct OpSpec opspecs[NUM_OPCODES] = { // I/O { OpCode::FETCH_CONTROL_VALUE, "FETCH_CONTROL_VALUE", "cb", nullptr, run_FETCH_CONTROL_VALUE }, + { OpCode::FETCH_CONTROL_VALUE_TO_AUDIO, "FETCH_CONTROL_VALUE_TO_AUDIO", "cb", nullptr, run_FETCH_CONTROL_VALUE_TO_AUDIO }, { OpCode::POST_RMS, "POST_RMS", "sib", nullptr, run_POST_RMS }, // generators diff --git a/noisicaa/audioproc/engine/opcodes.h b/noisicaa/audioproc/engine/opcodes.h index b5db9dbe..424f97cd 100644 --- a/noisicaa/audioproc/engine/opcodes.h +++ b/noisicaa/audioproc/engine/opcodes.h @@ -52,6 +52,7 @@ enum OpCode { // I/O FETCH_CONTROL_VALUE, + FETCH_CONTROL_VALUE_TO_AUDIO, POST_RMS, // generators diff --git a/noisicaa/audioproc/engine/opcodes.pxd b/noisicaa/audioproc/engine/opcodes.pxd index cf710996..36534dde 100644 --- a/noisicaa/audioproc/engine/opcodes.pxd +++ b/noisicaa/audioproc/engine/opcodes.pxd @@ -36,6 +36,7 @@ cdef extern from "noisicaa/audioproc/engine/opcodes.h" namespace "noisicaa" nogi MUL SET_FLOAT FETCH_CONTROL_VALUE + FETCH_CONTROL_VALUE_TO_AUDIO POST_RMS NOISE SINE diff --git a/noisicaa/audioproc/engine/plugin_host_process.py b/noisicaa/audioproc/engine/plugin_host_process.py index 74328788..37b06084 100644 --- a/noisicaa/audioproc/engine/plugin_host_process.py +++ b/noisicaa/audioproc/engine/plugin_host_process.py @@ -26,6 +26,7 @@ import functools import logging import os import threading +import traceback import typing from typing import Any, Dict, Tuple import uuid @@ -183,19 +184,23 @@ class PluginHost(plugin_host.PyPluginHost): self.__thread_result.set_result(True) async def __state_fetcher_main(self) -> None: - while True: - await asyncio.sleep(1.0, loop=self.__event_loop) - - state = self.get_state() - if state != self.__state: - self.__state = state - logger.info("Plugin state for %s changed:\n%s", self.__node_id, self.__state) - await asyncio.shield( - self.__callback_stub.call( - 'PLUGIN_STATE_CHANGE', - audioproc_pb2.PluginStateChange( - realm=self.__realm, node_id=self.__node_id, state=self.__state)), - loop=self.__event_loop) + try: + while True: + await asyncio.sleep(1.0, loop=self.__event_loop) + + state = self.get_state() + if state != self.__state: + self.__state = state + logger.info("Plugin state for %s changed:\n%s", self.__node_id, self.__state) + await asyncio.shield( + self.__callback_stub.call( + 'PLUGIN_STATE_CHANGE', + audioproc_pb2.PluginStateChange( + realm=self.__realm, node_id=self.__node_id, state=self.__state)), + loop=self.__event_loop) + + except: # pylint: disable=bare-except + logger.error("Exception in state fetcher:\n%s", traceback.format_exc()) class PluginHostProcess(core.ProcessBase): diff --git a/noisicaa/audioproc/engine/realm.cpp b/noisicaa/audioproc/engine/realm.cpp index 9544d08c..3c9301f4 100644 --- a/noisicaa/audioproc/engine/realm.cpp +++ b/noisicaa/audioproc/engine/realm.cpp @@ -178,6 +178,19 @@ void Realm::cleanup() { _block_context.reset(); } +string Realm::dump() const { + string out = sprintf("=== Realm %s:\n", _name.c_str()); + + Program* program = _current_program.load(); + if (program != nullptr) { + out += program->spec->dump(); + } else { + out += "No current program.\n"; + } + + return out; +} + void Realm::clear_programs() { Program* program = _next_program.exchange(nullptr); if (program != nullptr) { diff --git a/noisicaa/audioproc/engine/realm.h b/noisicaa/audioproc/engine/realm.h index 88bac4f0..1264aea9 100644 --- a/noisicaa/audioproc/engine/realm.h +++ b/noisicaa/audioproc/engine/realm.h @@ -115,6 +115,7 @@ public: Status setup(); void cleanup() override; + string dump() const; void clear_programs(); void set_notification_callback( diff --git a/noisicaa/audioproc/engine/realm.pxd b/noisicaa/audioproc/engine/realm.pxd index f22b8e2e..b4190c63 100644 --- a/noisicaa/audioproc/engine/realm.pxd +++ b/noisicaa/audioproc/engine/realm.pxd @@ -44,6 +44,7 @@ cdef extern from "noisicaa/audioproc/engine/realm.h" namespace "noisicaa" nogil: Status setup() void cleanup() + string dump() void clear_programs() void set_notification_callback( void (*callback)(void*, const string&), void* userdata); diff --git a/noisicaa/audioproc/engine/realm.pyx b/noisicaa/audioproc/engine/realm.pyx index d4e678a1..4cc7fc62 100644 --- a/noisicaa/audioproc/engine/realm.pyx +++ b/noisicaa/audioproc/engine/realm.pyx @@ -24,6 +24,7 @@ from cpython.ref cimport PyObject from cpython.exc cimport PyErr_Fetch, PyErr_Restore from libc.stdint cimport uint8_t, uint32_t from libc.string cimport memmove +from libcpp.string cimport string from noisicaa import core from noisicaa import audioproc @@ -147,6 +148,12 @@ cdef class PyRealm(object): logger.info("Realm '%s' cleaned up.", self.name) + def dump(self): + cdef string out + with nogil: + out = self.__realm.dump() + return out.decode('ascii') + def clear_programs(self): with nogil: self.__realm.clear_programs() diff --git a/noisicaa/audioproc/engine/spec.cpp b/noisicaa/audioproc/engine/spec.cpp index d320bb48..6b3be978 100644 --- a/noisicaa/audioproc/engine/spec.cpp +++ b/noisicaa/audioproc/engine/spec.cpp @@ -21,6 +21,7 @@ */ #include +#include "noisicaa/core/logging.h" #include "noisicaa/audioproc/engine/spec.h" #include "noisicaa/audioproc/engine/control_value.h" #include "noisicaa/audioproc/engine/processor.h" @@ -38,6 +39,55 @@ Spec::~Spec() { _buffer_map.clear(); } +string Spec::dump() const { + string out = ""; + + int i = 0; + for (const auto& opcode : _opcodes) { + const auto& opspec = opspecs[opcode.opcode]; + + string args = ""; + for (size_t a = 0 ; a < opcode.args.size() ; ++a) { + const auto& arg = opcode.args[a]; + + if (a > 0) { + args += ", "; + } + + switch (opspec.argspec[a]) { + case 'i': + args += sprintf("%ld", arg.int_value()); + break; + case 'b': { + args += sprintf("#BUF<%d>", arg.int_value()); + break; + } + case 'p': { + Processor* processor = _processors[arg.int_value()]; + args += sprintf("#PROC<%016lx>", processor->id()); + break; + } + case 'c': { + ControlValue* cv = _control_values[arg.int_value()]; + args += sprintf("#CV<%s>", cv->name().c_str()); + break; + } + case 'f': + args += sprintf("%f", arg.float_value()); + break; + case 's': + args += sprintf("\"%s\"", arg.string_value().c_str()); + break; + } + } + + out += sprintf("% 3d %s(%s)\n", i, opspec.name, args.c_str()); + ++i; + } + + return out; +} + Status Spec::append_opcode(OpCode opcode, ...) { vector args; diff --git a/noisicaa/audioproc/engine/spec.h b/noisicaa/audioproc/engine/spec.h index fda4fb66..4aa34fc5 100644 --- a/noisicaa/audioproc/engine/spec.h +++ b/noisicaa/audioproc/engine/spec.h @@ -37,6 +37,7 @@ namespace noisicaa { using namespace std; +class Logger; class Processor; class ControlValue; class BufferType; @@ -55,6 +56,8 @@ public: Spec(const Spec&) = delete; Spec operator=(const Spec&) = delete; + string dump() const; + void set_bpm(uint32_t bpm) { _bpm = bpm; } uint32_t bpm() const { return _bpm; } diff --git a/noisicaa/audioproc/engine/spec.pyx b/noisicaa/audioproc/engine/spec.pyx index 70250e4a..0af629e7 100644 --- a/noisicaa/audioproc/engine/spec.pyx +++ b/noisicaa/audioproc/engine/spec.pyx @@ -32,23 +32,24 @@ from .realm cimport PyRealm opcode_map = { - 'NOOP': OpCode.NOOP, - 'END': OpCode.END, - 'CALL_CHILD_REALM': OpCode.CALL_CHILD_REALM, - 'COPY': OpCode.COPY, - 'CLEAR': OpCode.CLEAR, - 'MIX': OpCode.MIX, - 'MUL': OpCode.MUL, - 'SET_FLOAT': OpCode.SET_FLOAT, - 'FETCH_CONTROL_VALUE': OpCode.FETCH_CONTROL_VALUE, - 'POST_RMS': OpCode.POST_RMS, - 'NOISE': OpCode.NOISE, - 'SINE': OpCode.SINE, - 'MIDI_MONKEY': OpCode.MIDI_MONKEY, - 'CONNECT_PORT': OpCode.CONNECT_PORT, - 'CALL': OpCode.CALL, - 'LOG_RMS': OpCode.LOG_RMS, - 'LOG_ATOM': OpCode.LOG_ATOM, + 'NOOP': OpCode.NOOP, + 'END': OpCode.END, + 'CALL_CHILD_REALM': OpCode.CALL_CHILD_REALM, + 'COPY': OpCode.COPY, + 'CLEAR': OpCode.CLEAR, + 'MIX': OpCode.MIX, + 'MUL': OpCode.MUL, + 'SET_FLOAT': OpCode.SET_FLOAT, + 'FETCH_CONTROL_VALUE': OpCode.FETCH_CONTROL_VALUE, + 'FETCH_CONTROL_VALUE_TO_AUDIO': OpCode.FETCH_CONTROL_VALUE_TO_AUDIO, + 'POST_RMS': OpCode.POST_RMS, + 'NOISE': OpCode.NOISE, + 'SINE': OpCode.SINE, + 'MIDI_MONKEY': OpCode.MIDI_MONKEY, + 'CONNECT_PORT': OpCode.CONNECT_PORT, + 'CALL': OpCode.CALL, + 'LOG_RMS': OpCode.LOG_RMS, + 'LOG_ATOM': OpCode.LOG_ATOM, } opname = { diff --git a/noisicaa/audioproc/public/CMakeLists.txt b/noisicaa/audioproc/public/CMakeLists.txt index 5b445795..1c8f8427 100644 --- a/noisicaa/audioproc/public/CMakeLists.txt +++ b/noisicaa/audioproc/public/CMakeLists.txt @@ -29,6 +29,7 @@ set(LIB_SRCS instrument_spec.pb.cc musical_time.cpp musical_time.pb.cc + node_port_properties.pb.cc player_state.pb.cc plugin_state.pb.cc processor_message.pb.cc @@ -63,6 +64,8 @@ cpp_proto(host_parameters.proto) py_proto(host_parameters.proto) cpp_proto(project_properties.proto) py_proto(project_properties.proto) +cpp_proto(node_port_properties.proto) +py_proto(node_port_properties.proto) add_library(noisicaa-audioproc-public SHARED ${LIB_SRCS}) target_compile_options(noisicaa-audioproc-public PRIVATE -fPIC -std=c++11 -Wall -Werror -pedantic -DHAVE_PTHREAD_SPIN_LOCK) diff --git a/noisicaa/audioproc/public/__init__.py b/noisicaa/audioproc/public/__init__.py index 8fd39423..8489ebd2 100644 --- a/noisicaa/audioproc/public/__init__.py +++ b/noisicaa/audioproc/public/__init__.py @@ -63,3 +63,6 @@ from .backend_settings_pb2 import ( from .host_parameters_pb2 import ( HostParameters, ) +from .node_port_properties_pb2 import ( + NodePortProperties, +) diff --git a/noisicaa/audioproc/public/node_port_properties.proto b/noisicaa/audioproc/public/node_port_properties.proto new file mode 100644 index 00000000..45a145ac --- /dev/null +++ b/noisicaa/audioproc/public/node_port_properties.proto @@ -0,0 +1,30 @@ +/* + * @begin:license + * + * Copyright (c) 2015-2019, Benjamin Niemann + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + * + * @end:license + */ + +syntax = "proto2"; + +package noisicaa.pb; + +message NodePortProperties { + optional string name = 1; + optional bool exposed = 2; +} diff --git a/noisicaa/builtin_nodes/custom_csound/node_description.py b/noisicaa/builtin_nodes/custom_csound/node_description.py index f519292b..eaf3e779 100644 --- a/noisicaa/builtin_nodes/custom_csound/node_description.py +++ b/noisicaa/builtin_nodes/custom_csound/node_description.py @@ -45,8 +45,8 @@ CustomCSoundDescription = node_db.NodeDescription( type=node_db.PortDescription.AUDIO, ), node_db.PortDescription( - name='ctrl', - display_name='ctrl', + name='kctrl', + display_name='ctrl (k-rate)', direction=node_db.PortDescription.INPUT, type=node_db.PortDescription.KRATE_CONTROL, float_value=node_db.FloatValueDescription( @@ -54,6 +54,16 @@ CustomCSoundDescription = node_db.NodeDescription( max=1.0, default=0.0), ), + node_db.PortDescription( + name='actrl', + display_name='ctrl (a-rate)', + direction=node_db.PortDescription.INPUT, + type=node_db.PortDescription.ARATE_CONTROL, + float_value=node_db.FloatValueDescription( + min=0.0, + max=1.0, + default=0.0), + ), node_db.PortDescription( name='ev', direction=node_db.PortDescription.INPUT, diff --git a/noisicaa/builtin_nodes/custom_csound/processor_test.py b/noisicaa/builtin_nodes/custom_csound/processor_test.py index ae113974..90c80c84 100644 --- a/noisicaa/builtin_nodes/custom_csound/processor_test.py +++ b/noisicaa/builtin_nodes/custom_csound/processor_test.py @@ -49,7 +49,8 @@ class ProcessorCustomCSoundTest( audio_l_in = self.buffer_mgr.allocate('in:left', buffers.PyFloatAudioBlockBuffer()) audio_r_in = self.buffer_mgr.allocate('in:right', buffers.PyFloatAudioBlockBuffer()) - ctrl = self.buffer_mgr.allocate('ctrl', buffers.PyFloatControlValueBuffer()) + kctrl = self.buffer_mgr.allocate('kctrl', buffers.PyFloatControlValueBuffer()) + actrl = self.buffer_mgr.allocate('actrl', buffers.PyFloatAudioBlockBuffer()) self.buffer_mgr.allocate('ev', buffers.PyAtomDataBuffer()) audio_l_out = self.buffer_mgr.allocate('out:left', buffers.PyFloatAudioBlockBuffer()) audio_r_out = self.buffer_mgr.allocate('out:right', buffers.PyFloatAudioBlockBuffer()) @@ -59,20 +60,22 @@ class ProcessorCustomCSoundTest( forge.set_buffer(self.buffer_mgr.data('ev'), 10240) with forge.sequence(): pass - ctrl[0] = 0.0 + kctrl[0] = 0.0 for i in range(self.host_system.block_size): audio_l_in[i] = 0.0 audio_r_in[i] = 0.0 audio_l_out[i] = 0.0 audio_r_out[i] = 0.0 + actrl[i] = 0.0 proc.connect_port(self.ctxt, 0, self.buffer_mgr.data('in:left')) proc.connect_port(self.ctxt, 1, self.buffer_mgr.data('in:right')) - proc.connect_port(self.ctxt, 2, self.buffer_mgr.data('ctrl')) - proc.connect_port(self.ctxt, 3, self.buffer_mgr.data('ev')) - proc.connect_port(self.ctxt, 4, self.buffer_mgr.data('out:left')) - proc.connect_port(self.ctxt, 5, self.buffer_mgr.data('out:right')) + proc.connect_port(self.ctxt, 2, self.buffer_mgr.data('kctrl')) + proc.connect_port(self.ctxt, 3, self.buffer_mgr.data('actrl')) + proc.connect_port(self.ctxt, 4, self.buffer_mgr.data('ev')) + proc.connect_port(self.ctxt, 5, self.buffer_mgr.data('out:left')) + proc.connect_port(self.ctxt, 6, self.buffer_mgr.data('out:right')) return proc @@ -110,8 +113,8 @@ class ProcessorCustomCSoundTest( def test_filter(self): orchestra = textwrap.dedent('''\ instr 1 - gaOutLeft = gkCtrl * gaInLeft - gaOutRight = gkCtrl * gaInRight + gaOutLeft = gkKctrl * gaInLeft + gaOutRight = gkKctrl * gaInRight endin ''') score = textwrap.dedent('''\ @@ -125,7 +128,7 @@ class ProcessorCustomCSoundTest( for i in range(self.host_system.block_size): audio_l_in[i] = 1.0 audio_r_in[i] = 1.0 - self.buffer_mgr['ctrl'][0] = 0.5 + self.buffer_mgr['kctrl'][0] = 0.5 proc.process_block(self.ctxt, None) # TODO: pass time_mapper diff --git a/noisicaa/model/CMakeLists.txt b/noisicaa/model/CMakeLists.txt index cffd973a..d63c9614 100644 --- a/noisicaa/model/CMakeLists.txt +++ b/noisicaa/model/CMakeLists.txt @@ -26,6 +26,7 @@ add_python_package( control_value.py key_signature.py key_signature_test.py + node_port_properties.py pos2f.py pitch.py pitch_test.py diff --git a/noisicaa/model/__init__.py b/noisicaa/model/__init__.py index 227db541..5abd5317 100644 --- a/noisicaa/model/__init__.py +++ b/noisicaa/model/__init__.py @@ -46,6 +46,7 @@ from .pos2f import Pos2F from .sizef import SizeF from .color import Color from .control_value import ControlValue +from .node_port_properties import NodePortProperties from .project import ( ObjectBase, ProjectChild, diff --git a/noisicaa/model/node_port_properties.py b/noisicaa/model/node_port_properties.py new file mode 100644 index 00000000..b1e36ea7 --- /dev/null +++ b/noisicaa/model/node_port_properties.py @@ -0,0 +1,65 @@ +#!/usr/bin/python3 + +# @begin:license +# +# Copyright (c) 2015-2019, Benjamin Niemann +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +# +# @end:license + +from google.protobuf import message as protobuf + +from noisicaa import audioproc +from . import model_base + + +class NodePortProperties(model_base.ProtoValue): + def __init__(self, name: str, *, exposed: bool = False) -> None: + self.__name = name + self.__exposed = exposed + + def __str__(self) -> str: + return '<%s exposed=%s>' % (self.__name, self.__exposed) + __repr__ = __str__ + + def to_proto(self) -> audioproc.NodePortProperties: + return audioproc.NodePortProperties( + name=self.__name, + exposed=self.__exposed) + + @classmethod + def from_proto(cls, pb: protobuf.Message) -> 'NodePortProperties': + if not isinstance(pb, audioproc.NodePortProperties): + raise TypeError(type(pb).__name__) + return NodePortProperties( + name=pb.name, + exposed=pb.exposed) + + @property + def name(self) -> str: + return self.__name + + @property + def exposed(self) -> bool: + return self.__exposed + + def __eq__(self, other: object) -> bool: + if not isinstance(other, NodePortProperties): + return False + + return ( + self.__name == other.__name + and self.__exposed == other.__exposed) diff --git a/noisicaa/model/project.proto b/noisicaa/model/project.proto index d90d3503..4e80bd9b 100644 --- a/noisicaa/model/project.proto +++ b/noisicaa/model/project.proto @@ -26,6 +26,7 @@ package noisicaa.pb; import "noisicaa/core/proto_types.proto"; import "noisicaa/audioproc/public/control_value.proto"; +import "noisicaa/audioproc/public/node_port_properties.proto"; import "noisicaa/audioproc/public/plugin_state.proto"; import "noisicaa/model/model_base.proto"; @@ -54,6 +55,7 @@ message BaseNode { optional Color graph_color = 6; repeated ControlValue control_values = 3; optional PluginState plugin_state = 4; + repeated NodePortProperties port_properties = 7; } message Node { diff --git a/noisicaa/model/project.py b/noisicaa/model/project.py index 58a7c4d8..a40b2c0e 100644 --- a/noisicaa/model/project.py +++ b/noisicaa/model/project.py @@ -32,6 +32,7 @@ from . import pos2f from . import sizef from . import color from . import control_value +from . import node_port_properties from . import model_base from . import project_pb2 @@ -84,7 +85,6 @@ class ControlValueMap(object): self.__initialized = False self.__control_values = {} # type: Dict[str, control_value.ControlValue] - self.__control_value_listeners = [] # type: List[core.Listener] self.__control_values_listener = None # type: core.Listener self.control_value_changed = core.CallbackMap[str, model_base.PropertyValueChange]() @@ -101,7 +101,8 @@ class ControlValueMap(object): for port in self.__node.description.ports: if (port.direction == node_db.PortDescription.INPUT - and port.type == node_db.PortDescription.KRATE_CONTROL): + and port.type in (node_db.PortDescription.KRATE_CONTROL, + node_db.PortDescription.ARATE_CONTROL)): self.__control_values[port.name] = control_value.ControlValue( name=port.name, value=port.float_value.default, generation=1) @@ -158,6 +159,8 @@ class BaseNode(ProjectChild): color.Color, default=color.Color(0.8, 0.8, 0.8, 1.0)) control_values = model_base.WrappedProtoListProperty(control_value.ControlValue) plugin_state = model_base.ProtoProperty(audioproc.PluginState, allow_none=True) + port_properties = model_base.WrappedProtoListProperty( + node_port_properties.NodePortProperties) def __init__(self, **kwargs: Any) -> None: super().__init__(**kwargs) @@ -170,6 +173,8 @@ class BaseNode(ProjectChild): core.Callback[model_base.PropertyListChange[control_value.ControlValue]]() self.plugin_state_changed = \ core.Callback[model_base.PropertyChange[audioproc.PluginState]]() + self.port_properties_changed = \ + core.Callback[model_base.PropertyListChange[node_port_properties.NodePortProperties]]() self.control_value_map = ControlValueMap(self) @@ -177,6 +182,17 @@ class BaseNode(ProjectChild): def control_values(self) -> Sequence[control_value.ControlValue]: return self.get_property_value('control_values') + @property + def port_properties(self) -> Sequence[node_port_properties.NodePortProperties]: + return self.get_property_value('port_properties') + + def get_port_properties(self, port_name: str) -> node_port_properties.NodePortProperties: + for np in self.port_properties: + if np.name == port_name: + return np + + return node_port_properties.NodePortProperties(port_name) + @property def pipeline_node_id(self) -> str: return '%016x' % self.id @@ -189,6 +205,15 @@ class BaseNode(ProjectChild): def description(self) -> node_db.NodeDescription: raise NotImplementedError + @property + def connections(self) -> Sequence['NodeConnection']: + result = [] + for conn in self.project.get_property_value('node_connections'): + if conn.source_node is self or conn.dest_node is self: + result.append(conn) + + return result + def upstream_nodes(self) -> List['BaseNode']: node_ids = set() # type: Set[int] self.__upstream_nodes(node_ids) diff --git a/noisicaa/music/commands.proto b/noisicaa/music/commands.proto index 1640034d..3d94b291 100644 --- a/noisicaa/music/commands.proto +++ b/noisicaa/music/commands.proto @@ -25,6 +25,7 @@ syntax = "proto2"; import "noisicaa/core/empty_message.proto"; import "noisicaa/core/proto_types.proto"; import "noisicaa/audioproc/public/control_value.proto"; +import "noisicaa/audioproc/public/node_port_properties.proto"; import "noisicaa/audioproc/public/plugin_state.proto"; import "noisicaa/model/model_base.proto"; import "noisicaa/model/project.proto"; @@ -55,6 +56,7 @@ message UpdateNode { optional ControlValue set_control_value = 6; optional PluginState set_plugin_state = 7; optional bytes load_from_preset = 8; + optional NodePortProperties set_port_properties = 9; } message DeleteNode { diff --git a/noisicaa/music/graph.py b/noisicaa/music/graph.py index 5c5bf644..a7e5a09f 100644 --- a/noisicaa/music/graph.py +++ b/noisicaa/music/graph.py @@ -113,6 +113,20 @@ class DeleteNodeConnection(commands.Command): class UpdateNode(commands.Command): proto_type = 'update_node' + def validate(self) -> None: + pb = down_cast(commands_pb2.UpdateNode, self.pb) + + if pb.node_id not in self.pool: + raise ValueError("Unknown node %016x" % pb.node_id) + + node = down_cast(pmodel.BaseNode, self.pool[pb.node_id]) + + if pb.HasField('set_port_properties'): + if not any( + port_desc.name == pb.set_port_properties.name + for port_desc in node.description.ports): + raise ValueError("Invalid port name '%s'" % pb.set_port_properties.name) + def run(self) -> None: pb = down_cast(commands_pb2.UpdateNode, self.pb) node = down_cast(pmodel.BaseNode, self.pool[pb.node_id]) @@ -138,6 +152,9 @@ class UpdateNode(commands.Command): pb.set_control_value.value, pb.set_control_value.generation) + if pb.HasField('set_port_properties'): + node.set_port_properties(pb.set_port_properties) + # class NodeToPreset(commands.Command): # proto_type = 'node_to_preset' @@ -196,9 +213,16 @@ class BaseNode(pmodel.BaseNode): # pylint: disable=abstract-method remove_node=audioproc.RemoveNode(id=self.pipeline_node_id)) def get_initial_parameter_mutations(self) -> Iterator[audioproc.Mutation]: + for props in self.port_properties: + yield audioproc.Mutation( + set_node_port_properties=audioproc.SetNodePortProperties( + node_id=self.pipeline_node_id, + port_properties=props.to_proto())) + for port in self.description.ports: if (port.direction == node_db.PortDescription.INPUT - and port.type == node_db.PortDescription.KRATE_CONTROL): + and (port.type == node_db.PortDescription.KRATE_CONTROL, + port.type == node_db.PortDescription.ARATE_CONTROL)): for cv in self.control_values: if cv.name == port.name: yield audioproc.Mutation( @@ -237,6 +261,27 @@ class BaseNode(pmodel.BaseNode): # pylint: disable=abstract-method node_id=self.pipeline_node_id, state=plugin_state))) + def set_port_properties(self, port_properties: audioproc.NodePortProperties) -> None: + new_props = None # type: audioproc.NodePortProperties + + for idx, props in enumerate(self.port_properties): + if props.name == port_properties.name: + new_props = props.to_proto() + new_props.MergeFrom(port_properties) + self.port_properties[idx] = model.NodePortProperties.from_proto(new_props) + break + else: + new_props = port_properties + self.port_properties.append( + model.NodePortProperties.from_proto(port_properties)) + + if self.attached_to_project: + self.project.handle_pipeline_mutation( + audioproc.Mutation( + set_node_port_properties=audioproc.SetNodePortProperties( + node_id=self.pipeline_node_id, + port_properties=new_props))) + def create_node_connector( self, message_cb: Callable[[audioproc.ProcessorMessage], None] ) -> node_connector.NodeConnector: diff --git a/noisicaa/music/graph_test.py b/noisicaa/music/graph_test.py index b0b8287b..7fba6833 100644 --- a/noisicaa/music/graph_test.py +++ b/noisicaa/music/graph_test.py @@ -24,6 +24,7 @@ import logging from typing import List from noisidev import unittest +from noisicaa.core import ipc from noisicaa import audioproc from noisicaa import model from . import commands_test @@ -132,6 +133,27 @@ class GraphCommandsTest(commands_test.CommandsTestMixin, unittest.AsyncTestCase) set_plugin_state=plugin_state)) self.assertEqual(node.plugin_state, plugin_state) + async def test_set_port_properties(self): + await self.client.send_command(project_client.create_node( + 'builtin://csound/reverb', + graph_pos=model.Pos2F(200, 100))) + node = self.project.nodes[-1] + + await self.client.send_command(project_client.update_node( + node, + set_port_properties=model.NodePortProperties('mix', exposed=True))) + self.assertTrue(node.get_port_properties('mix').exposed) + + await self.client.send_command(project_client.update_node( + node, + set_port_properties=model.NodePortProperties('mix', exposed=False))) + self.assertFalse(node.get_port_properties('mix').exposed) + + with self.assertRaises(ipc.RemoteException): + await self.client.send_command(project_client.update_node( + node, + set_port_properties=model.NodePortProperties('holla'))) + # @unittest.skip("Implementation broken") # async def test_node_to_preset(self): # await self.client.send_command(commands_pb2.Command( diff --git a/noisicaa/music/mutations.proto b/noisicaa/music/mutations.proto index 8f410ef9..229bc6ec 100644 --- a/noisicaa/music/mutations.proto +++ b/noisicaa/music/mutations.proto @@ -25,6 +25,7 @@ syntax = "proto2"; import "noisicaa/core/proto_types.proto"; import "noisicaa/audioproc/public/musical_time.proto"; import "noisicaa/audioproc/public/control_value.proto"; +import "noisicaa/audioproc/public/node_port_properties.proto"; import "noisicaa/audioproc/public/plugin_state.proto"; import "noisicaa/model/model_base.proto"; import "noisicaa/model/project.proto"; @@ -103,6 +104,7 @@ message MutationList { SizeF sizef = 109; Color color = 110; ControlValue control_value = 108; + NodePortProperties node_port_properties = 111; } } repeated Slot slots = 2; diff --git a/noisicaa/music/mutations.py b/noisicaa/music/mutations.py index 18cd7899..f117a4c1 100644 --- a/noisicaa/music/mutations.py +++ b/noisicaa/music/mutations.py @@ -89,6 +89,8 @@ class MutationList(object): return model.Color.from_proto(slot.color) elif vtype == 'control_value': return model.ControlValue.from_proto(slot.control_value) + elif vtype == 'node_port_properties': + return model.NodePortProperties.from_proto(slot.node_port_properties) else: raise TypeError(vtype) @@ -312,6 +314,8 @@ class MutationCollector(object): slot.color.CopyFrom(value.to_proto()) elif isinstance(value, model.ControlValue): slot.control_value.CopyFrom(value.to_proto()) + elif isinstance(value, model.NodePortProperties): + slot.node_port_properties.CopyFrom(value.to_proto()) else: raise TypeError(type(value)) diff --git a/noisicaa/music/pmodel.py b/noisicaa/music/pmodel.py index 17d5c7dc..d0c0b2df 100644 --- a/noisicaa/music/pmodel.py +++ b/noisicaa/music/pmodel.py @@ -94,6 +94,10 @@ class BaseNode(ProjectChild, model.BaseNode, ObjectBase): def plugin_state(self, value: audioproc.PluginState) -> None: self.set_property_value('plugin_state', value) + @property + def port_properties(self) -> MutableSequence[model.NodePortProperties]: + return self.get_property_value('port_properties') + def get_add_mutations(self) -> Iterator[audioproc.Mutation]: raise NotImplementedError @@ -109,6 +113,9 @@ class BaseNode(ProjectChild, model.BaseNode, ObjectBase): def set_plugin_state(self, plugin_state: audioproc.PluginState) -> None: raise NotImplementedError + def set_port_properties(self, port_properties: audioproc.NodePortProperties) -> None: + raise NotImplementedError + def create_node_connector( self, message_cb: Callable[[audioproc.ProcessorMessage], None]) -> NodeConnector: raise NotImplementedError diff --git a/noisicaa/music/project.py b/noisicaa/music/project.py index 5e0b6cb5..1c67b797 100644 --- a/noisicaa/music/project.py +++ b/noisicaa/music/project.py @@ -127,6 +127,7 @@ class BaseProject(pmodel.Project): self.dispatch_command_sequence(commands.CommandSequence.create(proto)) def dispatch_command_sequence(self, sequence: commands.CommandSequence) -> None: + logger.info("Executing command sequence:\n%s", sequence) sequence.apply(self.command_registry, self._pool) logger.info( "Executed command sequence %s (%d operations)", @@ -299,12 +300,12 @@ class Project(BaseProject): if self.__storage.logs_since_last_checkpoint > 1000: self.create_checkpoint() - def dispatch_command_sequence(self, sequence: commands.CommandSequence) -> Any: + def dispatch_command_sequence(self, sequence: commands.CommandSequence) -> None: if self.closed: raise RuntimeError( "Command sequence %s executed on closed project." % sequence.command_names) - result = super().dispatch_command_sequence(sequence) + super().dispatch_command_sequence(sequence) if not sequence.is_noop: if (self.__latest_command_sequence is None @@ -314,8 +315,6 @@ class Project(BaseProject): self.__latest_command_sequence = sequence self.__latest_command_time = time.time() - return result - def undo(self) -> None: if self.closed: raise RuntimeError("Undo executed on closed project.") diff --git a/noisicaa/music/project_client.py b/noisicaa/music/project_client.py index 724e8cde..c1ea843c 100644 --- a/noisicaa/music/project_client.py +++ b/noisicaa/music/project_client.py @@ -90,6 +90,7 @@ def update_node( set_graph_color: model.Color = None, set_control_value: model.ControlValue = None, set_plugin_state: audioproc.PluginState = None, + set_port_properties: model.NodePortProperties = None, ) -> commands_pb2.Command: return commands_pb2.Command( command='update_node', @@ -101,7 +102,9 @@ def update_node( set_graph_color=set_graph_color.to_proto() if set_graph_color is not None else None, set_control_value=( set_control_value.to_proto() if set_control_value is not None else None), - set_plugin_state=set_plugin_state)) + set_plugin_state=set_plugin_state, + set_port_properties=( + set_port_properties.to_proto() if set_port_properties is not None else None))) def delete_node( @@ -297,6 +300,7 @@ class ProjectClient(object): request: mutations_pb2.MutationList, response: empty_message_pb2.EmptyMessage ) -> None: + logger.debug("Received project mutations:\n%s", request) mutation_list = mutations_lib.MutationList(self.__pool, request) mutation_list.apply_forward() diff --git a/noisicaa/music/project_client_model.py b/noisicaa/music/project_client_model.py index 968a2079..727e89e9 100644 --- a/noisicaa/music/project_client_model.py +++ b/noisicaa/music/project_client_model.py @@ -20,7 +20,7 @@ # # @end:license -from typing import Any, Sequence +from typing import cast, Any, List, Sequence from noisicaa.core.typing_extra import down_cast from noisicaa import audioproc @@ -59,6 +59,9 @@ class BaseNode(ProjectChild, model.BaseNode, ObjectBase): # pylint: disable=abs def plugin_state(self) -> audioproc.PluginState: return self.get_property_value('plugin_state') + @property + def connections(self) -> Sequence['NodeConnection']: + return cast(List['NodeConnection'], super().connections) class Node(BaseNode, model.Node, ObjectBase): diff --git a/noisicaa/music/project_process.py b/noisicaa/music/project_process.py index f8d3e2bc..afd78312 100644 --- a/noisicaa/music/project_process.py +++ b/noisicaa/music/project_process.py @@ -25,6 +25,7 @@ import copy import logging import os import os.path +import traceback import typing from typing import Any, Type, Dict, Iterable, TypeVar @@ -85,10 +86,12 @@ class Session(ipc.CallbackSessionMixin, ipc.Session): del self.__players[player.id] async def clear_players(self) -> None: - for player in self.__players.values(): - await player.cleanup() + players = list(self.__players.values()) self.__players.clear() + for player in players: + await player.cleanup() + async def publish_mutations(self, mutations: mutations_pb2.MutationList) -> None: assert self.callback_alive @@ -109,8 +112,11 @@ class Session(ipc.CallbackSessionMixin, ipc.Session): if os.path.isfile(checkpoint_path): checkpoint = session_data_pb2.SessionDataCheckpoint() with open(checkpoint_path, 'rb') as fp: - success = checkpoint.ParseFromString(fp.read()) - assert success + checkpoint_serialized = fp.read() + + # mypy thinks that ParseFromString has no return value. bug in the stubs? + bytes_parsed = checkpoint.ParseFromString(checkpoint_serialized) # type: ignore + assert bytes_parsed == len(checkpoint_serialized) for session_value in checkpoint.session_values: self.session_data[session_value.name] = session_value @@ -395,7 +401,9 @@ class ProjectProcess(core.ProcessBase): except commands.ClientError: raise except Exception: - logger.exception("Exception while handling command sequence\n%s", request) + logger.error( + "Exception while handling command sequence\n%s\n%s", + request, traceback.format_exc()) self.start_shutdown() raise ipc.CloseConnection diff --git a/noisicaa/node_db/private/csound_scanner.py b/noisicaa/node_db/private/csound_scanner.py index 16d65499..7c5695a4 100644 --- a/noisicaa/node_db/private/csound_scanner.py +++ b/noisicaa/node_db/private/csound_scanner.py @@ -100,7 +100,8 @@ class CSoundScanner(scanner.Scanner): port_desc.csound_instr = csound_elem.get('instr') if (port_desc.direction == node_db.PortDescription.INPUT - and port_desc.type == node_db.PortDescription.KRATE_CONTROL): + and port_desc.type in (node_db.PortDescription.KRATE_CONTROL, + node_db.PortDescription.ARATE_CONTROL)): float_control_elem = port_elem.find('float-control') if float_control_elem is not None: value_desc = port_desc.float_value diff --git a/noisicaa/ui/control_value_dial.py b/noisicaa/ui/control_value_dial.py index 0e949e16..14d27444 100644 --- a/noisicaa/ui/control_value_dial.py +++ b/noisicaa/ui/control_value_dial.py @@ -109,8 +109,21 @@ class ControlValueDial(slots.SlotContainer, QtWidgets.QWidget): painter.translate(self.width() / 2, self.height() / 2) + if self.isEnabled(): + arc_bg_color = QtGui.QColor(0, 0, 0) + arc_fg_color = QtGui.QColor(100, 100, 255) + knob_inner_color = QtGui.QColor(0, 0, 0) + knob_border_color = QtGui.QColor(255, 255, 255) + text_color = QtGui.QColor(0, 0, 0) + else: + arc_bg_color = QtGui.QColor(80, 80, 80) + arc_fg_color = QtGui.QColor(120, 120, 120) + knob_inner_color = QtGui.QColor(80, 80, 80) + knob_border_color = QtGui.QColor(140, 140, 140) + text_color = QtGui.QColor(80, 80, 80) + pen = QtGui.QPen() - pen.setColor(Qt.black) + pen.setColor(arc_bg_color) pen.setWidth(arc_width) pen.setCapStyle(Qt.RoundCap) painter.setPen(pen) @@ -120,7 +133,7 @@ class ControlValueDial(slots.SlotContainer, QtWidgets.QWidget): zero_value = self.normalize(0.0) pen = QtGui.QPen() - pen.setColor(QtGui.QColor(100, 100, 255)) + pen.setColor(arc_fg_color) pen.setWidth(arc_width - 2) pen.setCapStyle(Qt.RoundCap) painter.setPen(pen) @@ -132,9 +145,9 @@ class ControlValueDial(slots.SlotContainer, QtWidgets.QWidget): 0.5 * arc_size * math.cos(1.5 * math.pi * value - 1.25 * math.pi), 0.5 * arc_size * math.sin(1.5 * math.pi * value - 1.25 * math.pi)) painter.setPen(Qt.NoPen) - painter.setBrush(QtGui.QColor(255, 255, 255)) + painter.setBrush(knob_border_color) painter.drawEllipse(knob_pos, arc_width / 2 + 1, arc_width / 2 + 1) - painter.setBrush(Qt.black) + painter.setBrush(knob_inner_color) painter.drawEllipse(knob_pos, arc_width / 2 - 1, arc_width / 2 - 1) if size > 40: @@ -142,7 +155,7 @@ class ControlValueDial(slots.SlotContainer, QtWidgets.QWidget): font.setPixelSize(10) painter.setFont(font) pen = QtGui.QPen() - pen.setColor(Qt.black) + pen.setColor(text_color) painter.setPen(pen) painter.drawText( QtCore.QRectF(-arc_size / 2, -arc_size / 4, arc_size, arc_size / 2), diff --git a/noisicaa/ui/editor_app.py b/noisicaa/ui/editor_app.py index 239efa31..aa3bfb9d 100644 --- a/noisicaa/ui/editor_app.py +++ b/noisicaa/ui/editor_app.py @@ -108,6 +108,7 @@ class EditorApp(ui_base.AbstractEditorApp): self.show_edit_areas_action = None # type: QtWidgets.QAction self.__audio_thread_profiler = None # type: audio_thread_profiler.AudioThreadProfiler self.profile_audio_thread_action = None # type: QtWidgets.QAction + self.dump_audioproc = None # type: QtWidgets.QAction self.audioproc_client = None # type: audioproc.AbstractAudioProcClient self.audioproc_process = None # type: str self.node_db = None # type: node_db.NodeDBClient @@ -155,6 +156,9 @@ class EditorApp(ui_base.AbstractEditorApp): self.profile_audio_thread_action = QtWidgets.QAction("Profile Audio Thread", self.qt_app) self.profile_audio_thread_action.triggered.connect(self.onProfileAudioThread) + self.dump_audioproc = QtWidgets.QAction("Dump AudioProc", self.qt_app) + self.dump_audioproc.triggered.connect(self.onDumpAudioProc) + await self.createAudioProcProcess() self.default_style = self.qt_app.style().objectName() @@ -328,6 +332,9 @@ class EditorApp(ui_base.AbstractEditorApp): self.__audio_thread_profiler.raise_() self.__audio_thread_profiler.activateWindow() + def onDumpAudioProc(self) -> None: + self.process.event_loop.create_task(self.audioproc_client.dump()) + def __handleEngineNotification(self, msg: audioproc.EngineNotification) -> None: for node_message_pb in msg.node_messages: msg_atom = node_message_pb.atom diff --git a/noisicaa/ui/editor_window.py b/noisicaa/ui/editor_window.py index 28de9ffa..07e61234 100644 --- a/noisicaa/ui/editor_window.py +++ b/noisicaa/ui/editor_window.py @@ -335,6 +335,7 @@ class EditorWindow(ui_base.AbstractEditorWindow): self._dev_menu.addAction(self._show_pipeline_perf_monitor_action) self._dev_menu.addAction(self._show_stat_monitor_action) self._dev_menu.addAction(self.app.profile_audio_thread_action) + self._dev_menu.addAction(self.app.dump_audioproc) menu_bar.addSeparator() diff --git a/noisicaa/ui/graph/base_node.py b/noisicaa/ui/graph/base_node.py index 7f650484..13136ce8 100644 --- a/noisicaa/ui/graph/base_node.py +++ b/noisicaa/ui/graph/base_node.py @@ -153,7 +153,7 @@ class SelectColorWidget(QtWidgets.QWidget): class NodeProps(QtCore.QObject): contentRectChanged = QtCore.pyqtSignal(QtCore.QRectF) - canvasRectChanged = QtCore.pyqtSignal(QtCore.QRectF) + canvasLayoutChanged = QtCore.pyqtSignal() class Title(QtWidgets.QGraphicsSimpleTextItem): @@ -418,10 +418,6 @@ class Node(ui_base.ProjectMixin, QtWidgets.QGraphicsItem): self.__out_ports = [] # type: List[node_db.PortDescription] for port_desc in self.__node.description.ports: - if (port_desc.direction == node_db.PortDescription.INPUT - and port_desc.type == node_db.PortDescription.KRATE_CONTROL): - continue - port = Port(port_desc, self) self.__ports[port_desc.name] = port @@ -481,6 +477,8 @@ class Node(ui_base.ProjectMixin, QtWidgets.QGraphicsItem): self.__node.graph_size_changed.add(self.__graphRectChanged)) self.__listeners.append( self.__node.graph_color_changed.add(lambda *_: self.__updateState())) + self.__listeners.append( + self.__node.port_properties_changed.add(lambda *_: self.__layout())) self.__state = None # type: audioproc.NodeStateChange.State @@ -500,7 +498,6 @@ class Node(ui_base.ProjectMixin, QtWidgets.QGraphicsItem): self.__canvas_rect = self.__transform.mapRect(self.contentRect()) self.__layout() self.props.contentRectChanged.emit(self.contentRect()) - self.props.canvasRectChanged.emit(self.canvasRect()) def createBodyWidget(self) -> QtWidgets.QWidget: return None @@ -584,12 +581,10 @@ class Node(ui_base.ProjectMixin, QtWidgets.QGraphicsItem): def setCanvasTopLeft(self, pos: QtCore.QPointF) -> None: self.__canvas_rect.moveTopLeft(pos) self.__layout() - self.props.canvasRectChanged.emit(self.__canvas_rect) def setCanvasRect(self, rect: QtCore.QRectF) -> None: self.__canvas_rect = rect self.__layout() - self.props.canvasRectChanged.emit(self.__canvas_rect) def canvasRect(self) -> QtCore.QRectF: return self.__canvas_rect @@ -598,7 +593,6 @@ class Node(ui_base.ProjectMixin, QtWidgets.QGraphicsItem): self.__transform = transform self.__canvas_rect = self.__transform.mapRect(self.contentRect()) self.__layout() - self.props.canvasRectChanged.emit(self.__canvas_rect) def resizeSide(self, pos: QtCore.QPointF) -> Optional[str]: t = self.__canvas_rect.top() @@ -679,11 +673,24 @@ class Node(ui_base.ProjectMixin, QtWidgets.QGraphicsItem): path.addRoundedRect(0, 0, w, h, 5, 5) self.__box.setPath(path) - show_ports = (0.5 * h > 10 * max(len(self.__in_ports), len(self.__out_ports))) - for idx, desc in enumerate(self.__in_ports): + visible_in_ports = [] + for desc in self.__in_ports: + port_properties = self.__node.get_port_properties(desc.name) + if (desc.direction == node_db.PortDescription.INPUT + and desc.type in (node_db.PortDescription.KRATE_CONTROL, + node_db.PortDescription.ARATE_CONTROL) + and not port_properties.exposed): + port = self.__ports[desc.name] + port.setVisible(False) + continue + + visible_in_ports.append(desc) + + show_ports = (0.5 * h > 10 * max(len(visible_in_ports), len(self.__out_ports))) + for idx, desc in enumerate(visible_in_ports): port = self.__ports[desc.name] - if len(self.__in_ports) > 1: - y = h * (0.5 * idx / (len(self.__in_ports) - 1) + 0.25) + if len(visible_in_ports) > 1: + y = h * (0.5 * idx / (len(visible_in_ports) - 1) + 0.25) else: y = h * 0.5 port.setPos(0, y) @@ -766,6 +773,8 @@ class Node(ui_base.ProjectMixin, QtWidgets.QGraphicsItem): self.__drag_rect = QtCore.QRectF(0, 0, drag_rect_width, drag_rect_height) + self.props.canvasLayoutChanged.emit() + def paint( self, painter: QtGui.QPainter, option: QtWidgets.QStyleOptionGraphicsItem, widget: Optional[QtWidgets.QWidget] = None) -> None: @@ -798,7 +807,11 @@ class Node(ui_base.ProjectMixin, QtWidgets.QGraphicsItem): color_menu.addAction(color_action) def onRemove(self) -> None: - self.send_command_async(music.delete_node(self.__node)) + commands = [] + for conn in self.__node.connections: + commands.append(music.delete_node_connection(conn)) + commands.append(music.delete_node(self.__node)) + self.send_commands_async(*commands) def onSetColor(self, color: model.Color) -> None: if color != self.__node.graph_color: @@ -838,13 +851,23 @@ class Connection(ui_base.ProjectMixin, QtWidgets.QGraphicsPathItem): self.__highlighted = False - self.__src_node.props.canvasRectChanged.connect(lambda _: self.__update()) - self.__dest_node.props.canvasRectChanged.connect(lambda _: self.__update()) + self.__src_node_canvas_layout_changed_connection = \ + self.__src_node.props.canvasLayoutChanged.connect(self.__update) + self.__dest_node_canvas_layout_changed_connection = \ + self.__dest_node.props.canvasLayoutChanged.connect(self.__update) self.__update() def cleanup(self) -> None: - pass + if self.__src_node_canvas_layout_changed_connection is not None: + self.__src_node.props.canvasLayoutChanged.disconnect( + self.__src_node_canvas_layout_changed_connection) + self.__src_node_canvas_layout_changed_connection = None + + if self.__dest_node_canvas_layout_changed_connection is not None: + self.__dest_node.props.canvasLayoutChanged.disconnect( + self.__dest_node_canvas_layout_changed_connection) + self.__dest_node_canvas_layout_changed_connection = None def connection(self) -> music.NodeConnection: return self.__connection diff --git a/noisicaa/ui/graph/generic_node.py b/noisicaa/ui/graph/generic_node.py index 3b14081e..7171f5f7 100644 --- a/noisicaa/ui/graph/generic_node.py +++ b/noisicaa/ui/graph/generic_node.py @@ -27,6 +27,7 @@ from PyQt5.QtCore import Qt from PyQt5 import QtWidgets from noisicaa import core +from noisicaa import model from noisicaa import music from noisicaa import node_db from noisicaa.ui import ui_base @@ -49,26 +50,72 @@ class ControlValueWidget(control_value_connector.ControlValueConnector): self.__node = node self.__port = port - dial = control_value_dial.ControlValueDial(parent) - dial.setRange(port.float_value.min, port.float_value.max) - dial.setDefault(port.float_value.default) - self.connect(dial.valueChanged, dial.setValue) + self.__port_properties_listener = self.__node.port_properties_changed.add( + self.__portPropertiesChanged) + + port_properties = self.__node.get_port_properties(self.__port.name) + + self.__dial = control_value_dial.ControlValueDial(parent) + self.__dial.setDisabled(port_properties.exposed) + self.__dial.setRange(port.float_value.min, port.float_value.max) + self.__dial.setDefault(port.float_value.default) + self.connect(self.__dial.valueChanged, self.__dial.setValue) + + self.__exposed = QtWidgets.QCheckBox(parent) + self.__exposed.setChecked(port_properties.exposed) + self.__exposed.toggled.connect(self.__exposedEdited) self.__widget = QtWidgets.QWidget(parent) layout = QtWidgets.QHBoxLayout() layout.setSpacing(0) layout.setContentsMargins(0, 0, 0, 0) - layout.addWidget(dial) + layout.addWidget(self.__exposed) + layout.addWidget(self.__dial) layout.addStretch(1) self.__widget.setLayout(layout) + def cleanup(self) -> None: + if self.__port_properties_listener is not None: + self.__port_properties_listener.remove() + self.__port_properties_listener = None + + super().cleanup() + def label(self) -> str: return self.__port.display_name + ":" def widget(self) -> QtWidgets.QWidget: return self.__widget + def __exposedEdited(self, exposed: bool) -> None: + port_properties = self.__node.get_port_properties(self.__port.name) + if port_properties.exposed == exposed: + return + + commands = [] # type: List[music.Command] + + if not exposed: + for conn in self.__node.connections: + if conn.dest_port == self.__port.name or conn.source_port == self.__port.name: + commands.append(music.delete_node_connection(conn)) + + port_properties = model.NodePortProperties( + name=self.__port.name, + exposed=exposed) + commands.append(music.update_node( + self.__node, + set_port_properties=port_properties)) + + self.send_commands_async(*commands) + + self.__dial.setDisabled(exposed) + + def __portPropertiesChanged(self, change: model.PropertyListChange) -> None: + port_properties = self.__node.get_port_properties(self.__port.name) + self.__exposed.setChecked(port_properties.exposed) + self.__dial.setDisabled(port_properties.exposed) + class GenericNodeWidget(ui_base.ProjectMixin, QtWidgets.QWidget): def __init__(self, node: music.BaseNode, **kwargs: Any) -> None: @@ -108,7 +155,8 @@ class GenericNodeWidget(ui_base.ProjectMixin, QtWidgets.QWidget): for port in self.__node.description.ports: if (port.direction == node_db.PortDescription.INPUT - and port.type == node_db.PortDescription.KRATE_CONTROL): + and port.type in (node_db.PortDescription.KRATE_CONTROL, + node_db.PortDescription.ARATE_CONTROL)): widget = ControlValueWidget( node=self.__node, port=port, diff --git a/noisicaa/ui/ui_base.py b/noisicaa/ui/ui_base.py index 04d4626a..8372c545 100644 --- a/noisicaa/ui/ui_base.py +++ b/noisicaa/ui/ui_base.py @@ -255,6 +255,7 @@ class AbstractEditorApp(object): runtime_settings = None # type: runtime_settings_lib.RuntimeSettings show_edit_areas_action = None # type: QtWidgets.QAction profile_audio_thread_action = None # type: QtWidgets.QAction + dump_audioproc = None # type: QtWidgets.QAction node_db = None # type: node_db_lib.NodeDBClient instrument_db = None # type: instrument_db_lib.InstrumentDBClient urid_mapper = None # type: lv2.ProxyURIDMapper