Optionally "expose" control values as input ports, so they can be connected to other nodes.

Also:
- Also add ControlValueDials for a-rate control ports (and make them exposeable).
- Fix exception in UI when removing nodes with connections.
- Fix exception in PluginHost when cleaning up some LV2 plugins.
- Dump audio engine opcode list to log.
looper
Ben Niemann 2019-03-24 05:30:10 +01:00
parent 901ed7e066
commit 6d52afb424
48 changed files with 709 additions and 91 deletions

View File

@ -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: ...

View File

@ -26,5 +26,7 @@ install_files(
butterhp.csnd
butterlp.csnd
delay.csnd
lfo-arate.csnd
lfo-krate.csnd
reverb.csnd
)

View File

@ -0,0 +1,55 @@
<?xml version="1.0"?>
<!--
@begin:license
Copyright (c) 2015-2019, Benjamin Niemann <pink@odahoda.de>
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
-->
<csound>
<display-name>LFO (a-rate)</display-name>
<ports>
<port name="out" type="aratecontrol" direction="output"/>
<port name="freq" type="kratecontrol" direction="input">
<float-control min="0" max="1000" default="1"/>
<display-name>Frequency</display-name>
</port>
<port name="amp" type="kratecontrol" direction="input">
<float-control min="0" max="1" default="1"/>
<display-name>Amplitude</display-name>
</port>
</ports>
<orchestra>
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
</orchestra>
<score>
i1 0 -1
</score>
</csound>

View File

@ -0,0 +1,55 @@
<?xml version="1.0"?>
<!--
@begin:license
Copyright (c) 2015-2019, Benjamin Niemann <pink@odahoda.de>
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
-->
<csound>
<display-name>LFO (k-rate)</display-name>
<ports>
<port name="out" type="kratecontrol" direction="output"/>
<port name="freq" type="kratecontrol" direction="input">
<float-control min="0" max="1000" default="1"/>
<display-name>Frequency</display-name>
</port>
<port name="amp" type="kratecontrol" direction="input">
<float-control min="0" max="1" default="1"/>
<display-name>Amplitude</display-name>
</port>
</ports>
<orchestra>
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
</orchestra>
<score>
i1 0 -1
</score>
</csound>

View File

@ -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,
)

View File

@ -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;
}
}

View File

@ -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')

View File

@ -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

View File

@ -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()

View File

@ -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:

View File

@ -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<OpArg>& 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<OpArg>& 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

View File

@ -52,6 +52,7 @@ enum OpCode {
// I/O
FETCH_CONTROL_VALUE,
FETCH_CONTROL_VALUE_TO_AUDIO,
POST_RMS,
// generators

View File

@ -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

View File

@ -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)
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)
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):

View File

@ -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) {

View File

@ -115,6 +115,7 @@ public:
Status setup();
void cleanup() override;
string dump() const;
void clear_programs();
void set_notification_callback(

View File

@ -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);

View File

@ -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()

View File

@ -21,6 +21,7 @@
*/
#include <stdarg.h>
#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<OpArg> args;

View File

@ -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; }

View File

@ -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 = {

View File

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

View File

@ -63,3 +63,6 @@ from .backend_settings_pb2 import (
from .host_parameters_pb2 import (
HostParameters,
)
from .node_port_properties_pb2 import (
NodePortProperties,
)

View File

@ -0,0 +1,30 @@
/*
* @begin:license
*
* Copyright (c) 2015-2019, Benjamin Niemann <pink@odahoda.de>
*
* 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;
}

View File

@ -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,

View File

@ -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

View File

@ -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

View File

@ -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,

View File

@ -0,0 +1,65 @@
#!/usr/bin/python3
# @begin:license
#
# Copyright (c) 2015-2019, Benjamin Niemann <pink@odahoda.de>
#
# 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)

View File

@ -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 {

View File

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

View File

@ -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 {

View File

@ -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:

View File

@ -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(

View File

@ -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;

View File

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

View File

@ -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

View File

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