Compare commits

...

22 Commits
main ... fixes

Author SHA1 Message Date
Ben Niemann f6e814fa85 Apparently fixes some use-after-free bug, though I don't understand why... 2 years ago
Ben Niemann 3212ec7520 Better spec dump. 2 years ago
Ben Niemann d42dbc2dd0 Fix some lint. 2 years ago
Ben Niemann a3c4a4f96c Show engine state and load in settings dialog. 2 years ago
Ben Niemann d02e7c678a Separate engine state and load into separate notification messages. 2 years ago
Ben Niemann dc799a83fe Move ownership of the EngineState to the EditorApp. 2 years ago
Ben Niemann 4a19708c5f Fix playback of test sample. 2 years ago
Ben Niemann 08979cc189 Track label could be shown, even if track is hidden. 2 years ago
Ben Niemann b1948dff9f Handle more errors gracefully during project setup. 2 years ago
Ben Niemann d73e407c0b Emit pipeline mutations for nodes and connections from change callbacks. 2 years ago
Ben Niemann 5938df561c Call './waf build' when reloading noisicaä with F5. 2 years ago
Ben Niemann 6dc0947636 When noisicaä crashed, don't reopen the project(s) on next start. 2 years ago
Ben Niemann d71bd64534 Demote "noteoff failed" message, which is WAI. 2 years ago
Ben Niemann 89e720d1de Rerender samples when BPM changes. 2 years ago
Ben Niemann 2948947301 Fix corrupting of device list when MIDI devices are unplugged/plugged in. 2 years ago
Ben Niemann 76ef47e15c Fix truncated unittest report files. 2 years ago
Ben Niemann f7c6525db4 Dialog to append N measures or fill track to end. 2 years ago
Ben Niemann f7fb775787 Fix crash when BPM is changed during playback of sample tracks. 2 years ago
Ben Niemann 9cde07e2da Update size of TrackLabel when track gets renamed. 2 years ago
Ben Niemann 3798c90215 Rename nodes via context menu. 2 years ago
Ben Niemann 0bc5b0da1a Fix crackle when using the mixer dials. 2 years ago
Ben Niemann 717554860c Fix control dials in mixer node. 2 years ago
  1. 6
      3rdparty/typeshed/PyQt5/QtWidgets.pyi
  2. 1
      bin/noisicaä
  3. 1
      noisicaa/audioproc/__init__.py
  4. 4
      noisicaa/audioproc/audioproc_client.py
  5. 12
      noisicaa/audioproc/audioproc_process.py
  6. 17
      noisicaa/audioproc/engine/control_value.cpp
  7. 4
      noisicaa/audioproc/engine/control_value.h
  8. 3
      noisicaa/audioproc/engine/engine.cpp
  9. 2
      noisicaa/audioproc/engine/fluidsynth_util.cpp
  10. 1
      noisicaa/audioproc/engine/graph.py
  11. 13
      noisicaa/audioproc/engine/processor_sound_file.cpp
  12. 2
      noisicaa/audioproc/engine/realm.cpp
  13. 129
      noisicaa/audioproc/engine/spec.cpp
  14. 3
      noisicaa/audioproc/engine/spec.h
  15. 1
      noisicaa/audioproc/public/__init__.py
  16. 16
      noisicaa/audioproc/public/engine_notification.proto
  17. 26
      noisicaa/audioproc/public/time_mapper.cpp
  18. 14
      noisicaa/audioproc/public/time_mapper.h
  19. 5
      noisicaa/audioproc/public/time_mapper.pxd
  20. 36
      noisicaa/audioproc/public/time_mapper.pyx
  21. 4
      noisicaa/builtin_nodes/mixer/node_ui.py
  22. 24
      noisicaa/builtin_nodes/mixer/processor.cpp
  23. 6
      noisicaa/builtin_nodes/sample_track/processor.cpp
  24. 1
      noisicaa/builtin_nodes/sample_track/processor.h
  25. 15
      noisicaa/builtin_nodes/sample_track/track_ui.py
  26. 3
      noisicaa/constants.py
  27. 7
      noisicaa/editor_main.py
  28. 4
      noisicaa/instrument_db/private/db_test.py
  29. 36
      noisicaa/music/project.py
  30. 5
      noisicaa/ui/device_list.py
  31. 167
      noisicaa/ui/device_list_test.py
  32. 53
      noisicaa/ui/editor_app.py
  33. 79
      noisicaa/ui/editor_window.py
  34. 4
      noisicaa/ui/engine_state.py
  35. 9
      noisicaa/ui/graph/base_node.py
  36. 20
      noisicaa/ui/project_view.py
  37. 59
      noisicaa/ui/qtmisc.py
  38. 56
      noisicaa/ui/settings_dialog.py
  39. 15
      noisicaa/ui/track_list/editor.py
  40. 79
      noisicaa/ui/track_list/measured_track_editor.py
  41. 2
      noisicaa/ui/ui_base.py
  42. 2
      noisicaa/ui/wscript
  43. 9
      noisidev/test_runner.py

6
3rdparty/typeshed/PyQt5/QtWidgets.pyi vendored

