Browse Source

Add MIDI Looper node.

pyside
Ben Niemann 3 years ago
parent
commit
bc9be73125
  1. 2
      3rdparty/typeshed/sortedcontainers.pyi
  2. 1
      listdeps
  3. 1
      noisicaa/audioproc/__init__.py
  4. 2
      noisicaa/audioproc/engine/spec.pyi
  5. 8
      noisicaa/audioproc/public/CMakeLists.txt
  6. 3
      noisicaa/audioproc/public/__init__.py
  7. 32
      noisicaa/audioproc/public/midi_event.proto
  8. 9
      noisicaa/audioproc/public/musical_time.cpp
  9. 3
      noisicaa/audioproc/public/musical_time.h
  10. 2
      noisicaa/audioproc/public/musical_time.pxd
  11. 115
      noisicaa/audioproc/public/musical_time.pyi
  12. 15
      noisicaa/audioproc/public/musical_time.pyx
  13. 15
      noisicaa/audioproc/public/musical_time_test.pyx
  14. 41
      noisicaa/audioproc/public/time_mapper.pyi
  15. 3
      noisicaa/builtin_nodes/CMakeLists.txt
  16. 2
      noisicaa/builtin_nodes/beat_track/track_ui.py
  17. 31
      noisicaa/builtin_nodes/custom_csound/node_ui.py
  18. 47
      noisicaa/builtin_nodes/midi_looper/CMakeLists.txt
  19. 21
      noisicaa/builtin_nodes/midi_looper/__init__.py
  20. 49
      noisicaa/builtin_nodes/midi_looper/model.desc.pb
  21. 92
      noisicaa/builtin_nodes/midi_looper/model.py
  22. 48
      noisicaa/builtin_nodes/midi_looper/model_test.py
  23. 49
      noisicaa/builtin_nodes/midi_looper/node_description.py
  24. 293
      noisicaa/builtin_nodes/midi_looper/node_ui.py
  25. 67
      noisicaa/builtin_nodes/midi_looper/node_ui_test.py
  26. 364
      noisicaa/builtin_nodes/midi_looper/processor.cpp
  27. 104
      noisicaa/builtin_nodes/midi_looper/processor.h
  28. 42
      noisicaa/builtin_nodes/midi_looper/processor.proto
  29. 90
      noisicaa/builtin_nodes/midi_looper/processor_test.py
  30. 5
      noisicaa/builtin_nodes/model_registry.proto
  31. 4
      noisicaa/builtin_nodes/model_registry.py
  32. 2
      noisicaa/builtin_nodes/node_description_registry.py
  33. 4
      noisicaa/builtin_nodes/processor_message_registry.proto
  34. 4
      noisicaa/builtin_nodes/processor_registry.cpp
  35. 2
      noisicaa/builtin_nodes/ui_registry.py
  36. 10
      noisicaa/lv2/atom.pyx
  37. 1
      noisicaa/music/__init__.py
  38. 3
      noisicaa/music/base_track.py
  39. 2
      noisicaa/music/mutations.proto
  40. 4
      noisicaa/music/mutations.py
  41. 2
      noisicaa/music/project_client.py
  42. 2
      noisicaa/ui/CMakeLists.txt
  43. 24
      noisicaa/ui/graph/base_node.py
  44. 378
      noisicaa/ui/pianoroll.py
  45. 58
      noisicaa/ui/pianoroll_test.py
  46. 36
      noisicaa/ui/slots.py
  47. 3
      noisicaa/ui/track_list/measured_track_editor.py
  48. 1
      noisicaa/value_types/CMakeLists.txt
  49. 1
      noisicaa/value_types/__init__.py
  50. 94
      noisicaa/value_types/midi_event.py
  51. 3
      noisidev/build_model.py
  52. 15
      noisidev/model_desc.proto

2
3rdparty/typeshed/sortedcontainers.pyi vendored

@ -0,0 +1,2 @@
from typing import Any
def __getattr__(arrr: str) -> Any: ...

1
listdeps

@ -64,6 +64,7 @@ PIP_DEPS = {
PKG('pyparsing'),
# TODO: get my changes upstream and use regular quamash package from pip.
PKG('git+https://github.com/odahoda/quamash.git#egg=quamash'),
PKG('sortedcontainers'),
PKG('toposort'),
PKG('urwid'),
],

1
noisicaa/audioproc/__init__.py

@ -60,4 +60,5 @@ from .public import (
HostParameters,
NodePortProperties,
NodeParameters,
MidiEvent,
)

2
noisicaa/audioproc/engine/spec.pyi

@ -32,7 +32,7 @@ opname = ... # type: Dict[int, str]
class PySpec(object):
bpm = ... # type: int
duration = ... # type: audioproc.MusicalTime
duration = ... # type: audioproc.MusicalDuration
def __init__(self) -> None: ...
def dump(self) -> str: ...

8
noisicaa/audioproc/public/CMakeLists.txt

