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