@ -2715,6 +2715,8 @@ class QDial(QAbstractSlider):
class QDialogButtonBox(QWidget):
accepted = ... # type: PYQT_SIGNAL
rejected = ... # type: PYQT_SIGNAL
class StandardButton(int): ...
NoButton = ... # type: 'QDialogButtonBox.StandardButton'
@ -2780,10 +2782,10 @@ class QDialogButtonBox(QWidget):
def event(self, event: QtCore.QEvent) -> bool: ...
def changeEvent(self, event: QtCore.QEvent) -> None: ...
def rejected(self) -> None: ...
#def rejected(self) -> None: ...
def helpRequested(self) -> None: ...
def clicked(self, button: QAbstractButton) -> None: ...
def accepted(self) -> None: ...
#def accepted(self) -> None: ...
def centerButtons(self) -> bool: ...
def setCenterButtons(self, center: bool) -> None: ...
def button(self, which: 'QDialogButtonBox.StandardButton') -> QPushButton: ...

1
bin/noisicaä

@ -42,6 +42,7 @@ fi
(cd $ROOTDIR && ./waf build)
export NOISICAA_SRC_ROOT="${ROOTDIR}"
export NOISICAA_INSTALL_ROOT="${LIBDIR}"
export NOISICAA_DATA_DIR="${LIBDIR}/data"
export PYTHONPATH="$LIBDIR:$PYTHONPATH"

1
noisicaa/audioproc/__init__.py