@ -18,7 +18,10 @@
#
# @end:license
add_python_package()
add_python_package(
time_mapper.pyi
musical_time.pyi
)
set(LIB_SRCS
backend_settings.pb.cc
@ -27,6 +30,7 @@ set(LIB_SRCS
engine_notification.pb.cc
host_parameters.pb.cc
instrument_spec.pb.cc
midi_event.pb.cc
musical_time.cpp
musical_time.pb.cc
node_parameters.pb.cc
@ -69,6 +73,8 @@ cpp_proto(node_port_properties.proto)
py_proto(node_port_properties.proto)
cpp_proto(node_parameters.proto)
py_proto(node_parameters.proto)
cpp_proto(midi_event.proto)
py_proto(midi_event.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)

3
noisicaa/audioproc/public/__init__.py

@ -69,3 +69,6 @@ from .node_port_properties_pb2 import (
from .node_parameters_pb2 import (
NodeParameters,
)
from .midi_event_pb2 import (
MidiEvent,
)

32
noisicaa/audioproc/public/midi_event.proto

@ -0,0 +1,32 @@
/*
* @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";
import "noisicaa/audioproc/public/musical_time.proto";
package noisicaa.pb;
message MidiEvent {
required MusicalTime time = 1;
required bytes midi = 2;
}

9
noisicaa/audioproc/public/musical_time.cpp

@ -85,6 +85,15 @@ void Fraction::div(int64_t n, int64_t d) {
reduce();
}
void Fraction::mod(int64_t n, int64_t d) {
assert(n != 0);
int64_t t = _denominator;
int64_t a = (n * _denominator);
_numerator = ((_numerator * d) % a + a) % a;
_denominator = t * d;
reduce();
}
int Fraction::cmp(int64_t n, int64_t d) const {
assert(d > 0);
int64_t c = _numerator * d - _denominator * n;

3
noisicaa/audioproc/public/musical_time.h

@ -64,6 +64,7 @@ protected:
void sub(int64_t n, int64_t d);
void mul(int64_t n, int64_t d);
void div(int64_t n, int64_t d);
void mod(int64_t n, int64_t d);
private:
int64_t _numerator;
@ -153,6 +154,7 @@ public:
void sub(const MusicalDuration& t) { Fraction::sub(t.numerator(), t.denominator()); }
void mul(const Fraction& t) { Fraction::mul(t.numerator(), t.denominator()); }
void div(const Fraction& t) { Fraction::div(t.numerator(), t.denominator()); }
void mod(const Fraction& t) { Fraction::mod(t.numerator(), t.denominator()); }
MusicalTime& operator+=(const MusicalDuration& t) { add(t); return *this; }
MusicalTime& operator-=(const MusicalDuration& t) { sub(t); return *this; }
@ -168,6 +170,7 @@ public:
}
friend MusicalTime operator*(MusicalTime a, const Fraction& b) { a.mul(b); return a; }
friend MusicalTime operator/(MusicalTime a, const Fraction& b) { a.div(b); return a; }
friend MusicalTime operator%(MusicalTime a, const Fraction& b) { a.mod(b); return a; }
friend bool operator==(const MusicalTime& a, const MusicalTime& b) { return a.cmp(b) == 0; }
friend bool operator!=(const MusicalTime& a, const MusicalTime& b) { return a.cmp(b) != 0; }

2
noisicaa/audioproc/public/musical_time.pxd

@ -69,6 +69,7 @@ cdef extern from "noisicaa/audioproc/public/musical_time.h" namespace "noisicaa"
void sub(const MusicalDuration& t)
void mul(const Fraction& t)
void div(const Fraction& t)
void mod(const Fraction& t)
# Actual implementation in C++ have different signatures, which
# Cython does not understand. But it is sufficient that Cython
@ -78,6 +79,7 @@ cdef extern from "noisicaa/audioproc/public/musical_time.h" namespace "noisicaa"
MusicalDuration operator-(const MusicalTime& b)
MusicalTime operator*(const Fraction& b)
MusicalTime operator/(const Fraction& b)
MusicalTime operator%(const Fraction& b)
bool operator==(const MusicalTime& b)
bool operator!=(const MusicalTime& b)
bool operator<(const MusicalTime& b)

115
noisicaa/audioproc/public/musical_time.pyi

@ -0,0 +1,115 @@
#!/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
import fractions
from typing import overload, Any, Union
from google.protobuf import message as protobuf
from noisicaa import value_types
from . import musical_time_pb2
class PyMusicalDuration(value_types.ProtoValue):
@overload
def __init__(self) -> None: ...
@overload
def __init__(self, numerator: int, denominator: int) -> None: ...
@overload
def __init__(self, duration: PyMusicalDuration) -> None: ...
@overload
def __init__(self, duration: fractions.Fraction) -> None: ...
@overload
def __init__(self, duration: int) -> None: ...
def __hash__(self) -> int: ...
def __str__(self) -> str: ...
def __repr__(self) -> str: ...
def __getstate__(self) -> Any: ...
def __setstate__(self, state: Any) -> None: ...
@property
def numerator(self) -> int: ...
@property
def denominator(self) -> int: ...
@property
def fraction(self) -> fractions.Fraction: ...
def to_float(self) -> float: ...
def __bool__(self) -> bool: ...
def __eq__(self, other: Any) -> bool: ...
def __ne__(self, other: Any) -> bool: ...
def __gt__(self, other: PyMusicalDuration) -> bool: ...
def __ge__(self, other: PyMusicalDuration) -> bool: ...
def __le__(self, other: PyMusicalDuration) -> bool: ...
def __lt__(self, other: PyMusicalDuration) -> bool: ...
def __add__(self, other: PyMusicalDuration) -> PyMusicalDuration: ...
def __sub__(self, other: PyMusicalDuration) -> PyMusicalDuration: ...
def __mul__(self, other: Union[PyMusicalDuration, PyMusicalTime, fractions.Fraction, int]) -> PyMusicalDuration: ...
def __truediv__(self, other: Union[PyMusicalDuration, PyMusicalTime, fractions.Fraction, int]) -> PyMusicalDuration: ...
def __int__(self) -> int: ...
def __float__(self) -> float: ...
@classmethod
def from_proto(cls, pb: protobuf.Message) -> PyMusicalDuration: ...
def to_proto(self) -> musical_time_pb2.MusicalDuration: ...
class PyMusicalTime(value_types.ProtoValue):
@overload
def __init__(self) -> None: ...
@overload
def __init__(self, numerator: int, denominator: int) -> None: ...
@overload
def __init__(self, duration: PyMusicalTime) -> None: ...
@overload
def __init__(self, duration: fractions.Fraction) -> None: ...
@overload
def __init__(self, duration: int) -> None: ...
def __hash__(self) -> int: ...
def __str__(self) -> str: ...
def __repr__(self) -> str: ...
def __getstate__(self) -> Any: ...
def __setstate__(self, state: Any) -> None: ...
@property
def numerator(self) -> int: ...
@property
def denominator(self) -> int: ...
@property
def fraction(self) -> fractions.Fraction: ...
def to_float(self) -> float: ...
def __bool__(self) -> bool: ...
def __eq__(self, other: Any) -> bool: ...
def __ne__(self, other: Any) -> bool: ...
def __gt__(self, other: PyMusicalTime) -> bool: ...
def __ge__(self, other: PyMusicalTime) -> bool: ...
def __lt__(self, other: PyMusicalTime) -> bool: ...
def __le__(self, other: PyMusicalTime) -> bool: ...
def __add__(self, other: PyMusicalDuration) -> PyMusicalTime: ...
@overload
def __sub__(self, other: PyMusicalDuration) -> PyMusicalTime: ...
@overload
def __sub__(self, other: PyMusicalTime) -> PyMusicalDuration: ...
def __mul__(self, other: Union[PyMusicalDuration, PyMusicalTime, fractions.Fraction, int]) -> PyMusicalTime: ...
def __truediv__(self, other: Union[PyMusicalDuration, PyMusicalTime, fractions.Fraction, int]) -> PyMusicalTime: ...
def __mod__(self, other: Union[PyMusicalDuration, PyMusicalTime, fractions.Fraction, int]) -> PyMusicalTime: ...
def __int__(self) -> int: ...
def __float__(self) -> float: ...
@classmethod
def from_proto(cls, pb: protobuf.Message) -> PyMusicalTime: ...
def to_proto(self) -> musical_time_pb2.MusicalTime: ...

15
noisicaa/audioproc/public/musical_time.pyx

@ -135,6 +135,12 @@ cdef class PyMusicalDuration(object):
v /= _as_fraction(other)
return PyMusicalDuration.create(v)
def __int__(self):
return int(self._duration.numerator() // self._duration.denominator())
def __float__(self):
return float(self._duration.to_double())
@classmethod
def from_proto(cls, pb):
return cls(pb.numerator, pb.denominator)
@ -245,6 +251,15 @@ cdef class PyMusicalTime(object):
v /= _as_fraction(other)
return PyMusicalTime.create(v)
def __mod__(PyMusicalTime self, other):
return PyMusicalTime.create(self._time % _as_fraction(other))
def __int__(self):
return int(self._time.numerator() // self._time.denominator())
def __float__(self):
return float(self._time.to_double())
@classmethod
def from_proto(cls, pb):
return cls(pb.numerator, pb.denominator)

15
noisicaa/audioproc/public/musical_time_test.pyx

@ -222,6 +222,21 @@ class MusicalTimeTest(unittest.TestCase):
t = MusicalTime(2, 3) / MusicalTime(n, d)
self.assertEqual(fractions.Fraction(t.numerator(), t.denominator()), expected)
def test_mod(self):
cdef MusicalTime t
for n in range(-100, 100):
if n == 0:
continue
for d in range(1, 100):
expected = fractions.Fraction(2, 3) % fractions.Fraction(n, d)
t = MusicalTime(2, 3)
t.mod(MusicalTime(n, d))
self.assertEqual(fractions.Fraction(t.numerator(), t.denominator()), expected, "2/3 %% %d/%d" % (n, d))
t = MusicalTime(2, 3) % MusicalTime(n, d)
self.assertEqual(fractions.Fraction(t.numerator(), t.denominator()), expected)
def test_cmp(self):
self.assertTrue(MusicalTime(1, 2) == MusicalTime(2, 4))
self.assertFalse(MusicalTime(1, 2) == MusicalTime(3, 4))

41
noisicaa/audioproc/public/time_mapper.pyi

@ -0,0 +1,41 @@
# @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 typing import Iterator
from .musical_time import PyMusicalTime, PyMusicalDuration
from noisicaa import music
class PyTimeMapper(object):
bpm = ... # type: int
duration = ... # type: PyMusicalDuration
def __init__(self, sample_rate: int) -> None: ...
def setup(self, project: music.BaseProject = None) -> None: ...
def cleanup(self) -> None: ...
@property
def end_time(self) -> PyMusicalTime: ...
@property
def num_samples(self) -> int: ...
def sample_to_musical_time(self, sample_time: int) -> PyMusicalTime: ...
def musical_to_sample_time(self, musical_time: PyMusicalTime) -> int: ...
def __iter__(self) -> Iterator[PyMusicalTime]: ...
def find(self, t: PyMusicalTime) -> Iterator[PyMusicalTime]: ...

3
noisicaa/builtin_nodes/CMakeLists.txt

@ -39,6 +39,7 @@ target_link_libraries(noisicaa-builtin_nodes-processor_message_registry PRIVATE
target_link_libraries(noisicaa-builtin_nodes-processor_message_registry PRIVATE noisicaa-builtin_nodes-pianoroll-processor_messages)
target_link_libraries(noisicaa-builtin_nodes-processor_message_registry PRIVATE noisicaa-builtin_nodes-midi_source-processor_messages)
target_link_libraries(noisicaa-builtin_nodes-processor_message_registry PRIVATE noisicaa-builtin_nodes-midi_cc_to_cv-processor_messages)
target_link_libraries(noisicaa-builtin_nodes-processor_message_registry PRIVATE noisicaa-builtin_nodes-midi_looper-processor_messages)
add_library(noisicaa-builtin_nodes-processors SHARED processor_registry.cpp)
target_link_libraries(noisicaa-builtin_nodes-processors PRIVATE noisicaa-builtin_nodes-control_track-processor)
@ -53,6 +54,7 @@ target_link_libraries(noisicaa-builtin_nodes-processors PRIVATE noisicaa-builtin
target_link_libraries(noisicaa-builtin_nodes-processors PRIVATE noisicaa-builtin_nodes-noise-processor)
target_link_libraries(noisicaa-builtin_nodes-processors PRIVATE noisicaa-builtin_nodes-step_sequencer-processor)
target_link_libraries(noisicaa-builtin_nodes-processors PRIVATE noisicaa-builtin_nodes-midi_cc_to_cv-processor)
target_link_libraries(noisicaa-builtin_nodes-processors PRIVATE noisicaa-builtin_nodes-midi_looper-processor)
add_subdirectory(score_track)
add_subdirectory(beat_track)
@ -68,3 +70,4 @@ add_subdirectory(vca)
add_subdirectory(noise)
add_subdirectory(step_sequencer)
add_subdirectory(midi_cc_to_cv)
add_subdirectory(midi_looper)

2
noisicaa/builtin_nodes/beat_track/track_ui.py

@ -128,7 +128,7 @@ class BeatMeasureEditor(measured_track_editor.MeasureEditor):
def measure(self) -> model.BeatMeasure:
return down_cast(model.BeatMeasure, super().measure)
def xToTime(self, x: int) -> audioproc.MusicalTime:
def xToTime(self, x: int) -> audioproc.MusicalDuration:
return audioproc.MusicalDuration(
int(8 * self.measure.time_signature.upper * x / self.width()),
8 * self.measure.time_signature.upper)

31
noisicaa/builtin_nodes/custom_csound/node_ui.py

@ -426,33 +426,16 @@ class Editor(ui_base.ProjectMixin, core.AutoCleanupMixin, QtWidgets.QDialog):
class CustomCSoundNode(generic_node.GenericNode):
has_window = True
def __init__(self, *, node: music.BaseNode, **kwargs: Any) -> None:
super().__init__(node=node, **kwargs)
assert isinstance(node, model.CustomCSound), type(node).__name__
self.__node = node # type: model.CustomCSound
self.__editor = None # type: Editor
def cleanup(self) -> None:
if self.__editor is not None:
self.__editor.close()
self.__editor.cleanup()
self.__editor = None
super().cleanup()
def buildContextMenu(self, menu: QtWidgets.QMenu) -> None:
show_editor = menu.addAction("CSound Editor")
show_editor.triggered.connect(self.__showEditor)
super().buildContextMenu(menu)
def __showEditor(self) -> None:
if self.__editor is None:
self.__editor = Editor(
node=self.__node, parent=self.project_view, context=self.context)
self.__editor.show()
self.__editor.raise_()
self.__editor.activateWindow()
def createWindow(self, **kwargs: Any) -> QtWidgets.QWidget:
window = Editor(
node=self.__node, context=self.context, **kwargs)
self.add_cleanup_function(window.cleanup)
return window

47
noisicaa/builtin_nodes/midi_looper/CMakeLists.txt

@ -0,0 +1,47 @@
# @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
add_python_package(
node_description.py
model.py
model_test.py
node_ui.py
node_ui_test.py
processor_test.py
)
build_model(model.desc.pb _model.py noisicaa/builtin_nodes/model.tmpl.py)
py_proto(model.proto)
add_dependencies(noisicaa.builtin_nodes.midi_looper.model.proto model-noisicaa.builtin_nodes.midi_looper)
cpp_proto(model.proto)
py_proto(processor.proto)
cpp_proto(processor.proto)
add_dependencies(noisicaa.builtin_nodes.midi_looper.processor.proto model-noisicaa.builtin_nodes.midi_looper)
add_library(noisicaa-builtin_nodes-midi_looper-processor_messages SHARED processor.pb.cc)
target_link_libraries(noisicaa-builtin_nodes-midi_looper-processor_messages PRIVATE noisicaa-audioproc-public)
add_library(noisicaa-builtin_nodes-midi_looper-processor SHARED processor.cpp model.pb.cc)
target_compile_options(noisicaa-builtin_nodes-midi_looper-processor PRIVATE -fPIC -std=c++11 -Wall -Werror -pedantic -DHAVE_PTHREAD_SPIN_LOCK)
target_link_libraries(noisicaa-builtin_nodes-midi_looper-processor PRIVATE noisicaa-audioproc-public)
target_link_libraries(noisicaa-builtin_nodes-midi_looper-processor PRIVATE noisicaa-host_system)
target_link_libraries(noisicaa-builtin_nodes-midi_looper-processor PRIVATE noisicaa-builtin_nodes-processor_message_registry)
target_link_libraries(noisicaa-builtin_nodes-midi_looper-processor PRIVATE noisicaa-builtin_nodes-midi_looper-processor_messages)

21
noisicaa/builtin_nodes/midi_looper/__init__.py

@ -0,0 +1,21 @@
#!/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

49
noisicaa/builtin_nodes/midi_looper/model.desc.pb

@ -0,0 +1,49 @@
# @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
classes {
name: "MidiLooperPatch"
super_class: "noisicaa.music.model_base.ProjectChild"
proto_ext_name: "midi_looper_patch"
properties {
name: "events"
type: WRAPPED_PROTO_LIST
wrapped_type: "noisicaa.value_types.MidiEvent"
proto_id: 1
}
}
classes {
name: "MidiLooper"
super_class: "noisicaa.music.graph.BaseNode"
proto_ext_name: "midi_looper"
properties {
name: "duration"
type: WRAPPED_PROTO
wrapped_type: "noisicaa.audioproc.MusicalDuration"
proto_id: 1
}
properties {
name: "patches"
type: OBJECT_LIST
obj_type: "MidiLooperPatch"
proto_id: 2
}
}

92
noisicaa/builtin_nodes/midi_looper/model.py

@ -0,0 +1,92 @@
#!/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
import logging
from typing import cast, Any, Iterator, Iterable
from noisicaa import audioproc
from noisicaa import node_db
from noisicaa import value_types
from . import node_description
from . import processor_pb2
from . import _model
logger = logging.getLogger(__name__)
class MidiLooperPatch(_model.MidiLooperPatch):
@property
def midi_looper(self) -> 'MidiLooper':
return cast(MidiLooper, self.parent)
def set_events(self, events: Iterable[value_types.MidiEvent]) -> None:
self.events.clear()
self.events.extend(events)
self.midi_looper.update_spec()
class MidiLooper(_model.MidiLooper):
def create(self, **kwargs: Any) -> None:
super().create(**kwargs)
self.duration = audioproc.MusicalDuration(8, 4)
self.patches.append(self._pool.create(MidiLooperPatch))
def setup(self) -> None:
super().setup()
self.duration_changed.add(lambda _: self.update_spec())
# TODO: this causes a large number of spec updates when the patch is populated (one for each
# event added). It would be better to schedule a single update at the end of the mutation.
self.patches[0].object_changed.add(lambda _: self.update_spec())
def get_initial_parameter_mutations(self) -> Iterator[audioproc.Mutation]:
yield from super().get_initial_parameter_mutations()
yield self.__get_spec_mutation()
def update_spec(self) -> None:
if self.attached_to_project:
self.project.handle_pipeline_mutation(
self.__get_spec_mutation())
def __get_spec_mutation(self) -> audioproc.Mutation:
params = audioproc.NodeParameters()
spec = params.Extensions[processor_pb2.midi_looper_spec]
spec.duration.CopyFrom(self.duration.to_proto())
for event in self.patches[0].events:
pb_event = spec.events.add()
pb_event.CopyFrom(event.to_proto())
return audioproc.Mutation(
set_node_parameters=audioproc.SetNodeParameters(
node_id=self.pipeline_node_id,
parameters=params))
@property
def description(self) -> node_db.NodeDescription:
node_desc = node_db.NodeDescription()
node_desc.CopyFrom(node_description.MidiLooperDescription)
return node_desc
def set_duration(self, duration: audioproc.MusicalDuration) -> None:
self.duration = duration

48
noisicaa/builtin_nodes/midi_looper/model_test.py

@ -0,0 +1,48 @@
#!/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 typing import cast
from noisidev import unittest
from noisidev import unittest_mixins
from noisicaa import audioproc
from . import model
class MidiLooperTest(unittest_mixins.ProjectMixin, unittest.AsyncTestCase):
async def _add_node(self) -> model.MidiLooper:
with self.project.apply_mutations('test'):
return cast(
model.MidiLooper,
self.project.create_node('builtin://midi-looper'))
async def test_add_node(self):
node = await self._add_node()
self.assertIsInstance(node, model.MidiLooper)
async def test_duration(self):
node = await self._add_node()
self.assertEqual(node.duration, audioproc.MusicalDuration(8, 4))
with self.project.apply_mutations('test'):
node.set_duration(audioproc.MusicalDuration(4, 4))
self.assertEqual(node.duration, audioproc.MusicalDuration(4, 4))

49
noisicaa/builtin_nodes/midi_looper/node_description.py

@ -0,0 +1,49 @@
#!/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 noisicaa import node_db
MidiLooperDescription = node_db.NodeDescription(
uri='builtin://midi-looper',
display_name='MIDI Looper',
type=node_db.NodeDescription.PROCESSOR,
node_ui=node_db.NodeUIDescription(
type='builtin://midi-looper',
),
builtin_icon='node-type-builtin',
processor=node_db.ProcessorDescription(
type='builtin://midi-looper',
),
ports=[
node_db.PortDescription(
name='in',
direction=node_db.PortDescription.INPUT,
type=node_db.PortDescription.EVENTS,
),
node_db.PortDescription(
name='out',
direction=node_db.PortDescription.OUTPUT,
type=node_db.PortDescription.EVENTS,
),
]
)

293
noisicaa/builtin_nodes/midi_looper/node_ui.py

@ -0,0 +1,293 @@
#!/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
import enum
import logging
from typing import Any, Dict, List
from PyQt5.QtCore import Qt
from PyQt5 import QtCore
from PyQt5 import QtGui
from PyQt5 import QtWidgets
from noisicaa import core
from noisicaa import audioproc
from noisicaa import music
from noisicaa import value_types
from noisicaa.ui import ui_base
from noisicaa.ui import pianoroll
from noisicaa.ui import slots
from noisicaa.ui.graph import base_node
from noisicaa.builtin_nodes import processor_message_registry_pb2
from . import model
logger = logging.getLogger(__name__)
# Keep this in sync with ProcessorMidiLooper::RecordState in processor.h
class RecordState(enum.IntEnum):
UNSET = 0
OFF = 1
WAITING = 2
RECORDING = 3
class RecordButton(slots.SlotContainer, QtWidgets.QPushButton):
recordState, setRecordState, recordStateChanged = slots.slot(RecordState, 'recordState')
def __init__(self) -> None:
super().__init__()
self.setText("Record")
self.setIcon(QtGui.QIcon.fromTheme('media-record'))
self.__default_bg = self.palette().color(QtGui.QPalette.Button)
self.recordStateChanged.connect(self.__recordStateChanged)
self.__timer = QtCore.QTimer()
self.__timer.setInterval(250)
self.__timer.timeout.connect(self.__blink)
self.__blink_state = False
def __recordStateChanged(self, state: RecordState) -> None:
palette = self.palette()
if state == RecordState.OFF:
self.__timer.stop()
palette.setColor(self.backgroundRole(), self.__default_bg)
elif state == RecordState.WAITING:
self.__timer.start()
self.__blink_state = True
palette.setColor(self.backgroundRole(), QtGui.QColor(0, 255, 0))
elif state == RecordState.RECORDING:
self.__timer.stop()
palette.setColor(self.backgroundRole(), QtGui.QColor(255, 0, 0))
self.setPalette(palette)
def __blink(self) -> None:
self.__blink_state = not self.__blink_state
palette = self.palette()
if self.__blink_state:
palette.setColor(self.backgroundRole(), QtGui.QColor(0, 255, 0))
else:
palette.setColor(self.backgroundRole(), self.__default_bg)
self.setPalette(palette)
class MidiLooperNodeWidget(ui_base.ProjectMixin, core.AutoCleanupMixin, QtWidgets.QWidget):
def __init__(self, node: model.MidiLooper, session_prefix: str, **kwargs: Any) -> None:
super().__init__(**kwargs)
self.__node = node
self.__visible = False
self.__slot_connections = slots.SlotConnectionManager(
session_prefix='midi_looper:%016x:%s' % (self.__node.id, session_prefix),
context=self.context)
self.add_cleanup_function(self.__slot_connections.cleanup)
self.__listeners = core.ListenerMap[str]()
self.add_cleanup_function(self.__listeners.cleanup)
self.__listeners['node-messages'] = self.audioproc_client.node_messages.add(
'%016x' % self.__node.id, self.__nodeMessage)
self.__event_map = [] # type: List[int]
self.__recorded_events = [] # type: List[value_types.MidiEvent]
self.__duration = QtWidgets.QSpinBox()
self.__duration.setObjectName('duration')
self.__duration.setSuffix(" beats")
self.__duration.setKeyboardTracking(False)
self.__duration.setRange(1, 100)
num_beats = self.__node.duration / audioproc.MusicalDuration(1, 4)
assert num_beats.denominator == 1
self.__duration.setValue(num_beats.numerator)
self.__duration.valueChanged.connect(self.__durationEdited)
self.__listeners['duration'] = self.__node.duration_changed.add(self.__durationChanged)
self.__record = RecordButton()
self.__record.clicked.connect(self.__recordClicked)
self.__pianoroll = pianoroll.PianoRoll()
self.__pianoroll.setDuration(self.__node.duration)
for event in self.__node.patches[0].events:
self.__event_map.append(self.__pianoroll.addEvent(event))
self.__listeners['events'] = self.__node.patches[0].events_changed.add(
self.__eventsChanged)
l2 = QtWidgets.QHBoxLayout()
l2.setContentsMargins(0, 0, 0, 0)
l2.addWidget(self.__record)
l2.addWidget(self.__duration)
l2.addStretch(1)
l1 = QtWidgets.QVBoxLayout()
l1.setContentsMargins(0, 0, 0, 0)
l1.addLayout(l2)
l1.addWidget(self.__pianoroll)
self.setLayout(l1)
def showEvent(self, evt: QtGui.QShowEvent) -> None:
if not self.__visible:
self.__pianoroll.connectSlots(self.__slot_connections, 'pianoroll')
self.__visible = True
super().showEvent(evt)
def hideEvent(self, evt: QtGui.QHideEvent) -> None:
if self.__visible:
self.__pianoroll.disconnectSlots(self.__slot_connections, 'pianoroll')
self.__visible = False
super().hideEvent(evt) # type: ignore
def __eventsChanged(self, change: music.PropertyListChange[value_types.MidiEvent]) -> None:
if isinstance(change, music.PropertyListInsert):
event_id = self.__pianoroll.addEvent(change.new_value)
self.__event_map.insert(change.index, event_id)
elif isinstance(change, music.PropertyListDelete):
event_id = self.__event_map.pop(change.index)
self.__pianoroll.removeEvent(event_id)
else:
raise TypeError(type(change))
def __durationChanged(
self, change: music.PropertyValueChange[audioproc.MusicalDuration]) -> None:
num_beats = change.new_value / audioproc.MusicalDuration(1, 4)
assert num_beats.denominator == 1
self.__duration.setValue(num_beats.numerator)
self.__pianoroll.setDuration(change.new_value)
def __durationEdited(self, beats: int) -> None:
duration = audioproc.MusicalDuration(beats, 4)
if duration == self.__node.duration:
return
with self.project.apply_mutations('%s: Change duration' % self.__node.name):
self.__node.set_duration(duration)
def __recordClicked(self) -> None:
msg = audioproc.ProcessorMessage(node_id=self.__node.pipeline_node_id)
pb = msg.Extensions[processor_message_registry_pb2.midi_looper_record]
pb.start = 1
self.call_async(self.project_view.sendNodeMessage(msg))
def __nodeMessage(self, msg: Dict[str, Any]) -> None:
current_position_urid = (
'http://noisicaa.odahoda.de/lv2/processor_midi_looper#current_position')
if current_position_urid in msg:
numerator, denominator = msg[current_position_urid]
current_position = audioproc.MusicalTime(numerator, denominator)
self.__pianoroll.setPlaybackPosition(current_position)
record_state_urid = 'http://noisicaa.odahoda.de/lv2/processor_midi_looper#record_state'
if record_state_urid in msg:
record_state = RecordState(msg[record_state_urid])
self.__record.setRecordState(record_state)
if record_state == RecordState.RECORDING:
self.__recorded_events.clear()
self.__pianoroll.clearEvents()
self.__pianoroll.setUnfinishedNoteMode(
pianoroll.UnfinishedNoteMode.ToPlaybackPosition)
else:
if record_state == RecordState.OFF:
del self.__listeners['events']
with self.project.apply_mutations('%s: Record patch' % self.__node.name):
patch = self.__node.patches[0]
patch.set_events(self.__recorded_events)
self.__recorded_events.clear()
self.__pianoroll.clearEvents()
self.__event_map.clear()
for event in self.__node.patches[0].events:
self.__event_map.append(self.__pianoroll.addEvent(event))
self.__listeners['events'] = self.__node.patches[0].events_changed.add(
self.__eventsChanged)
self.__pianoroll.setUnfinishedNoteMode(pianoroll.UnfinishedNoteMode.ToEnd)
recorded_event_urid = 'http://noisicaa.odahoda.de/lv2/processor_midi_looper#recorded_event'
if recorded_event_urid in msg:
time_numerator, time_denominator, midi, recorded = msg[recorded_event_urid]
time = audioproc.MusicalTime(time_numerator, time_denominator)
if recorded:
event = value_types.MidiEvent(time, midi)
self.__recorded_events.append(event)
self.__pianoroll.addEvent(event)
if midi[0] & 0xf0 == 0x90:
self.__pianoroll.noteOn(midi[1])
elif midi[0] & 0xf0 == 0x80:
self.__pianoroll.noteOff(midi[1])
class MidiLooperNode(base_node.Node):
has_window = True
def __init__(self, *, node: music.BaseNode, **kwargs: Any) -> None:
assert isinstance(node, model.MidiLooper), type(node).__name__
self.__widget = None # type: QtWidgets.QWidget
self.__node = node # type: model.MidiLooper
super().__init__(node=node, **kwargs)
def createBodyWidget(self) -> QtWidgets.QWidget:
assert self.__widget is None
body = MidiLooperNodeWidget(
node=self.__node,
session_prefix='inline',
context=self.context)
self.add_cleanup_function(body.cleanup)
body.setAutoFillBackground(False)
body.setAttribute(Qt.WA_NoSystemBackground, True)
self.__widget = QtWidgets.QScrollArea()
self.__widget.setWidgetResizable(True)
self.__widget.setFrameShape(QtWidgets.QFrame.NoFrame)
self.__widget.setWidget(body)
return self.__widget
def createWindow(self, **kwargs: Any) -> QtWidgets.QWidget:
window = QtWidgets.QDialog(**kwargs)
window.setAttribute(Qt.WA_DeleteOnClose, False)
window.setWindowTitle("MIDI Looper")
body = MidiLooperNodeWidget(
node=self.__node,
session_prefix='window',
context=self.context)
self.add_cleanup_function(body.cleanup)
layout = QtWidgets.QVBoxLayout()
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(body)
window.setLayout(layout)
return window

67
noisicaa/builtin_nodes/midi_looper/node_ui_test.py

@ -0,0 +1,67 @@
#!/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 import QtWidgets
from noisidev import uitest
from noisicaa import audioproc
from . import node_ui
class MidiLooperNodeTest(uitest.ProjectMixin, uitest.UITestCase):
async def setup_testcase(self):
with self.project.apply_mutations('test'):
self.node = self.project.create_node('builtin://midi-looper')
async def test_init(self):
widget = node_ui.MidiLooperNode(node=self.node, context=self.context)
widget.cleanup()
class MidiLooperNodeWidgetTest(uitest.ProjectMixin, uitest.UITestCase):
async def setup_testcase(self):
with self.project.apply_mutations('test'):
self.node = self.project.create_node('builtin://midi-looper')
async def test_init(self):
widget = node_ui.MidiLooperNodeWidget(
node=self.node, session_prefix='test', context=self.context)
widget.cleanup()
async def test_duration(self):
widget = node_ui.MidiLooperNodeWidget(
node=self.node, session_prefix='test', context=self.context)
try:
duration = widget.findChild(QtWidgets.QSpinBox, 'duration')
assert duration is not None
self.assertEqual(
duration.value(), (self.node.duration / audioproc.MusicalDuration(1, 4)).numerator)
with self.project.apply_mutations('test'):
self.node.set_duration(audioproc.MusicalDuration(5, 4))
self.assertEqual(audioproc.MusicalDuration(duration.value(), 4), self.node.duration)
duration.setValue(7)
self.assertEqual(self.node.duration, audioproc.MusicalDuration(7, 4))
finally:
widget.cleanup()

364
noisicaa/builtin_nodes/midi_looper/processor.cpp

@ -0,0 +1,364 @@
/*
* @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
*/
#include <math.h>
#include "noisicaa/audioproc/engine/misc.h"
#include "noisicaa/audioproc/public/musical_time.h"
#include "noisicaa/audioproc/public/engine_notification.pb.h"
#include "noisicaa/audioproc/public/processor_message.pb.h"
#include "noisicaa/audioproc/engine/message_queue.h"
#include "noisicaa/host_system/host_system.h"
#include "noisicaa/builtin_nodes/processor_message_registry.pb.h"
#include "noisicaa/builtin_nodes/midi_looper/processor.h"
#include "noisicaa/builtin_nodes/midi_looper/processor.pb.h"
#include "noisicaa/builtin_nodes/midi_looper/model.pb.h"
namespace noisicaa {
ProcessorMidiLooper::ProcessorMidiLooper(
const string& realm_name, const string& node_id, HostSystem *host_system,
const pb::NodeDescription& desc)
: Processor(
realm_name, node_id, "noisicaa.audioproc.engine.processor.midi_looper", host_system, desc),
_next_spec(nullptr),
_current_spec(nullptr),
_old_spec(nullptr) {
_current_position_urid = _host_system->lv2->map(
"http://noisicaa.odahoda.de/lv2/processor_midi_looper#current_position");
_record_state_urid = _host_system->lv2->map(
"http://noisicaa.odahoda.de/lv2/processor_midi_looper#record_state");
_recorded_event_urid = _host_system->lv2->map(
"http://noisicaa.odahoda.de/lv2/processor_midi_looper#recorded_event");
lv2_atom_forge_init(&_node_msg_forge, &_host_system->lv2->urid_map);
lv2_atom_forge_init(&_out_forge, &_host_system->lv2->urid_map);
}
Status ProcessorMidiLooper::setup_internal() {
RETURN_IF_ERROR(Processor::setup_internal());
_buffers.resize(_desc.ports_size());
_next_record_state.store(UNSET);
_record_state = OFF;
_recorded_count = 0;
_playback_pos = MusicalTime(-1, 1);
_playback_index = 0;
_last_seen_spec = nullptr;
return Status::Ok();
}
void ProcessorMidiLooper::cleanup_internal() {
pb::MidiLooperSpec* spec = _next_spec.exchange(nullptr);
if (spec != nullptr) {
delete spec;
}
spec = _current_spec.exchange(nullptr);
if (spec != nullptr) {
delete spec;
}
spec = _old_spec.exchange(nullptr);
if (spec != nullptr) {
delete spec;
}
_buffers.clear();
Processor::cleanup_internal();
}
Status ProcessorMidiLooper::handle_message_internal(pb::ProcessorMessage* msg) {
unique_ptr<pb::ProcessorMessage> msg_ptr(msg);
if (msg->HasExtension(pb::midi_looper_record)) {
const pb::MidiLooperRecord& m = msg->GetExtension(pb::midi_looper_record);
if (m.start()) {
_next_record_state.store(WAITING);
}
return Status::Ok();
}
return Processor::handle_message_internal(msg_ptr.release());
}
Status ProcessorMidiLooper::set_parameters_internal(const pb::NodeParameters& parameters) {
if (parameters.HasExtension(pb::midi_looper_spec)) {
const auto& spec = parameters.GetExtension(pb::midi_looper_spec);
Status status = set_spec(spec);
if (status.is_error()) {
_logger->warning("Failed to update spec: %s", status.message());
}
}
return Processor::set_parameters_internal(parameters);
}
Status ProcessorMidiLooper::connect_port_internal(
BlockContext* ctxt, uint32_t port_idx, BufferPtr buf) {
if (port_idx >= _buffers.size()) {
return ERROR_STATUS("Invalid port index %d", port_idx);