@ -39,6 +39,7 @@ from .audioproc_client import (
from .public import (
NodeStateChange,
EngineStateChange,
EngineLoad,
EngineNotification,
MusicalDuration,
MusicalTime,

4
noisicaa/audioproc/audioproc_client.py

@ -155,6 +155,7 @@ class AudioProcClient(AbstractAudioProcClient):
self.engine_notifications = core.Callback[engine_notification_pb2.EngineNotification]()
self.engine_state_changed = core.Callback[engine_notification_pb2.EngineStateChange]()
self.engine_load_changed = core.Callback[engine_notification_pb2.EngineLoad]()
self.player_state_changed = core.CallbackMap[str, player_state_pb2.PlayerState]()
self.node_state_changed = core.CallbackMap[str, engine_notification_pb2.NodeStateChange]()
self.node_messages = core.CallbackMap[str, Dict[str, Any]]()
@ -222,6 +223,9 @@ class AudioProcClient(AbstractAudioProcClient):
for engine_state_change in request.engine_state_changes:
self.engine_state_changed.call(engine_state_change)
for engine_load in request.engine_load:
self.engine_load_changed.call(engine_load)
if request.HasField('perf_stats'):
perf_stats = core.PerfStats()
perf_stats.deserialize(request.perf_stats)

12
noisicaa/audioproc/audioproc_process.py

@ -444,20 +444,22 @@ class AudioProcProcess(core.ProcessBase):
sink.inputs['in:right'].connect(node.outputs['out:right'], node_db.PortDescription.AUDIO)
realm.update_spec()
sound_file_complete_urid = self.__urid_mapper.map(
"http://noisicaa.odahoda.de/lv2/processor_sound_file#complete")
sound_file_complete_uri = 'http://noisicaa.odahoda.de/lv2/processor_sound_file#complete'
complete = asyncio.Event(loop=self.event_loop)
def handle_notification(notification: engine_notification_pb2.EngineNotification) -> None:
for node_message in notification.node_messages:
if node_message.node_id == node.id:
msg = lv2.wrap_atom(self.__urid_mapper, node_message.atom)
if msg.type_urid == sound_file_complete_urid:
msg = lv2.wrap_atom(self.__urid_mapper, node_message.atom).as_object
if msg.get(sound_file_complete_uri, False):
complete.set()
listener = self.__engine.notifications.add(handle_notification)
await complete.wait()
try:
await asyncio.wait_for(complete.wait(), timeout=10.0, loop=self.event_loop)
except asyncio.TimeoutError:
pass
listener.remove()
sink.inputs['in:left'].disconnect(node.outputs['out:left'])

17
noisicaa/audioproc/engine/control_value.cpp

@ -20,6 +20,7 @@
* @end:license
*/
#include "noisicaa/audioproc/engine/misc.h"
#include "noisicaa/audioproc/engine/control_value.h"
namespace noisicaa {
@ -31,12 +32,28 @@ ControlValue::ControlValue(ControlValueType type, const string& name, uint32_t g
ControlValue::~ControlValue() {}
const char* ControlValue::type_name() const {
switch (_type) {
case ControlValueType::FloatCV: return "FLOAT";
case ControlValueType::IntCV: return "INT";
default: return "??";
}
}
FloatControlValue::FloatControlValue(const string& name, float value, uint32_t generation)
: ControlValue(ControlValueType::FloatCV, name, generation),
_value(value) {}
string FloatControlValue::formatted_value() const {
return sprintf("%f", _value);
}
IntControlValue::IntControlValue(const string& name, int64_t value, uint32_t generation)
: ControlValue(ControlValueType::IntCV, name, generation),
_value(value) {}
string IntControlValue::formatted_value() const {
return sprintf("%ld", _value);
}
} // namespace noisicaa

4
noisicaa/audioproc/engine/control_value.h

@ -42,8 +42,10 @@ public:
virtual ~ControlValue();
ControlValueType type() const { return _type; }
const char* type_name() const;
const string& name() const { return _name; }
uint32_t generation() const { return _generation; }
virtual string formatted_value() const = 0;
protected:
ControlValue(ControlValueType type, const string& name, uint32_t generation);
@ -60,6 +62,7 @@ class FloatControlValue : public ControlValue {
public:
FloatControlValue(const string& name, float value, uint32_t generation);
string formatted_value() const;
float value() const { return _value; }
void set_value(float value, uint32_t generation) {
_value = value;
@ -74,6 +77,7 @@ class IntControlValue : public ControlValue {
public:
IntControlValue(const string& name, int64_t value, uint32_t generation);
string formatted_value() const;
int64_t value() const { return _value; }
void set_value(int64_t value, uint32_t generation) {
_value = value;

3
noisicaa/audioproc/engine/engine.cpp

@ -101,8 +101,7 @@ void Engine::out_messages_pump_main() {
case MessageType::ENGINE_LOAD: {
EngineLoadMessage* tmsg = (EngineLoadMessage*)msg;
auto n = notification.add_engine_state_changes();
n->set_state(pb::EngineStateChange::RUNNING);
auto n = notification.add_engine_load();
n->set_load(tmsg->load);
break;
}

2
noisicaa/audioproc/engine/fluidsynth_util.cpp

@ -167,7 +167,7 @@ Status FluidSynthUtil::process_block(
} else if ((midi[0] & 0xf0) == 0x80) {
int rc = fluid_synth_noteoff(_synth, 0, midi[1]);
if (rc == FLUID_FAILED) {
_logger->info("noteoff failed.");
_logger->debug("noteoff failed.");
}
} else {
_logger->warning("Ignoring unsupported midi event %d.", midi[0] & 0xf0);

1
noisicaa/audioproc/engine/graph.py

@ -272,6 +272,7 @@ class Node(object):
The counterpart of setup().
"""
logger.info("%s: cleanup()", self.name)
self.__control_values.clear()
def set_session_value(self, key: str, value: session_data_pb2.SessionValue) -> None:
pass

13
noisicaa/audioproc/engine/processor_sound_file.cpp

@ -92,15 +92,20 @@ Status ProcessorSoundFile::process_block_internal(BlockContext* ctxt, TimeMapper
if (_playing) {
_playing = false;
uint8_t buf[100];
uint8_t atom[200];
LV2_Atom_Forge forge;
lv2_atom_forge_init(&forge, &_host_system->lv2->urid_map);
lv2_atom_forge_set_buffer(&forge, atom, sizeof(atom));
lv2_atom_forge_set_buffer(&forge, buf, sizeof(buf));
LV2_Atom_Forge_Frame oframe;
lv2_atom_forge_object(&forge, &oframe, _host_system->lv2->urid.core_nodemsg, 0);
lv2_atom_forge_atom(&forge, 0, _sound_file_complete_urid);
lv2_atom_forge_key(&forge, _sound_file_complete_urid);
lv2_atom_forge_bool(&forge, true);
NodeMessage::push(ctxt->out_messages, node_id(), (LV2_Atom*)buf);
lv2_atom_forge_pop(&forge, &oframe);
NodeMessage::push(ctxt->out_messages, node_id(), (LV2_Atom*)atom);
}
*l_out++ = 0.0;

2
noisicaa/audioproc/engine/realm.cpp

@ -210,7 +210,7 @@ string Realm::dump() const {
Program* program = _current_program.load();
if (program != nullptr) {
out += program->spec->dump();
out += program->spec->dump(_host_system);
} else {
out += "No current program.\n";
}

129
noisicaa/audioproc/engine/spec.cpp

@ -40,50 +40,105 @@ Spec::~Spec() {
_buffer_map.clear();
}
string Spec::dump() const {
string Spec::dump(HostSystem* host_system) const {
string out = "";
int i = 0;
for (const auto& opcode : _opcodes) {
const auto& opspec = opspecs[opcode.opcode];
if (_buffers.size() > 0) {
out += "Buffers:\n";
unsigned int i = 0;
for (const auto& buf : _buffers) {
out += sprintf(
"% 3u %s [%d bytes]\n",
i, pb::PortDescription::Type_Name(buf->type()).c_str(), buf->size(host_system));
++i;
}
}
string args = "";
for (size_t a = 0 ; a < opcode.args.size() ; ++a) {
const auto& arg = opcode.args[a];
if (_processors.size() > 0) {
out += "Processors:\n";
unsigned int i = 0;
for (const auto& proc : _processors) {
out += sprintf(
"% 3u %016lx [node_id=%s, state=%s]\n",
i, proc->id(), proc->node_id().c_str(), proc->state_name(proc->state()));
++i;
}
}
if (a > 0) {
args += ", ";
}
if (_control_values.size() > 0) {
out += "Control Values:\n";
unsigned int i = 0;
for (const auto& cv : _control_values) {
out += sprintf(
"% 3u %s [type=%s, value=%s, generation=%d]\n",
i, cv->name().c_str(), cv->type_name(), cv->formatted_value().c_str(), cv->generation());
++i;
}
}
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;
}
if (_child_realms.size() > 0) {
out += "Child Realms:\n";
unsigned int i = 0;
for (const auto& cr : _child_realms) {
out += sprintf(
"% 3u %s\n",
i, cr->name().c_str());
++i;
}
}
out += sprintf("% 3d %s(%s)\n", i, opspec.name, args.c_str());
++i;
if (_opcodes.size() > 0) {
out += "Opcodes:\n";
unsigned 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 'r': {
Realm* cr = _child_realms[arg.int_value()];
args += sprintf("#REALM<%s>", cr->name().c_str());
break;
}
case 'f':
args += sprintf("%f", arg.float_value());
break;
case 's':
args += sprintf("\"%s\"", arg.string_value().c_str());
break;
default:
args += sprintf("?%c?", opspec.argspec[a]);
break;
}
}
out += sprintf("% 3u %s(%s)\n", i, opspec.name, args.c_str());
++i;
}
}
return out;

3
noisicaa/audioproc/engine/spec.h

@ -42,6 +42,7 @@ class Processor;
class ControlValue;
class BufferType;
class Realm;
class HostSystem;
struct Instruction {
OpCode opcode;
@ -56,7 +57,7 @@ public:
Spec(const Spec&) = delete;
Spec operator=(const Spec&) = delete;
string dump() const;
string dump(HostSystem* host_system) const;
void set_bpm(uint32_t bpm) { _bpm = bpm; }
uint32_t bpm() const { return _bpm; }

1
noisicaa/audioproc/public/__init__.py

@ -21,6 +21,7 @@
from .engine_notification_pb2 import (
NodeStateChange,
EngineStateChange,
EngineLoad,
EngineNotification,
)
from .musical_time import (

16
noisicaa/audioproc/public/engine_notification.proto

@ -54,7 +54,10 @@ message EngineStateChange {
CLEANUP = 4;
}
required State state = 1;
optional float load = 2;
}
message EngineLoad {
optional float load = 1;
}
message DeviceManagerMessage {
@ -66,9 +69,10 @@ message DeviceManagerMessage {
message EngineNotification {
repeated EngineStateChange engine_state_changes = 1;
optional bytes perf_stats = 2;
optional PlayerState player_state = 3;
repeated NodeStateChange node_state_changes = 4;
repeated NodeMessage node_messages = 5;
repeated DeviceManagerMessage device_manager_messages = 6;
repeated EngineLoad engine_load = 2;
optional bytes perf_stats = 3;
optional PlayerState player_state = 4;
repeated NodeStateChange node_state_changes = 5;
repeated NodeMessage node_messages = 6;
repeated DeviceManagerMessage device_manager_messages = 7;
}

26
noisicaa/audioproc/public/time_mapper.cpp

@ -20,6 +20,7 @@
* @end:license
*/
#include <assert.h>
#include "noisicaa/audioproc/public/time_mapper.h"
namespace noisicaa {
@ -32,6 +33,31 @@ Status TimeMapper::setup() {
}
void TimeMapper::cleanup() {
_callback = nullptr;
_userdata = nullptr;
}
void TimeMapper::set_change_callback(void (*func)(void*), void* userdata) {
assert(_callback == nullptr);
_callback = func;
_userdata = userdata;
}
void TimeMapper::_changed() {
++_serialnum;
if (_callback != nullptr) {
_callback(_userdata);
}
}
void TimeMapper::set_bpm(uint32_t bpm) {
_bpm = bpm;
_changed();
}
void TimeMapper::set_duration(MusicalDuration duration) {
_duration = duration;
_changed();
}
MusicalTime TimeMapper::sample_to_musical_time(uint64_t sample_time) const {

14
noisicaa/audioproc/public/time_mapper.h

@ -39,12 +39,16 @@ public:
Status setup();
void cleanup();
void set_change_callback(void (*func)(void*), void* userdata);
uint32_t serialnum() const { return _serialnum; }
uint32_t sample_rate() const { return _sample_rate; }
void set_bpm(uint32_t bpm) { _bpm = bpm; }
void set_bpm(uint32_t bpm);
uint32_t bpm() const { return _bpm; }
void set_duration(MusicalDuration duration) { _duration = duration; }
void set_duration(MusicalDuration duration);
MusicalDuration duration() const { return _duration; }
MusicalTime end_time() const { return MusicalTime(0, 1) + _duration; }
uint64_t num_samples() const { return musical_to_sample_time(end_time()); }
@ -100,6 +104,12 @@ public:
iterator find(MusicalTime t) { return iterator(this, musical_to_sample_time(t)); }
private:
void _changed();
uint32_t _serialnum = 1;
void (*_callback)(void*) = nullptr;
void *_userdata = nullptr;
uint32_t _bpm = 120;
uint32_t _sample_rate;
MusicalDuration _duration = MusicalDuration(4, 1);

5
noisicaa/audioproc/public/time_mapper.pxd

@ -35,6 +35,8 @@ cdef extern from "noisicaa/audioproc/public/time_mapper.h" namespace "noisicaa"
Status setup()
void cleanup()
void set_change_callback(void (*func)(void*&), void* userdata)
uint32_t sample_rate() const
void set_bpm(uint32_t bpm)
@ -63,6 +65,9 @@ cdef class PyTimeMapper(object):
cdef unique_ptr[TimeMapper] __ptr
cdef TimeMapper* __tmap
cdef dict __listeners
cdef object __project
cdef TimeMapper* get(self)
cdef TimeMapper* release(self)
@staticmethod
cdef void __change_callback(void* c_self) with gil

36
noisicaa/audioproc/public/time_mapper.pyx

@ -18,6 +18,8 @@
#
# @end:license
from cpython.ref cimport PyObject
from cpython.exc cimport PyErr_Fetch, PyErr_Restore
from cython.operator cimport dereference, preincrement
from noisicaa.core.status cimport check
from .musical_time cimport PyMusicalTime, PyMusicalDuration
@ -28,19 +30,26 @@ cdef class PyTimeMapper(object):
self.__ptr.reset(new TimeMapper(sample_rate))
self.__tmap = self.__ptr.get()
self.__listeners = {}
self.__project = None
def setup(self, project=None):
with nogil:
check(self.__tmap.setup())
if project is not None:
self.bpm = project.bpm
self.__listeners['bpm'] = project.bpm_changed.add(self.__on_bpm_changed)
self.__project = project
self.duration = project.duration
self.__listeners['duration'] = project.duration_changed.add(self.__on_duration_changed)
self.bpm = self.__project.bpm
self.__listeners['bpm'] = self.__project.bpm_changed.add(self.__on_bpm_changed)
self.duration = self.__project.duration
self.__listeners['duration'] = self.__project.duration_changed.add(self.__on_duration_changed)
self.__tmap.set_change_callback(self.__change_callback, <PyObject*>self)
def cleanup(self):
self.__project = None
for listener in self.__listeners.values():
listener.remove()
self.__listeners.clear()
@ -53,6 +62,25 @@ cdef class PyTimeMapper(object):
cdef TimeMapper* release(self):
return self.__ptr.release()
@staticmethod
cdef void __change_callback(void* c_self) with gil:
self = <PyTimeMapper><PyObject*>c_self
# Have to stash away any active exception, because otherwise exception handling
# might get confused.
# See https://github.com/cython/cython/issues/1877
cdef PyObject* exc_type
cdef PyObject* exc_value
cdef PyObject* exc_trackback
PyErr_Fetch(&exc_type, &exc_value, &exc_trackback)
try:
if self.__project is not None:
self.__project.time_mapper_changed.call()
finally:
PyErr_Restore(exc_type, exc_value, exc_trackback)
def __on_bpm_changed(self, change):
self.bpm = change.new_value

4
noisicaa/builtin_nodes/mixer/node_ui.py

@ -56,6 +56,7 @@ class MixerNodeWidget(ui_base.ProjectMixin, core.AutoCleanupMixin, QtWidgets.QWi
name='hp_cutoff',
context=self.context)
self.__hp_cutoff_dial = control_value_dial.ControlValueDial(self)
self.__hp_cutoff_dial.setMinimumSize(32, 32)
self.__hp_cutoff_dial.setRange(10.0, 20000.0)
self.__hp_cutoff_dial.setDefault(1.0)
self.__hp_cutoff_dial.setLogScale(True)
@ -74,6 +75,7 @@ class MixerNodeWidget(ui_base.ProjectMixin, core.AutoCleanupMixin, QtWidgets.QWi
name='lp_cutoff',
context=self.context)
self.__lp_cutoff_dial = control_value_dial.ControlValueDial(self)
self.__lp_cutoff_dial.setMinimumSize(32, 32)
self.__lp_cutoff_dial.setRange(10.0, 20000.0)
self.__lp_cutoff_dial.setDefault(20000.0)
self.__lp_cutoff_dial.setLogScale(True)
@ -92,6 +94,7 @@ class MixerNodeWidget(ui_base.ProjectMixin, core.AutoCleanupMixin, QtWidgets.QWi
name='gain',
context=self.context)
self.__gain_dial = control_value_dial.ControlValueDial(self)
self.__gain_dial.setMinimumSize(32, 32)
self.__gain_dial.setRange(-40.0, 20.0)
self.__gain_dial.setDefault(0.0)
self.__gain_dial.setDisplayFunc(lambda value: '%+.2fdB' % value)
@ -114,6 +117,7 @@ class MixerNodeWidget(ui_base.ProjectMixin, core.AutoCleanupMixin, QtWidgets.QWi
name='pan',
context=self.context)
self.__pan_dial = control_value_dial.ControlValueDial(self)
self.__pan_dial.setMinimumSize(32, 32)
self.__pan_dial.setRange(-1.0, 1.0)
self.__pan_dial.setDefault(0.0)
self.__pan_dial.setDisplayFunc(lambda value: '%.2f' % value)

24
noisicaa/builtin_nodes/mixer/processor.cpp

@ -64,25 +64,29 @@ instr 2
; filters
if (gk_hp_cutoff > 1) then
a_sig_l = butterhp(a_sig_l, gk_hp_cutoff)
a_sig_r = butterhp(a_sig_r, gk_hp_cutoff)
a_hp_cutoff = tone(a(gk_hp_cutoff), 10)
a_sig_l = butterhp(a_sig_l, a_hp_cutoff)
a_sig_r = butterhp(a_sig_r, a_hp_cutoff)
endif
if (gk_lp_cutoff < 20000) then
a_sig_l = butterlp(a_sig_l, gk_lp_cutoff)
a_sig_r = butterlp(a_sig_r, gk_lp_cutoff)
a_lp_cutoff = tone(a(gk_lp_cutoff), 10)
a_sig_l = butterlp(a_sig_l, a_lp_cutoff)
a_sig_r = butterlp(a_sig_r, a_lp_cutoff)
endif
; pan signal
i_sqrt2 = 1.414213562373095
k_theta = 3.141592653589793 * 45 * (1 - gk_pan) / 180
a_sig_l = i_sqrt2 * sin(k_theta) * a_sig_l
a_sig_r = i_sqrt2 * cos(k_theta) * a_sig_r
a_pan = tone(a(gk_pan), 10)
a_theta = 3.141592653589793 * 45 * (1 - a_pan) / 180
a_sig_l = i_sqrt2 * sin(a_theta) * a_sig_l
a_sig_r = i_sqrt2 * cos(a_theta) * a_sig_r
; apply gain
k_volume = db(gk_gain)
ga_out_l = k_volume * a_sig_l
ga_out_r = k_volume * a_sig_r
a_gain = tone(a(gk_gain), 10)
a_volume = db(a_gain)
ga_out_l = a_volume * a_sig_l
ga_out_r = a_volume * a_sig_r
end:
endin

6
noisicaa/builtin_nodes/sample_track/processor.cpp

@ -98,6 +98,7 @@ void SampleScript::apply_mutation(Logger* logger, pb::ProcessorMessage* msg) {
// Invalidate script's cursor (so ProcessorSampleScript::process_block() is forced to do a seek
// first).
offset = -1;
tmap_serialnum = 0;
}
ProcessorSampleScript::ProcessorSampleScript(
@ -155,13 +156,16 @@ Status ProcessorSampleScript::process_block_internal(BlockContext* ctxt, TimeMap
lvalue = 0.0;
rvalue = 0.0;
} else {
if (script->offset < 0 || script->current_time != stime->start_time) {
if (script->offset < 0
|| script->tmap_serialnum != time_mapper->serialnum()
|| script->current_time != stime->start_time) {
// seek to new time.
// TODO: We could to better than a sequential search.
// - Do a binary search to find the new script->offset.
script->offset = 0;
script->tmap_serialnum = time_mapper->serialnum();
script->current_audio_file = nullptr;
while ((size_t)script->offset < script->samples.size()) {
const Sample& sample = script->samples[script->offset];

1
noisicaa/builtin_nodes/sample_track/processor.h

@ -57,6 +57,7 @@ public:
vector<Sample> samples;
uint32_t tmap_serialnum = 0;
int offset = -1;
MusicalTime current_time = MusicalTime(0, 1);

15
noisicaa/builtin_nodes/sample_track/track_ui.py

@ -254,7 +254,7 @@ class SampleItem(core.AutoCleanupMixin, object):
self.__pos = QtCore.QPoint()
self.__width = 50
self.__height = None # type: int
self.__updateRect()
self.updateRect()
self.__listeners = core.ListenerList()
self.add_cleanup_function(self.__listeners.cleanup)
@ -304,15 +304,15 @@ class SampleItem(core.AutoCleanupMixin, object):
return QtCore.QRect(self.pos(), self.size())
def __onTimeChanged(self, change: music.PropertyValueChange[audioproc.MusicalTime]) -> None:
self.__updateRect()
self.updateRect()
self.__track_editor.update()
def __onScaleXChanged(self, scale_x: fractions.Fraction) -> None:
self.__updateRect()
self.updateRect()
self.__tile_cache.clear()
self.purgePaintCaches()
def __updateRect(self) -> None:
def updateRect(self) -> None:
tmap = self.__track_editor.project.time_mapper
num_samples = int(math.ceil(
@ -506,6 +506,7 @@ class SampleTrackEditor(time_view_mixin.ContinuousTimeMixin, base_track_editor.B
self.addSample(len(self.__samples), sample)
self.__listeners.add(self.track.samples_changed.add(self.onSamplesChanged))
self.__listeners.add(self.project.time_mapper_changed.add(self.__timeMapperChanged))
self.playbackPositionChanged.connect(self.__playbackPositionChanged)
@ -548,6 +549,12 @@ class SampleTrackEditor(time_view_mixin.ContinuousTimeMixin, base_track_editor.B
item.cleanup()
self.update()
def __timeMapperChanged(self) -> None:
self.purgePaintCaches()
for item in self.__samples:
item.updateRect()
self.update()
def __playbackPositionChanged(self, time: audioproc.MusicalTime) -> None:
if self.__playback_time is not None:
x = self.timeToX(self.__playback_time)

3
noisicaa/constants.py

@ -32,11 +32,10 @@ EXIT_EXCEPTION = 1
EXIT_RESTART = 17
EXIT_RESTART_CLEAN = 18
ROOT = os.path.abspath(os.path.dirname(__file__))
DATA_DIR = os.environ['NOISICAA_DATA_DIR']
CACHE_DIR = os.path.abspath(os.path.join(os.path.expanduser('~'), '.cache', 'noisicaä'))
RUN_DIR = os.path.abspath(os.path.join(os.path.expanduser('~'), '.config', 'noisicaä'))
def __xdg_user_dir(resource: str) -> str:
try:

7
noisicaa/editor_main.py

@ -23,7 +23,9 @@
import argparse
import asyncio
import functools
import os
import signal
import subprocess
import sys
import time
from typing import List
@ -172,6 +174,11 @@ class Editor(object):
else:
return proc.returncode
if self.runtime_settings.dev_mode:
subprocess.check_call(
['./waf', 'build'],
cwd=os.environ['NOISICAA_SRC_ROOT'])
def ui_closed(self, task: asyncio.Task) -> None:
if task.exception():
self.logger.error("UI failed with an exception: %s", task.exception())

4
noisicaa/instrument_db/private/db_test.py

@ -22,10 +22,8 @@
import asyncio
import logging
import os.path
from noisidev import unittest
from noisicaa import constants
from noisicaa import instrument_db
from . import db
@ -45,7 +43,7 @@ class InstrumentDBTest(unittest.AsyncTestCase):
try:
instdb.setup()
instdb.start_scan([os.path.join(constants.ROOT, '..', 'testdata')], False)
instdb.start_scan([unittest.TESTDATA_DIR], False)
self.assertTrue(await complete.wait())
finally:

36
noisicaa/music/project.py

@ -60,6 +60,7 @@ class BaseProject(_model.Project, model_base.ObjectBase):
self.__time_mapper = audioproc.TimeMapper(44100)
self.__time_mapper.setup(self)
self.time_mapper_changed = core.Callback()
self._in_mutation = False
@ -87,19 +88,44 @@ class BaseProject(_model.Project, model_base.ObjectBase):
conn.dest_node.connections_changed.call(change)
self.node_connections_changed.add(self.__node_connections_changed)
self.nodes_changed.add(self.__nodes_changed)
def __node_connections_changed(
self, change: model_base.PropertyListChange[graph.NodeConnection]
) -> None:
if isinstance(change, model_base.PropertyListInsert):
conn = change.new_value
for mutation in conn.get_add_mutations():
self.handle_pipeline_mutation(mutation)
elif isinstance(change, model_base.PropertyListDelete):
conn = change.old_value
for mutation in conn.get_remove_mutations():
self.handle_pipeline_mutation(mutation)
else:
raise ValueError(change)
conn.source_node.connections_changed.call(change)
conn.dest_node.connections_changed.call(change)
def __nodes_changed(
self, change: model_base.PropertyListChange[graph.Node]
) -> None:
if isinstance(change, model_base.PropertyListInsert):
node = change.new_value
for mutation in node.get_add_mutations():
self.handle_pipeline_mutation(mutation)
elif isinstance(change, model_base.PropertyListDelete):
node = change.old_value
for mutation in node.get_remove_mutations():
self.handle_pipeline_mutation(mutation)
elif isinstance(change, model_base.PropertyListMove):
pass
else:
raise ValueError(change)
@property
def time_mapper(self) -> audioproc.TimeMapper:
return self.__time_mapper
@ -206,9 +232,6 @@ class BaseProject(_model.Project, model_base.ObjectBase):
node.connections_changed.call(
model_base.PropertyListInsert(self, 'node_connections', -1, conn))
for mutation in node.get_add_mutations():
self.handle_pipeline_mutation(mutation)
self.nodes.append(node)
def remove_node(self, node: graph.BaseNode) -> None:
@ -221,9 +244,6 @@ class BaseProject(_model.Project, model_base.ObjectBase):
for cidx in sorted(delete_connections, reverse=True):
self.remove_node_connection(self.node_connections[cidx])
for mutation in node.get_remove_mutations():
self.handle_pipeline_mutation(mutation)
del self.nodes[node.index]
def create_node_connection(
@ -250,12 +270,8 @@ class BaseProject(_model.Project, model_base.ObjectBase):
def add_node_connection(self, connection: graph.NodeConnection) -> None:
self.node_connections.append(connection)
for mutation in connection.get_add_mutations():
self.handle_pipeline_mutation(mutation)
def remove_node_connection(self, connection: graph.NodeConnection) -> None:
for mutation in connection.get_remove_mutations():
self.handle_pipeline_mutation(mutation)
del self.node_connections[connection.index]
def get_add_mutations(self) -> Iterator[audioproc.Mutation]:

5
noisicaa/ui/device_list.py

@ -171,10 +171,11 @@ class DeviceList(QtCore.QAbstractItemModel):
return QtCore.QModelIndex()
item = down_cast(ModelItem, index.internalPointer())
if item is self.__root:
parent = item.parent
if parent is self.__root:
return QtCore.QModelIndex()
return self.createIndex(item.parent.index, 0, item.parent)
return self.createIndex(parent.index, 0, parent)
def columnCount(self, parent: QtCore.QModelIndex = QtCore.QModelIndex()) -> int:
return 1

167
noisicaa/ui/device_list_test.py

@ -0,0 +1,167 @@
#!/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 PyQt5.QtCore import Qt
from PyQt5 import QtCore
from noisidev import uitest
from noisicaa import audioproc
from . import device_list
class DeviceListTest(uitest.UITestCase):
def assertIndexEqual(self, i1, i2):
def cmp_index(i1, i2):
if not i1.isValid() and not i2.isValid():
return True
return (
i1.row() == i2.row()
and i1.column() == i2.column()
and cmp_index(i1.parent(), i2.parent()))
def format_index(i):
if not i.isValid():
return ''
s = format_index(i.parent())
if s:
s += '>'
s += '[%d,%d]' % (i.row(), i.column())
return s
self.assertTrue(cmp_index(i1, i2), '%s != %s' % (format_index(i1), format_index(i2)))
def fill_model(self, model):
for i in range(5):
model.addDevice(audioproc.DeviceDescription(
uri='alsa://%d' % (i + 10),
type=audioproc.DeviceDescription.MIDI_CONTROLLER,
display_name='holla %d' % (i + 1),
ports=[
audioproc.DevicePortDescription(
uri='alsa://%d/0' % (i + 10),
type=audioproc.DevicePortDescription.MIDI,
display_name='port1',
readable=True,
writable=True),
]))
async def test_addDevice_at_bottom(self):
model = device_list.DeviceList()
self.fill_model(model)
root_index = QtCore.QModelIndex()
self.assertEqual(model.rowCount(root_index), 5)
self.assertEqual(model.parent(root_index), QtCore.QModelIndex())
model.addDevice(audioproc.DeviceDescription(
uri='alsa://20',
type=audioproc.DeviceDescription.MIDI_CONTROLLER,
display_name='knut',
ports=[
audioproc.DevicePortDescription(
uri='alsa://20/0',
type=audioproc.DevicePortDescription.MIDI,
display_name='port1',
readable=True,
writable=True),
]))
self.assertEqual(model.rowCount(root_index), 6)
dev1_index = model.index(5, parent=root_index)
self.assertEqual(model.data(dev1_index, Qt.UserRole), 'alsa://20')
self.assertEqual(model.data(dev1_index, Qt.DisplayRole), 'knut')
self.assertEqual(model.rowCount(dev1_index), 1)
self.assertIndexEqual(model.parent(dev1_index), root_index)
dev1p1_index = model.index(0, parent=dev1_index)
self.assertEqual(model.data(dev1p1_index, Qt.UserRole), 'alsa://20/0')
self.assertEqual(model.data(dev1p1_index, Qt.DisplayRole), 'port1')
self.assertEqual(model.rowCount(dev1p1_index), 0)
self.assertIndexEqual(model.parent(dev1p1_index), dev1_index)
async def test_addDevice_at_top(self):
model = device_list.DeviceList()
self.fill_model(model)
root_index = QtCore.QModelIndex()
self.assertEqual(model.rowCount(root_index), 5)
self.assertEqual(model.parent(root_index), QtCore.QModelIndex())
model.addDevice(audioproc.DeviceDescription(
uri='alsa://20',
type=audioproc.DeviceDescription.MIDI_CONTROLLER,
display_name='anna',
ports=[
audioproc.DevicePortDescription(
uri='alsa://20/0',
type=audioproc.DevicePortDescription.MIDI,
display_name='port1',
readable=True,
writable=True),
]))
self.assertEqual(model.rowCount(root_index), 6)
dev1_index = model.index(0, parent=root_index)
self.assertEqual(model.data(dev1_index, Qt.UserRole), 'alsa://20')
self.assertEqual(model.data(dev1_index, Qt.DisplayRole), 'anna')
self.assertEqual(model.rowCount(dev1_index), 1)
self.assertIndexEqual(model.parent(dev1_index), root_index)
dev1p1_index = model.index(0, parent=dev1_index)
self.assertEqual(model.data(dev1p1_index, Qt.UserRole), 'alsa://20/0')
self.assertEqual(model.data(dev1p1_index, Qt.DisplayRole), 'port1')
self.assertEqual(model.rowCount(dev1p1_index), 0)
self.assertIndexEqual(model.parent(dev1p1_index), dev1_index)
async def test_addDevice_middle(self):
model = device_list.DeviceList()
self.fill_model(model)
root_index = QtCore.QModelIndex()
self.assertEqual(model.rowCount(root_index), 5)
self.assertEqual(model.parent(root_index), QtCore.QModelIndex())
model.addDevice(audioproc.DeviceDescription(
uri='alsa://20',
type=audioproc.DeviceDescription.MIDI_CONTROLLER,
display_name='holla 2b',
ports=[
audioproc.DevicePortDescription(
uri='alsa://20/0',
type=audioproc.DevicePortDescription.MIDI,
display_name='port1',
readable=True,
writable=True),
]))
self.assertEqual(model.rowCount(root_index), 6)
dev1_index = model.index(2, parent=root_index)
self.assertEqual(model.data(dev1_index, Qt.UserRole), 'alsa://20')
self.assertEqual(model.data(dev1_index, Qt.DisplayRole), 'holla 2b')
self.assertEqual(model.rowCount(dev1_index), 1)
self.assertIndexEqual(model.parent(dev1_index), root_index)
dev1p1_index = model.index(0, parent=dev1_index)
self.assertEqual(model.data(dev1p1_index, Qt.UserRole), 'alsa://20/0')
self.assertEqual(model.data(dev1p1_index, Qt.DisplayRole), 'port1')
self.assertEqual(model.rowCount(dev1p1_index), 0)
self.assertIndexEqual(model.parent(dev1p1_index), dev1_index)

53
noisicaa/ui/editor_app.py

@ -24,8 +24,10 @@ import asyncio
import functools
import logging
import os
import os.path
import sys
import textwrap
import time
import traceback
import types