Change the primary metaphor to 'modular synth' instead of 'multitrack recorder'.
- tracks are now just special types of nodes in the pipeline graph. - completely reimplemented the pipeline graph UI. - greatly simplified the UI, got rid of all the docks. - some functionality got lost along the way or hasn't been reimplemented yet.looper
parent
f898f8b922
commit
66d7f8c6af
|
@ -5,6 +5,9 @@
|
|||
; - correct indentation for arg list continuation
|
||||
|
||||
((nil . (
|
||||
; Projetile
|
||||
(projectile-project-test-cmd . "bin/runtests")
|
||||
|
||||
; Uses spaces for indentation.
|
||||
(indent-tabs-mode . nil)
|
||||
|
||||
|
|
|
@ -33,6 +33,7 @@ class pyqtSignal:
|
|||
def disconnect(self, slot: typing.Optional[typing.Callable] = 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: ...
|
||||
|
@ -6388,6 +6389,7 @@ class QRectF(sip.simplewrapper):
|
|||
def left(self) -> float: ...
|
||||
def normalized(self) -> 'QRectF': ...
|
||||
def __repr__(self) -> str: ...
|
||||
def __ior__(self, o: 'QRectF') -> 'QRectF': ...
|
||||
|
||||
|
||||
class QRegExp(sip.simplewrapper):
|
||||
|
|
|
@ -5664,6 +5664,7 @@ class QLineEdit(QWidget):
|
|||
textChanged = ... # type: PYQT_SIGNAL
|
||||
textEdited = ... # type: PYQT_SIGNAL
|
||||
editingFinished = ... # type: PYQT_SIGNAL
|
||||
returnPressed = ... # type: PYQT_SIGNAL
|
||||
|
||||
class ActionPosition(int): ...
|
||||
LeadingPosition = ... # type: 'QLineEdit.ActionPosition'
|
||||
|
@ -5723,7 +5724,6 @@ class QLineEdit(QWidget):
|
|||
def mousePressEvent(self, a0: QtGui.QMouseEvent) -> None: ...
|
||||
def initStyleOption(self, option: 'QStyleOptionFrame') -> None: ...
|
||||
def selectionChanged(self) -> None: ...
|
||||
def returnPressed(self) -> None: ...
|
||||
def cursorPositionChanged(self, a0: int, a1: int) -> None: ...
|
||||
def createStandardContextMenu(self) -> 'QMenu': ...
|
||||
def insert(self, a0: str) -> None: ...
|
||||
|
@ -5922,6 +5922,7 @@ class QListWidgetItem(sip.wrapper):
|
|||
|
||||
|
||||
class QListWidget(QListView):
|
||||
itemDoubleClicked = ... # type: PYQT_SIGNAL
|
||||
|
||||
def __init__(self, parent: typing.Optional[QWidget] = ...) -> None: ...
|
||||
|
||||
|
@ -5945,7 +5946,6 @@ class QListWidget(QListView):
|
|||
def itemChanged(self, item: QListWidgetItem) -> None: ...
|
||||
def itemEntered(self, item: QListWidgetItem) -> None: ...
|
||||
def itemActivated(self, item: QListWidgetItem) -> None: ...
|
||||
def itemDoubleClicked(self, item: QListWidgetItem) -> None: ...
|
||||
def itemClicked(self, item: QListWidgetItem) -> None: ...
|
||||
def itemPressed(self, item: QListWidgetItem) -> None: ...
|
||||
def scrollToItem(self, item: QListWidgetItem, hint: QAbstractItemView.ScrollHint = ...) -> None: ...
|
||||
|
|
|
@ -25,8 +25,10 @@
|
|||
<csound>
|
||||
<display-name>Butterworth Band-pass Filter</display-name>
|
||||
<ports>
|
||||
<port name="in" type="audio" direction="input"/>
|
||||
<port name="out" type="audio" direction="output"/>
|
||||
<port name="in/left" type="audio" direction="input"/>
|
||||
<port name="in/right" type="audio" direction="input"/>
|
||||
<port name="out/left" type="audio" direction="output"/>
|
||||
<port name="out/right" type="audio" direction="output"/>
|
||||
<port name="center" type="kratecontrol" direction="input">
|
||||
<float-control min="0" max="20000" default="2000"/>
|
||||
<display-name>Center frequency</display-name>
|
||||
|
|
|
@ -25,8 +25,10 @@
|
|||
<csound>
|
||||
<display-name>Butterworth Band-reject Filter</display-name>
|
||||
<ports>
|
||||
<port name="in" type="audio" direction="input"/>
|
||||
<port name="out" type="audio" direction="output"/>
|
||||
<port name="in/left" type="audio" direction="input"/>
|
||||
<port name="in/right" type="audio" direction="input"/>
|
||||
<port name="out/left" type="audio" direction="output"/>
|
||||
<port name="out/right" type="audio" direction="output"/>
|
||||
<port name="center" type="kratecontrol" direction="input">
|
||||
<float-control min="0" max="20000" default="2000"/>
|
||||
<display-name>Center frequency</display-name>
|
||||
|
|
|
@ -25,8 +25,10 @@
|
|||
<csound>
|
||||
<display-name>Butterworth High-pass Filter</display-name>
|
||||
<ports>
|
||||
<port name="in" type="audio" direction="input"/>
|
||||
<port name="out" type="audio" direction="output"/>
|
||||
<port name="in/left" type="audio" direction="input"/>
|
||||
<port name="in/right" type="audio" direction="input"/>
|
||||
<port name="out/left" type="audio" direction="output"/>
|
||||
<port name="out/right" type="audio" direction="output"/>
|
||||
<port name="cutoff" type="kratecontrol" direction="input">
|
||||
<float-control min="0" max="20000" default="2000"/>
|
||||
<display-name>Cutoff frequency</display-name>
|
||||
|
|
|
@ -25,8 +25,10 @@
|
|||
<csound>
|
||||
<display-name>Butterworth Low-pass Filter</display-name>
|
||||
<ports>
|
||||
<port name="in" type="audio" direction="input"/>
|
||||
<port name="out" type="audio" direction="output"/>
|
||||
<port name="in/left" type="audio" direction="input"/>
|
||||
<port name="in/right" type="audio" direction="input"/>
|
||||
<port name="out/left" type="audio" direction="output"/>
|
||||
<port name="out/right" type="audio" direction="output"/>
|
||||
<port name="cutoff" type="kratecontrol" direction="input">
|
||||
<float-control min="0" max="20000" default="2000"/>
|
||||
<display-name>Cutoff frequency</display-name>
|
||||
|
|
6
listdeps
6
listdeps
|
@ -81,8 +81,8 @@ PIP_DEPS = {
|
|||
PKG('mox3'),
|
||||
PKG('py-cpuinfo'),
|
||||
PKG('pyfakefs'),
|
||||
PKG('pylint'),
|
||||
PKG('mypy'),
|
||||
PKG('pylint==1.9.3'),
|
||||
PKG('mypy==0.610'),
|
||||
PKG('mypy-extensions'),
|
||||
],
|
||||
'vmtests': [
|
||||
|
@ -127,13 +127,11 @@ SYS_DEPS = {
|
|||
PKG('libboost-dev', 'ubuntu', '<17.10'),
|
||||
PKG('flex', 'ubuntu', '<17.10'),
|
||||
PKG('bison', 'ubuntu', '<17.10'),
|
||||
PKG('cmake', 'ubuntu', '<17.10'),
|
||||
PKG('csound', 'ubuntu', '>=17.10'),
|
||||
PKG('libcsound64-dev', 'ubuntu', '>=17.10'),
|
||||
|
||||
# capnp
|
||||
PKG('capnproto'),
|
||||
PKG('libcapnp-0.5.3'),
|
||||
PKG('libcapnp-dev'),
|
||||
|
||||
# protocol buffers
|
||||
|
|
|
@ -49,7 +49,7 @@ class ProcessorCVGeneratorTest(
|
|||
def setup_testcase(self):
|
||||
self.host_system.set_block_size(4096)
|
||||
|
||||
plugin_uri = 'builtin://cvgenerator'
|
||||
plugin_uri = 'builtin://control_track'
|
||||
node_description = self.node_db[plugin_uri]
|
||||
|
||||
self.proc = processor.PyProcessor('test_node', self.host_system, node_description)
|
||||
|
|
|
@ -47,7 +47,7 @@ class ProcessorPianoRollTestMixin(
|
|||
def setup_testcase(self):
|
||||
self.host_system.set_block_size(2 * 44100)
|
||||
|
||||
plugin_uri = 'builtin://pianoroll'
|
||||
plugin_uri = 'builtin://score_track'
|
||||
node_description = self.node_db[plugin_uri]
|
||||
|
||||
self.proc = processor.PyProcessor('test_node', self.host_system, node_description)
|
||||
|
|
|
@ -55,7 +55,7 @@ class ProcessorSampleScriptTest(
|
|||
def setup_testcase(self):
|
||||
self.host_system.set_block_size(4096)
|
||||
|
||||
plugin_uri = 'builtin://sample_script'
|
||||
plugin_uri = 'builtin://sample_track'
|
||||
node_description = self.node_db[plugin_uri]
|
||||
|
||||
self.proc = processor.PyProcessor('test_node', self.host_system, node_description)
|
||||
|
|
|
@ -22,11 +22,13 @@ add_python_package(
|
|||
model_base.py
|
||||
model_base_test.py
|
||||
clef.py
|
||||
color.py
|
||||
key_signature.py
|
||||
key_signature_test.py
|
||||
pos2f.py
|
||||
pitch.py
|
||||
pitch_test.py
|
||||
sizef.py
|
||||
time_signature.py
|
||||
time_signature_test.py
|
||||
project.py
|
||||
|
|
|
@ -39,6 +39,8 @@ from .time_signature import TimeSignature
|
|||
from .clef import Clef
|
||||
from .pitch import Pitch, NOTE_TO_MIDI
|
||||
from .pos2f import Pos2F
|
||||
from .sizef import SizeF
|
||||
from .color import Color
|
||||
from .project import (
|
||||
ObjectBase,
|
||||
ProjectChild,
|
||||
|
@ -47,32 +49,24 @@ from .project import (
|
|||
Beat,
|
||||
BeatMeasure,
|
||||
BeatTrack,
|
||||
CVGeneratorPipelineGraphNode,
|
||||
ControlPoint,
|
||||
ControlTrack,
|
||||
InstrumentPipelineGraphNode,
|
||||
MasterTrackGroup,
|
||||
Measure,
|
||||
MeasureReference,
|
||||
MeasuredTrack,
|
||||
Metadata,
|
||||
Note,
|
||||
PianoRollPipelineGraphNode,
|
||||
PipelineGraphConnection,
|
||||
PipelineGraphControlValue,
|
||||
PipelineGraphNode,
|
||||
Project,
|
||||
PropertyMeasure,
|
||||
PropertyTrack,
|
||||
Sample,
|
||||
SampleRef,
|
||||
SampleScriptPipelineGraphNode,
|
||||
SampleTrack,
|
||||
ScoreMeasure,
|
||||
ScoreTrack,
|
||||
Track,
|
||||
TrackGroup,
|
||||
TrackMixerPipelineGraphNode,
|
||||
)
|
||||
from .model_base_pb2 import (
|
||||
ObjectTree,
|
||||
|
|
|
@ -0,0 +1,75 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
# @begin:license
|
||||
#
|
||||
# Copyright (c) 2015-2018, 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 decimal
|
||||
from google.protobuf import message as protobuf
|
||||
|
||||
from . import project_pb2
|
||||
from . import model_base
|
||||
|
||||
|
||||
class Color(model_base.ProtoValue):
|
||||
def __init__(self, r: float, g: float, b: float, a: float = 1.0) -> None:
|
||||
self.__context = decimal.Context(prec=4)
|
||||
self.__r = self.__context.create_decimal_from_float(r)
|
||||
self.__g = self.__context.create_decimal_from_float(g)
|
||||
self.__b = self.__context.create_decimal_from_float(b)
|
||||
self.__a = self.__context.create_decimal_from_float(a)
|
||||
|
||||
def to_proto(self) -> project_pb2.Color:
|
||||
return project_pb2.Color(r=self.__r, g=self.__g, b=self.__b, a=self.__a)
|
||||
|
||||
@classmethod
|
||||
def from_proto(cls, pb: protobuf.Message) -> 'Color':
|
||||
if not isinstance(pb, project_pb2.Color):
|
||||
raise TypeError(type(pb).__name__)
|
||||
return Color(pb.r, pb.g, pb.b, pb.a)
|
||||
|
||||
@property
|
||||
def r(self) -> float:
|
||||
return float(self.__r)
|
||||
|
||||
@property
|
||||
def g(self) -> float:
|
||||
return float(self.__g)
|
||||
|
||||
@property
|
||||
def b(self) -> float:
|
||||
return float(self.__b)
|
||||
|
||||
@property
|
||||
def a(self) -> float:
|
||||
return float(self.__a)
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
if not isinstance(other, Color):
|
||||
return False
|
||||
|
||||
return (
|
||||
self.__r == other.__r
|
||||
and self.__g == other.__g
|
||||
and self.__b == other.__b
|
||||
and self.__a == other.__a)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return 'Color(%s, %s, %s, %s)' % (self.__r, self.__g, self.__b, self.__a)
|
||||
__repr__ = __str__
|
|
@ -28,7 +28,7 @@ message ObjectBase {
|
|||
required string type = 1;
|
||||
required uint64 id = 2;
|
||||
|
||||
extensions 1000 to max;
|
||||
extensions 100000 to max;
|
||||
}
|
||||
|
||||
message ObjectTree {
|
||||
|
|
|
@ -654,19 +654,30 @@ class ObjectBase(object):
|
|||
# Do not complain about 'id' arguments.
|
||||
# pylint: disable=redefined-builtin
|
||||
|
||||
class Spec(ObjectSpec):
|
||||
class ObjectBaseSpec(ObjectSpec):
|
||||
id = Property(int)
|
||||
|
||||
@classmethod
|
||||
def get_spec(cls) -> Type[ObjectSpec]:
|
||||
for spec in cls.__dict__.values():
|
||||
if isinstance(spec, type) and issubclass(spec, ObjectSpec):
|
||||
return spec
|
||||
|
||||
return None
|
||||
|
||||
def __init__(self, *, pb: model_base_pb2.ObjectBase, pool: AbstractPool) -> None:
|
||||
self.__proto = pb
|
||||
self._pool = pool
|
||||
|
||||
self.__properties = {} # type: Dict[str, PropertyBase]
|
||||
for cls in self.__class__.__mro__:
|
||||
if not issubclass(cls, ObjectBase) or 'Spec' not in cls.__dict__:
|
||||
if not issubclass(cls, ObjectBase):
|
||||
continue # pragma: no coverage
|
||||
spec = cls.__dict__['Spec']
|
||||
assert isinstance(spec, type) and issubclass(spec, ObjectSpec)
|
||||
|
||||
spec = cast(Type[ObjectBase], cls).get_spec()
|
||||
if spec is None:
|
||||
continue
|
||||
|
||||
for prop_name, prop in spec.__dict__.items():
|
||||
if isinstance(prop, PropertyBase):
|
||||
self.__properties[prop_name] = prop
|
||||
|
@ -699,7 +710,7 @@ class ObjectBase(object):
|
|||
|
||||
@property
|
||||
def id(self) -> int:
|
||||
return ObjectBase.Spec.id.get_value(self, self.__proto, self._pool)
|
||||
return ObjectBase.ObjectBaseSpec.id.get_value(self, self.__proto, self._pool)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return '<%s id=%s>' % (type(self).__name__, self.id)
|
||||
|
@ -851,10 +862,27 @@ class Pool(Generic[POOLOBJECTBASE], AbstractPool[POOLOBJECTBASE]):
|
|||
self.__obj_map = {} # type: Dict[int, POOLOBJECTBASE]
|
||||
self.__class_map = {} # type: Dict[str, Type[POOLOBJECTBASE]]
|
||||
|
||||
def __get_proto_type(self, cls: Type) -> str:
|
||||
proto_type = None
|
||||
for c in cls.__mro__:
|
||||
if not issubclass(c, ObjectBase):
|
||||
continue # pragma: no coverage
|
||||
|
||||
spec = cast(Type[ObjectBase], c).get_spec()
|
||||
if spec is None:
|
||||
continue
|
||||
|
||||
if spec.proto_type is not None:
|
||||
assert proto_type is None, (cls.__name__, c.__name__)
|
||||
proto_type = spec.proto_type
|
||||
|
||||
return proto_type
|
||||
|
||||
def register_class(self, cls: Type[POOLOBJECTBASE]) -> None:
|
||||
assert cls.Spec.proto_type is not None
|
||||
assert cls.Spec.proto_type not in self.__class_map
|
||||
self.__class_map[cls.Spec.proto_type] = cls
|
||||
proto_type = self.__get_proto_type(cls)
|
||||
assert proto_type is not None, cls.__name__
|
||||
assert proto_type not in self.__class_map
|
||||
self.__class_map[proto_type] = cls
|
||||
|
||||
def object_added(self, obj: POOLOBJECTBASE) -> None:
|
||||
pass
|
||||
|
@ -902,10 +930,11 @@ class Pool(Generic[POOLOBJECTBASE], AbstractPool[POOLOBJECTBASE]):
|
|||
|
||||
def create(
|
||||
self, cls: Type[OBJECT], id: Optional[int] = None, **kwargs: Any) -> OBJECT:
|
||||
assert cls.Spec.proto_type in self.__class_map, cls.__name__
|
||||
proto_type = self.__get_proto_type(cls)
|
||||
assert proto_type in self.__class_map, cls.__name__
|
||||
if id is None:
|
||||
id = random.getrandbits(64)
|
||||
pb = model_base_pb2.ObjectBase(id=id, type=cls.Spec.proto_type)
|
||||
pb = model_base_pb2.ObjectBase(id=id, type=proto_type)
|
||||
obj = cast(POOLOBJECTBASE, cls(pb=pb, pool=self))
|
||||
self.__obj_map[id] = obj
|
||||
obj.create(**kwargs) # type: ignore
|
||||
|
|
|
@ -31,14 +31,14 @@ message Proto {
|
|||
optional int32 b = 2;
|
||||
}
|
||||
|
||||
message GrandChild {
|
||||
}
|
||||
|
||||
message Child {
|
||||
optional uint64 child = 1;
|
||||
optional string value = 2;
|
||||
}
|
||||
|
||||
message GrandChild {
|
||||
}
|
||||
|
||||
message Root {
|
||||
optional string string_value = 1;
|
||||
optional int64 int_value = 2;
|
||||
|
@ -57,6 +57,6 @@ message Root {
|
|||
}
|
||||
|
||||
extend ObjectBase {
|
||||
optional Root root = 2000;
|
||||
optional Child child = 2001;
|
||||
optional Root root = 1000000;
|
||||
optional Child child = 1000001;
|
||||
}
|
||||
|
|
|
@ -66,12 +66,12 @@ class Proto(model_base.ProtoValue):
|
|||
|
||||
|
||||
class GrandChild(model_base.ObjectBase):
|
||||
class Spec(model_base.ObjectSpec):
|
||||
class GrandChildSpec(model_base.ObjectSpec):
|
||||
proto_type = 'grand_child'
|
||||
|
||||
|
||||
class Child(model_base.ObjectBase):
|
||||
class Spec(model_base.ObjectSpec):
|
||||
class ChildSpec(model_base.ObjectSpec):
|
||||
proto_type = 'child'
|
||||
proto_ext = model_base_test_pb2.child # type: ignore
|
||||
child = model_base.ObjectProperty(GrandChild)
|
||||
|
@ -103,7 +103,7 @@ class Child(model_base.ObjectBase):
|
|||
|
||||
|
||||
class Root(model_base.ObjectBase):
|
||||
class Spec(model_base.ObjectSpec):
|
||||
class RootSpec(model_base.ObjectSpec):
|
||||
proto_type = 'root'
|
||||
proto_ext = model_base_test_pb2.root # type: ignore
|
||||
|
||||
|
@ -280,8 +280,8 @@ class ObjectTest(unittest.TestCase):
|
|||
obj.setup()
|
||||
obj.setup_complete()
|
||||
|
||||
self.assertIs(obj.get_property('id'), model_base.ObjectBase.Spec.id)
|
||||
self.assertIs(obj.get_property('string_value'), Root.Spec.string_value)
|
||||
self.assertIs(obj.get_property('id'), model_base.ObjectBase.ObjectBaseSpec.id)
|
||||
self.assertIs(obj.get_property('string_value'), Root.RootSpec.string_value)
|
||||
|
||||
with self.assertRaises(AttributeError):
|
||||
obj.get_property('does_not_exist')
|
||||
|
|
|
@ -29,17 +29,12 @@ import "noisicaa/audioproc/public/plugin_state.proto";
|
|||
import "noisicaa/model/model_base.proto";
|
||||
|
||||
message Track {
|
||||
optional string name = 1;
|
||||
|
||||
optional bool visible = 2;
|
||||
optional bool muted = 3;
|
||||
optional float gain = 4;
|
||||
optional float pan = 5;
|
||||
|
||||
optional uint64 mixer_node = 6;
|
||||
optional bool visible = 1;
|
||||
optional uint32 list_position = 2;
|
||||
}
|
||||
|
||||
message Measure {
|
||||
optional TimeSignature time_signature = 1;
|
||||
}
|
||||
|
||||
message MeasureReference {
|
||||
|
@ -58,13 +53,6 @@ message Note {
|
|||
optional uint32 tuplet = 4;
|
||||
}
|
||||
|
||||
message TrackGroup {
|
||||
repeated uint64 tracks = 1;
|
||||
}
|
||||
|
||||
message MasterTrackGroup {
|
||||
}
|
||||
|
||||
message ScoreMeasure {
|
||||
optional Clef clef = 1;
|
||||
optional KeySignature key_signature = 2;
|
||||
|
@ -72,11 +60,7 @@ message ScoreMeasure {
|
|||
}
|
||||
|
||||
message ScoreTrack {
|
||||
optional string instrument = 1;
|
||||
optional int32 transpose_octaves = 2;
|
||||
|
||||
optional uint64 instrument_node = 3;
|
||||
optional uint64 event_source_node = 4;
|
||||
optional int32 transpose_octaves = 1;
|
||||
}
|
||||
|
||||
message Beat {
|
||||
|
@ -89,18 +73,7 @@ message BeatMeasure {
|
|||
}
|
||||
|
||||
message BeatTrack {
|
||||
optional string instrument = 1;
|
||||
optional Pitch pitch = 2;
|
||||
|
||||
optional uint64 instrument_node = 3;
|
||||
optional uint64 event_source_node = 4;
|
||||
}
|
||||
|
||||
message PropertyMeasure {
|
||||
optional TimeSignature time_signature = 1;
|
||||
}
|
||||
|
||||
message PropertyTrack {
|
||||
optional Pitch pitch = 1;
|
||||
}
|
||||
|
||||
message ControlPoint {
|
||||
|
@ -110,8 +83,7 @@ message ControlPoint {
|
|||
|
||||
message ControlTrack {
|
||||
repeated uint64 points = 1;
|
||||
optional uint64 generator_node = 2;
|
||||
}
|
||||
}
|
||||
|
||||
message SampleRef {
|
||||
optional MusicalTime time = 1;
|
||||
|
@ -120,7 +92,6 @@ message SampleRef {
|
|||
|
||||
message SampleTrack {
|
||||
repeated uint64 samples = 1;
|
||||
optional uint64 sample_script_node = 2;
|
||||
}
|
||||
|
||||
message PipelineGraphControlValue {
|
||||
|
@ -131,6 +102,8 @@ message PipelineGraphControlValue {
|
|||
message BasePipelineGraphNode {
|
||||
optional string name = 1;
|
||||
optional Pos2F graph_pos = 2;
|
||||
optional SizeF graph_size = 5;
|
||||
optional Color graph_color = 6;
|
||||
repeated uint64 control_values = 3;
|
||||
optional PluginState plugin_state = 4;
|
||||
}
|
||||
|
@ -142,24 +115,8 @@ message PipelineGraphNode {
|
|||
message AudioOutPipelineGraphNode {
|
||||
}
|
||||
|
||||
message TrackMixerPipelineGraphNode {
|
||||
optional uint64 track = 1;
|
||||
}
|
||||
|
||||
message PianoRollPipelineGraphNode {
|
||||
optional uint64 track = 1;
|
||||
}
|
||||
|
||||
message CVGeneratorPipelineGraphNode {
|
||||
optional uint64 track = 1;
|
||||
}
|
||||
|
||||
message SampleScriptPipelineGraphNode {
|
||||
optional uint64 track = 1;
|
||||
}
|
||||
|
||||
message InstrumentPipelineGraphNode {
|
||||
optional uint64 track = 1;
|
||||
optional string instrument_uri = 1;
|
||||
}
|
||||
|
||||
message PipelineGraphConnection {
|
||||
|
@ -171,7 +128,7 @@ message PipelineGraphConnection {
|
|||
|
||||
message Sample {
|
||||
optional string path = 1;
|
||||
}
|
||||
}
|
||||
|
||||
message Metadata {
|
||||
optional string author = 1;
|
||||
|
@ -182,46 +139,52 @@ message Metadata {
|
|||
|
||||
message Project {
|
||||
optional uint64 metadata = 1;
|
||||
optional uint64 master_group = 2;
|
||||
optional uint64 property_track = 3;
|
||||
repeated uint64 pipeline_graph_nodes = 4;
|
||||
repeated uint64 pipeline_graph_connections = 5;
|
||||
repeated uint64 samples = 6;
|
||||
optional uint32 bpm = 7;
|
||||
optional uint32 bpm = 2;
|
||||
repeated uint64 pipeline_graph_nodes = 3;
|
||||
repeated uint64 pipeline_graph_connections = 4;
|
||||
repeated uint64 samples = 5;
|
||||
}
|
||||
|
||||
extend ObjectBase {
|
||||
optional Track track = 1000;
|
||||
optional Measure measure = 1001;
|
||||
optional MeasureReference measure_reference = 1002;
|
||||
optional MeasuredTrack measured_track = 1003;
|
||||
optional Note note = 1004;
|
||||
optional TrackGroup track_group = 1005;
|
||||
optional MasterTrackGroup master_track_group = 1006;
|
||||
optional ScoreMeasure score_measure = 1007;
|
||||
optional ScoreTrack score_track = 1008;
|
||||
optional Beat beat = 1009;
|
||||
optional BeatMeasure beat_measure = 1010;
|
||||
optional BeatTrack beat_track = 1011;
|
||||
optional PropertyMeasure property_measure = 1012;
|
||||
optional PropertyTrack property_track = 1013;
|
||||
optional ControlPoint control_point = 1014;
|
||||
optional ControlTrack control_track = 1015;
|
||||
optional SampleRef sample_ref = 1016;
|
||||
optional SampleTrack sample_track = 1017;
|
||||
optional PipelineGraphControlValue pipeline_graph_control_value = 1018;
|
||||
optional BasePipelineGraphNode base_pipeline_graph_node = 1019;
|
||||
optional PipelineGraphNode pipeline_graph_node = 1020;
|
||||
optional AudioOutPipelineGraphNode audio_out_pipeline_graph_node = 1021;
|
||||
optional TrackMixerPipelineGraphNode track_mixer_pipeline_graph_node = 1022;
|
||||
optional PianoRollPipelineGraphNode pianoroll_pipeline_graph_node = 1023;
|
||||
optional CVGeneratorPipelineGraphNode cvgenerator_pipeline_graph_node = 1024;
|
||||
optional SampleScriptPipelineGraphNode sample_script_pipeline_graph_node = 1025;
|
||||
optional InstrumentPipelineGraphNode instrument_pipeline_graph_node = 1026;
|
||||
optional PipelineGraphConnection pipeline_graph_connection = 1027;
|
||||
optional Sample sample = 1028;
|
||||
optional Metadata metadata = 1029;
|
||||
optional Project project = 1030;
|
||||
// Project (1xxxxx)
|
||||
optional Project project = 100000;
|
||||
optional Metadata metadata = 100001;
|
||||
|
||||
// Samples (2xxxxx)
|
||||
optional Sample sample = 200000;
|
||||
|
||||
// Pipeline graph (3xxxxx)
|
||||
optional PipelineGraphControlValue pipeline_graph_control_value = 300000;
|
||||
optional PipelineGraphConnection pipeline_graph_connection = 300001;
|
||||
|
||||
optional BasePipelineGraphNode base_pipeline_graph_node = 301000;
|
||||
optional PipelineGraphNode pipeline_graph_node = 302000;
|
||||
optional AudioOutPipelineGraphNode audio_out_pipeline_graph_node = 303000;
|
||||
optional InstrumentPipelineGraphNode instrument_pipeline_graph_node = 304000;
|
||||
|
||||
// Tracks (4xxxxx)
|
||||
optional Track track = 400000;
|
||||
optional MeasuredTrack measured_track = 400100;
|
||||
optional Measure measure = 400101;
|
||||
optional MeasureReference measure_reference = 400102;
|
||||
|
||||
// Score track (401xxx)
|
||||
optional ScoreTrack score_track = 401000;
|
||||
optional ScoreMeasure score_measure = 401001;
|
||||
optional Note note = 401002;
|
||||
|
||||
// Beat track (402xxx)
|
||||
optional BeatTrack beat_track = 402000;
|
||||
optional BeatMeasure beat_measure = 402001;
|
||||
optional Beat beat = 402002;
|
||||
|
||||
// Control track (403xxx)
|
||||
optional ControlTrack control_track = 403000;
|
||||
optional ControlPoint control_point = 403001;
|
||||
|
||||
// Sample track (404xxx)
|
||||
optional SampleTrack sample_track = 404000;
|
||||
optional SampleRef sample_ref = 404001;
|
||||
}
|
||||
|
||||
message Pitch {
|
||||
|
@ -258,6 +221,18 @@ message Pos2F {
|
|||
required float y = 2;
|
||||
}
|
||||
|
||||
message SizeF {
|
||||
required float width = 1;
|
||||
required float height = 2;
|
||||
}
|
||||
|
||||
message Color {
|
||||
required float r = 1;
|
||||
required float g = 2;
|
||||
required float b = 3;
|
||||
required float a = 4 [default=1.0];
|
||||
}
|
||||
|
||||
message ControlValue {
|
||||
optional float value = 1;
|
||||
optional uint64 generation = 2;
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
|
||||
import fractions
|
||||
import logging
|
||||
from typing import cast, Any, Dict, Iterator, Sequence, List, Union # pylint: disable=unused-import
|
||||
from typing import cast, Any, Dict, Set, Iterator, Sequence, List, Union # pylint: disable=unused-import
|
||||
|
||||
from google.protobuf import message as protobuf # pylint: disable=unused-import
|
||||
|
||||
|
@ -37,6 +37,8 @@ from . import clef as clef_lib
|
|||
from . import key_signature as key_signature_lib
|
||||
from . import time_signature as time_signature_lib
|
||||
from . import pos2f
|
||||
from . import sizef
|
||||
from . import color
|
||||
from . import model_base
|
||||
from . import project_pb2
|
||||
|
||||
|
@ -76,7 +78,7 @@ class ProjectChild(ObjectBase):
|
|||
|
||||
|
||||
class Sample(ProjectChild):
|
||||
class Spec(model_base.ObjectSpec):
|
||||
class SampleSpec(model_base.ObjectSpec):
|
||||
proto_type = 'sample'
|
||||
proto_ext = project_pb2.sample # type: ignore
|
||||
|
||||
|
@ -89,7 +91,7 @@ class Sample(ProjectChild):
|
|||
|
||||
|
||||
class PipelineGraphControlValue(ProjectChild):
|
||||
class Spec(model_base.ObjectSpec):
|
||||
class PipelineGraphControlValueSpec(model_base.ObjectSpec):
|
||||
proto_type = 'pipeline_graph_control_value'
|
||||
proto_ext = project_pb2.pipeline_graph_control_value # type: ignore
|
||||
|
||||
|
@ -104,11 +106,14 @@ class PipelineGraphControlValue(ProjectChild):
|
|||
|
||||
|
||||
class BasePipelineGraphNode(ProjectChild):
|
||||
class Spec(model_base.ObjectSpec):
|
||||
class BasePipelineGraphNodeSpec(model_base.ObjectSpec):
|
||||
proto_ext = project_pb2.base_pipeline_graph_node # type: ignore
|
||||
|
||||
name = model_base.Property(str)
|
||||
graph_pos = model_base.WrappedProtoProperty(pos2f.Pos2F)
|
||||
graph_size = model_base.WrappedProtoProperty(sizef.SizeF)
|
||||
graph_color = model_base.WrappedProtoProperty(
|
||||
color.Color, default=color.Color(0.8, 0.8, 0.8, 1.0))
|
||||
control_values = model_base.ObjectListProperty(PipelineGraphControlValue)
|
||||
plugin_state = model_base.ProtoProperty(audioproc.PluginState, allow_none=True)
|
||||
|
||||
|
@ -117,6 +122,8 @@ class BasePipelineGraphNode(ProjectChild):
|
|||
|
||||
self.name_changed = core.Callback[model_base.PropertyChange[str]]()
|
||||
self.graph_pos_changed = core.Callback[model_base.PropertyChange[pos2f.Pos2F]]()
|
||||
self.graph_size_changed = core.Callback[model_base.PropertyChange[sizef.SizeF]]()
|
||||
self.graph_color_changed = core.Callback[model_base.PropertyChange[color.Color]]()
|
||||
self.control_values_changed = \
|
||||
core.Callback[model_base.PropertyListChange[PipelineGraphControlValue]]()
|
||||
self.plugin_state_changed = \
|
||||
|
@ -124,62 +131,140 @@ class BasePipelineGraphNode(ProjectChild):
|
|||
|
||||
@property
|
||||
def removable(self) -> bool:
|
||||
raise NotImplementedError
|
||||
return True
|
||||
|
||||
@property
|
||||
def description(self) -> node_db.NodeDescription:
|
||||
raise NotImplementedError
|
||||
|
||||
def upstream_nodes(self) -> List['BasePipelineGraphNode']:
|
||||
node_ids = set() # type: Set[int]
|
||||
self.__upstream_nodes(node_ids)
|
||||
return [self._pool[node_id] for node_id in sorted(node_ids)]
|
||||
|
||||
class Track(ProjectChild):
|
||||
class Spec(model_base.ObjectSpec):
|
||||
proto_ext = project_pb2.track # type: ignore
|
||||
def __upstream_nodes(self, seen: Set[int]) -> None:
|
||||
for connection in self.project.get_property_value('pipeline_graph_connections'):
|
||||
if connection.dest_node is self and connection.source_node.id not in seen:
|
||||
seen.add(connection.source_node.id)
|
||||
connection.source_node.__upstream_nodes(seen)
|
||||
|
||||
name = model_base.Property(str)
|
||||
visible = model_base.Property(bool, default=True)
|
||||
muted = model_base.Property(bool, default=False)
|
||||
gain = model_base.Property(float, default=0.0)
|
||||
pan = model_base.Property(float, default=0.0)
|
||||
mixer_node = model_base.ObjectReferenceProperty(BasePipelineGraphNode, allow_none=True)
|
||||
|
||||
class PipelineGraphConnection(ProjectChild):
|
||||
class PipelineGraphConnectionSpec(model_base.ObjectSpec):
|
||||
proto_type = 'pipeline_graph_connection'
|
||||
proto_ext = project_pb2.pipeline_graph_connection # type: ignore
|
||||
|
||||
source_node = model_base.ObjectReferenceProperty(BasePipelineGraphNode)
|
||||
source_port = model_base.Property(str)
|
||||
dest_node = model_base.ObjectReferenceProperty(BasePipelineGraphNode)
|
||||
dest_port = model_base.Property(str)
|
||||
|
||||
def __init__(self, **kwargs: Any) -> None:
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.name_changed = core.Callback[model_base.PropertyChange[str]]()
|
||||
self.visible_changed = core.Callback[model_base.PropertyChange[bool]]()
|
||||
self.muted_changed = core.Callback[model_base.PropertyChange[bool]]()
|
||||
self.gain_changed = core.Callback[model_base.PropertyChange[float]]()
|
||||
self.pan_changed = core.Callback[model_base.PropertyChange[float]]()
|
||||
self.mixer_node_changed = core.Callback[model_base.PropertyChange[BasePipelineGraphNode]]()
|
||||
self.source_node_changed = core.Callback[model_base.PropertyChange[BasePipelineGraphNode]]()
|
||||
self.source_port_changed = core.Callback[model_base.PropertyChange[str]]()
|
||||
self.dest_node_changed = core.Callback[model_base.PropertyChange[BasePipelineGraphNode]]()
|
||||
self.dest_port_changed = core.Callback[model_base.PropertyChange[str]]()
|
||||
|
||||
|
||||
class PipelineGraphNode(BasePipelineGraphNode):
|
||||
class PipelineGraphNodeSpec(model_base.ObjectSpec):
|
||||
proto_type = 'pipeline_graph_node'
|
||||
proto_ext = project_pb2.pipeline_graph_node # type: ignore
|
||||
|
||||
node_uri = model_base.Property(str)
|
||||
|
||||
def __init__(self, **kwargs: Any) -> None:
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.node_uri_changed = core.Callback[model_base.PropertyChange[str]]()
|
||||
|
||||
@property
|
||||
def node_uri(self) -> str:
|
||||
return self.get_property_value('node_uri')
|
||||
|
||||
@property
|
||||
def description(self) -> node_db.NodeDescription:
|
||||
return self.project.get_node_description(self.node_uri)
|
||||
|
||||
|
||||
class AudioOutPipelineGraphNode(BasePipelineGraphNode):
|
||||
class AudioOutPipelineGraphNodeSpec(model_base.ObjectSpec):
|
||||
proto_type = 'audio_out_pipeline_graph_node'
|
||||
|
||||
@property
|
||||
def removable(self) -> bool:
|
||||
return False
|
||||
|
||||
@property
|
||||
def description(self) -> node_db.NodeDescription:
|
||||
return node_db.Builtins.RealmSinkDescription
|
||||
|
||||
|
||||
class InstrumentPipelineGraphNode(BasePipelineGraphNode):
|
||||
class InstrumentPipelineGraphNodeSpec(model_base.ObjectSpec):
|
||||
proto_type = 'instrument_pipeline_graph_node'
|
||||
proto_ext = project_pb2.instrument_pipeline_graph_node # type: ignore
|
||||
|
||||
instrument_uri = model_base.Property(str)
|
||||
|
||||
def __init__(self, **kwargs: Any) -> None:
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.instrument_uri_changed = core.Callback[model_base.PropertyChange[str]]()
|
||||
|
||||
@property
|
||||
def description(self) -> node_db.NodeDescription:
|
||||
return instrument_db.parse_uri(
|
||||
self.get_property_value('instrument_uri'), self.project.get_node_description)
|
||||
|
||||
|
||||
class Track(BasePipelineGraphNode): # pylint: disable=abstract-method
|
||||
class TrackSpec(model_base.ObjectSpec):
|
||||
proto_ext = project_pb2.track # type: ignore
|
||||
|
||||
visible = model_base.Property(bool, default=True)
|
||||
list_position = model_base.Property(int, default=0)
|
||||
|
||||
def __init__(self, **kwargs: Any) -> None:
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.visible_changed = core.Callback[model_base.PropertyChange[bool]]()
|
||||
self.list_position_changed = core.Callback[model_base.PropertyChange[int]]()
|
||||
self.duration_changed = core.Callback[None]()
|
||||
|
||||
@property
|
||||
def duration(self) -> audioproc.MusicalDuration:
|
||||
return audioproc.MusicalDuration(1, 1)
|
||||
|
||||
@property
|
||||
def is_master_group(self) -> bool:
|
||||
return False
|
||||
|
||||
def walk_tracks(self, groups: bool = False, tracks: bool = True) -> Iterator['Track']:
|
||||
if tracks:
|
||||
yield self
|
||||
|
||||
|
||||
class Measure(ProjectChild):
|
||||
class MeasureSpec(model_base.ObjectSpec):
|
||||
proto_ext = project_pb2.measure # type: ignore
|
||||
|
||||
time_signature = model_base.WrappedProtoProperty(
|
||||
time_signature_lib.TimeSignature,
|
||||
default=time_signature_lib.TimeSignature(4, 4))
|
||||
|
||||
def __init__(self, **kwargs: Any) -> None:
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.time_signature_changed = \
|
||||
core.Callback[model_base.PropertyChange[time_signature_lib.TimeSignature]]()
|
||||
|
||||
@property
|
||||
def track(self) -> Track:
|
||||
return cast(Track, self.parent)
|
||||
|
||||
@property
|
||||
def duration(self) -> audioproc.MusicalDuration:
|
||||
time_signature = self.project.get_time_signature(self.index)
|
||||
time_signature = self.get_property_value('time_signature')
|
||||
return audioproc.MusicalDuration(time_signature.upper, time_signature.lower)
|
||||
|
||||
|
||||
class MeasureReference(ProjectChild):
|
||||
class Spec(model_base.ObjectSpec):
|
||||
class MeasureReferenceSpec(model_base.ObjectSpec):
|
||||
proto_type = 'measure_reference'
|
||||
proto_ext = project_pb2.measure_reference # type: ignore
|
||||
|
||||
|
@ -207,8 +292,8 @@ class MeasureReference(ProjectChild):
|
|||
return down_cast(MeasureReference, super().next_sibling)
|
||||
|
||||
|
||||
class MeasuredTrack(Track):
|
||||
class Spec(model_base.ObjectSpec):
|
||||
class MeasuredTrack(Track): # pylint: disable=abstract-method
|
||||
class MeasuredSpec(model_base.ObjectSpec):
|
||||
proto_ext = project_pb2.measured_track # type: ignore
|
||||
|
||||
measure_list = model_base.ObjectListProperty(MeasureReference)
|
||||
|
@ -263,7 +348,7 @@ class MeasuredTrack(Track):
|
|||
|
||||
|
||||
class Note(ProjectChild):
|
||||
class Spec(model_base.ObjectSpec):
|
||||
class NoteSpec(model_base.ObjectSpec):
|
||||
proto_type = 'note'
|
||||
proto_ext = project_pb2.note # type: ignore
|
||||
|
||||
|
@ -338,205 +423,8 @@ class Note(ProjectChild):
|
|||
self.measure.content_changed.call()
|
||||
|
||||
|
||||
class TrackGroup(Track):
|
||||
class Spec(model_base.ObjectSpec):
|
||||
proto_type = 'track_group'
|
||||
proto_ext = project_pb2.track_group # type: ignore
|
||||
|
||||
tracks = model_base.ObjectListProperty(Track)
|
||||
|
||||
def __init__(self, **kwargs: Any) -> None:
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.__listeners = {} # type: Dict[str, core.Listener]
|
||||
|
||||
self.tracks_changed = core.Callback[model_base.PropertyListChange[Track]]()
|
||||
|
||||
def setup(self) -> None:
|
||||
super().setup()
|
||||
for track in self.tracks:
|
||||
self.__add_track(track)
|
||||
self.tracks_changed.add(self.__tracks_changed)
|
||||
|
||||
def __tracks_changed(self, change: model_base.PropertyListChange) -> None:
|
||||
if isinstance(change, model_base.PropertyListInsert):
|
||||
self.__add_track(change.new_value)
|
||||
elif isinstance(change, model_base.PropertyListDelete):
|
||||
self.__remove_track(change.old_value)
|
||||
else:
|
||||
raise TypeError("Unsupported change type %s" % type(change))
|
||||
|
||||
def __add_track(self, track: Track) -> None:
|
||||
self.__listeners['%s:duration_changed' % track.id] = track.duration_changed.add(
|
||||
self.duration_changed.call)
|
||||
self.duration_changed.call()
|
||||
|
||||
def __remove_track(self, track: Track) -> None:
|
||||
self.__listeners.pop('%s:duration_changed' % track.id).remove()
|
||||
self.duration_changed.call()
|
||||
|
||||
@property
|
||||
def tracks(self) -> Sequence[Track]:
|
||||
return self.get_property_value('tracks')
|
||||
|
||||
@property
|
||||
def duration(self) -> audioproc.MusicalDuration:
|
||||
duration = audioproc.MusicalDuration()
|
||||
for track in self.tracks:
|
||||
duration = max(duration, track.duration)
|
||||
return duration
|
||||
|
||||
def walk_tracks(self, groups: bool = False, tracks: bool = True) -> Iterator[Track]:
|
||||
if groups:
|
||||
yield self
|
||||
|
||||
for track in self.tracks:
|
||||
yield from track.walk_tracks(groups, tracks)
|
||||
|
||||
|
||||
class MasterTrackGroup(TrackGroup):
|
||||
class Spec(model_base.ObjectSpec):
|
||||
proto_type = 'master_track_group'
|
||||
|
||||
@property
|
||||
def is_master_group(self) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
class PropertyMeasure(Measure):
|
||||
class Spec(model_base.ObjectSpec):
|
||||
proto_type = 'property_measure'
|
||||
proto_ext = project_pb2.property_measure # type: ignore
|
||||
|
||||
time_signature = model_base.WrappedProtoProperty(
|
||||
time_signature_lib.TimeSignature,
|
||||
default=time_signature_lib.TimeSignature(4, 4))
|
||||
|
||||
def __init__(self, **kwargs: Any) -> None:
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.time_signature_changed = \
|
||||
core.Callback[model_base.PropertyChange[time_signature_lib.TimeSignature]]()
|
||||
|
||||
@property
|
||||
def time_signature(self) -> time_signature_lib.TimeSignature:
|
||||
return self.get_property_value('time_signature')
|
||||
|
||||
|
||||
class PropertyTrack(MeasuredTrack):
|
||||
class Spec(model_base.ObjectSpec):
|
||||
proto_type = 'property_track'
|
||||
|
||||
|
||||
class PipelineGraphNode(BasePipelineGraphNode):
|
||||
class Spec(model_base.ObjectSpec):
|
||||
proto_type = 'pipeline_graph_node'
|
||||
proto_ext = project_pb2.pipeline_graph_node # type: ignore
|
||||
|
||||
node_uri = model_base.Property(str)
|
||||
|
||||
def __init__(self, **kwargs: Any) -> None:
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.node_uri_changed = core.Callback[model_base.PropertyChange[str]]()
|
||||
|
||||
@property
|
||||
def node_uri(self) -> str:
|
||||
return self.get_property_value('node_uri')
|
||||
|
||||
@property
|
||||
def removable(self) -> bool:
|
||||
return True
|
||||
|
||||
@property
|
||||
def description(self) -> node_db.NodeDescription:
|
||||
return self.project.get_node_description(self.node_uri)
|
||||
|
||||
|
||||
class AudioOutPipelineGraphNode(BasePipelineGraphNode):
|
||||
class Spec(model_base.ObjectSpec):
|
||||
proto_type = 'audio_out_pipeline_graph_node'
|
||||
|
||||
@property
|
||||
def removable(self) -> bool:
|
||||
return False
|
||||
|
||||
@property
|
||||
def description(self) -> node_db.NodeDescription:
|
||||
return node_db.Builtins.RealmSinkDescription
|
||||
|
||||
|
||||
class TrackMixerPipelineGraphNode(BasePipelineGraphNode):
|
||||
class Spec(model_base.ObjectSpec):
|
||||
proto_type = 'track_mixer_pipeline_graph_node'
|
||||
proto_ext = project_pb2.track_mixer_pipeline_graph_node # type: ignore
|
||||
|
||||
track = model_base.ObjectReferenceProperty(Track)
|
||||
|
||||
def __init__(self, **kwargs: Any) -> None:
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.track_changed = core.Callback[model_base.PropertyChange[Track]]()
|
||||
|
||||
@property
|
||||
def removable(self) -> bool:
|
||||
return False
|
||||
|
||||
@property
|
||||
def description(self) -> node_db.NodeDescription:
|
||||
return node_db.Builtins.TrackMixerDescription
|
||||
|
||||
|
||||
class InstrumentPipelineGraphNode(BasePipelineGraphNode):
|
||||
class Spec(model_base.ObjectSpec):
|
||||
proto_type = 'instrument_pipeline_graph_node'
|
||||
proto_ext = project_pb2.instrument_pipeline_graph_node # type: ignore
|
||||
|
||||
track = model_base.ObjectReferenceProperty(Track)
|
||||
|
||||
def __init__(self, **kwargs: Any) -> None:
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.track_changed = core.Callback[model_base.PropertyChange[Track]]()
|
||||
|
||||
@property
|
||||
def track(self) -> Track:
|
||||
return self.get_property_value('track')
|
||||
|
||||
@property
|
||||
def removable(self) -> bool:
|
||||
return False
|
||||
|
||||
@property
|
||||
def description(self) -> node_db.NodeDescription:
|
||||
return instrument_db.parse_uri(
|
||||
cast(Union[ScoreTrack, BeatTrack], self.track).instrument,
|
||||
self.project.get_node_description)
|
||||
|
||||
|
||||
class PianoRollPipelineGraphNode(BasePipelineGraphNode):
|
||||
class Spec(model_base.ObjectSpec):
|
||||
proto_type = 'pianoroll_pipeline_graph_node'
|
||||
proto_ext = project_pb2.pianoroll_pipeline_graph_node # type: ignore
|
||||
|
||||
track = model_base.ObjectReferenceProperty(Track)
|
||||
|
||||
def __init__(self, **kwargs: Any) -> None:
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.track_changed = core.Callback[model_base.PropertyChange[Track]]()
|
||||
|
||||
@property
|
||||
def removable(self) -> bool:
|
||||
return False
|
||||
|
||||
@property
|
||||
def description(self) -> node_db.NodeDescription:
|
||||
return node_db.Builtins.PianoRollDescription
|
||||
|
||||
|
||||
class ScoreMeasure(Measure):
|
||||
class Spec(model_base.ObjectSpec):
|
||||
class ScoreMeasureSpec(model_base.ObjectSpec):
|
||||
proto_type = 'score_measure'
|
||||
proto_ext = project_pb2.score_measure # type: ignore
|
||||
|
||||
|
@ -561,40 +449,26 @@ class ScoreMeasure(Measure):
|
|||
|
||||
self.notes_changed.add(lambda _: self.content_changed.call())
|
||||
|
||||
@property
|
||||
def time_signature(self) -> time_signature_lib.TimeSignature:
|
||||
return self.project.get_time_signature(self.index)
|
||||
|
||||
|
||||
class ScoreTrack(MeasuredTrack):
|
||||
class Spec(model_base.ObjectSpec):
|
||||
class ScoreTrackSpec(model_base.ObjectSpec):
|
||||
proto_type = 'score_track'
|
||||
proto_ext = project_pb2.score_track # type: ignore
|
||||
|
||||
instrument = model_base.Property(str)
|
||||
transpose_octaves = model_base.Property(int, default=0)
|
||||
instrument_node = model_base.ObjectReferenceProperty(
|
||||
InstrumentPipelineGraphNode, allow_none=True)
|
||||
event_source_node = model_base.ObjectReferenceProperty(
|
||||
PianoRollPipelineGraphNode, allow_none=True)
|
||||
|
||||
def __init__(self, **kwargs: Any) -> None:
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.instrument_changed = core.Callback[model_base.PropertyChange[str]]()
|
||||
self.transpose_octaves_changed = core.Callback[model_base.PropertyChange[int]]()
|
||||
self.instrument_node_changed = \
|
||||
core.Callback[model_base.PropertyChange[InstrumentPipelineGraphNode]]()
|
||||
self.event_source_node_changed = \
|
||||
core.Callback[model_base.PropertyChange[PianoRollPipelineGraphNode]]()
|
||||
|
||||
@property
|
||||
def instrument(self) -> str:
|
||||
return self.get_property_value('instrument')
|
||||
def description(self) -> node_db.NodeDescription:
|
||||
return node_db.Builtins.ScoreTrackDescription
|
||||
|
||||
|
||||
class Beat(ProjectChild):
|
||||
class Spec(model_base.ObjectSpec):
|
||||
class BeatSpec(model_base.ObjectSpec):
|
||||
proto_type = 'beat'
|
||||
proto_ext = project_pb2.beat # type: ignore
|
||||
|
||||
|
@ -619,7 +493,7 @@ class Beat(ProjectChild):
|
|||
|
||||
|
||||
class BeatMeasure(Measure):
|
||||
class Spec(model_base.ObjectSpec):
|
||||
class BeatMeasureSpec(model_base.ObjectSpec):
|
||||
proto_type = 'beat_measure'
|
||||
proto_ext = project_pb2.beat_measure # type: ignore
|
||||
|
||||
|
@ -636,61 +510,26 @@ class BeatMeasure(Measure):
|
|||
super().setup()
|
||||
self.beats_changed.add(lambda _: self.content_changed.call())
|
||||
|
||||
@property
|
||||
def time_signature(self) -> time_signature_lib.TimeSignature:
|
||||
return self.project.get_time_signature(self.index)
|
||||
|
||||
|
||||
class BeatTrack(MeasuredTrack):
|
||||
class Spec(model_base.ObjectSpec):
|
||||
class BeatTrackSpec(model_base.ObjectSpec):
|
||||
proto_type = 'beat_track'
|
||||
proto_ext = project_pb2.beat_track # type: ignore
|
||||
|
||||
instrument = model_base.Property(str)
|
||||
pitch = model_base.WrappedProtoProperty(pitch_lib.Pitch)
|
||||
instrument_node = model_base.ObjectReferenceProperty(
|
||||
InstrumentPipelineGraphNode, allow_none=True)
|
||||
event_source_node = model_base.ObjectReferenceProperty(
|
||||
PianoRollPipelineGraphNode, allow_none=True)
|
||||
|
||||
def __init__(self, **kwargs: Any) -> None:
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.instrument_changed = core.Callback[model_base.PropertyChange[str]]()
|
||||
self.pitch_changed = core.Callback[model_base.PropertyChange[pitch_lib.Pitch]]()
|
||||
self.instrument_node_changed = \
|
||||
core.Callback[model_base.PropertyChange[InstrumentPipelineGraphNode]]()
|
||||
self.event_source_node_changed = \
|
||||
core.Callback[model_base.PropertyChange[PianoRollPipelineGraphNode]]()
|
||||
|
||||
@property
|
||||
def instrument(self) -> str:
|
||||
return self.get_property_value('instrument')
|
||||
|
||||
|
||||
class CVGeneratorPipelineGraphNode(BasePipelineGraphNode):
|
||||
class Spec(model_base.ObjectSpec):
|
||||
proto_type = 'cvgenerator_pipeline_graph_node'
|
||||
proto_ext = project_pb2.cvgenerator_pipeline_graph_node # type: ignore
|
||||
|
||||
track = model_base.ObjectReferenceProperty(Track)
|
||||
|
||||
def __init__(self, **kwargs: Any) -> None:
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.track_changed = core.Callback[model_base.PropertyChange[Track]]()
|
||||
|
||||
@property
|
||||
def removable(self) -> bool:
|
||||
return False
|
||||
|
||||
@property
|
||||
def description(self) -> node_db.NodeDescription:
|
||||
return node_db.Builtins.CVGeneratorDescription
|
||||
return node_db.Builtins.BeatTrackDescription
|
||||
|
||||
|
||||
class ControlPoint(ProjectChild):
|
||||
class Spec(model_base.ObjectSpec):
|
||||
class ControlPointSpec(model_base.ObjectSpec):
|
||||
proto_type = 'control_point'
|
||||
proto_ext = project_pb2.control_point # type: ignore
|
||||
|
||||
|
@ -705,45 +544,24 @@ class ControlPoint(ProjectChild):
|
|||
|
||||
|
||||
class ControlTrack(Track):
|
||||
class Spec(model_base.ObjectSpec):
|
||||
class ControlTrackSpec(model_base.ObjectSpec):
|
||||
proto_type = 'control_track'
|
||||
proto_ext = project_pb2.control_track # type: ignore
|
||||
|
||||
points = model_base.ObjectListProperty(ControlPoint)
|
||||
generator_node = model_base.ObjectReferenceProperty(
|
||||
CVGeneratorPipelineGraphNode, allow_none=True)
|
||||
|
||||
def __init__(self, **kwargs: Any) -> None:
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.points_changed = core.Callback[model_base.PropertyListChange[ControlPoint]]()
|
||||
self.generator_node_changed = \
|
||||
core.Callback[model_base.PropertyChange[CVGeneratorPipelineGraphNode]]()
|
||||
|
||||
|
||||
class SampleScriptPipelineGraphNode(BasePipelineGraphNode):
|
||||
class Spec(model_base.ObjectSpec):
|
||||
proto_type = 'sample_script_pipeline_graph_node'
|
||||
proto_ext = project_pb2.sample_script_pipeline_graph_node # type: ignore
|
||||
|
||||
track = model_base.ObjectReferenceProperty(Track)
|
||||
|
||||
def __init__(self, **kwargs: Any) -> None:
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.track_changed = core.Callback[model_base.PropertyChange[Track]]()
|
||||
|
||||
@property
|
||||
def removable(self) -> bool:
|
||||
return False
|
||||
|
||||
@property
|
||||
def description(self) -> node_db.NodeDescription:
|
||||
return node_db.Builtins.SampleScriptDescription
|
||||
return node_db.Builtins.ControlTrackDescription
|
||||
|
||||
|
||||
class SampleRef(ProjectChild):
|
||||
class Spec(model_base.ObjectSpec):
|
||||
class SampleRefSpec(model_base.ObjectSpec):
|
||||
proto_type = 'sample_ref'
|
||||
proto_ext = project_pb2.sample_ref # type: ignore
|
||||
|
||||
|
@ -758,43 +576,24 @@ class SampleRef(ProjectChild):
|
|||
|
||||
|
||||
class SampleTrack(Track):
|
||||
class Spec(model_base.ObjectSpec):
|
||||
class SampleTrackSpec(model_base.ObjectSpec):
|
||||
proto_type = 'sample_track'
|
||||
proto_ext = project_pb2.sample_track # type: ignore
|
||||
|
||||
samples = model_base.ObjectListProperty(SampleRef)
|
||||
sample_script_node = model_base.ObjectReferenceProperty(
|
||||
SampleScriptPipelineGraphNode, allow_none=True)
|
||||
|
||||
def __init__(self, **kwargs: Any) -> None:
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.samples_changed = core.Callback[model_base.PropertyListChange[SampleRef]]()
|
||||
self.sample_script_node_changed = \
|
||||
core.Callback[model_base.PropertyChange[SampleScriptPipelineGraphNode]]()
|
||||
|
||||
|
||||
class PipelineGraphConnection(ProjectChild):
|
||||
class Spec(model_base.ObjectSpec):
|
||||
proto_type = 'pipeline_graph_connection'
|
||||
proto_ext = project_pb2.pipeline_graph_connection # type: ignore
|
||||
|
||||
source_node = model_base.ObjectReferenceProperty(BasePipelineGraphNode)
|
||||
source_port = model_base.Property(str)
|
||||
dest_node = model_base.ObjectReferenceProperty(BasePipelineGraphNode)
|
||||
dest_port = model_base.Property(str)
|
||||
|
||||
def __init__(self, **kwargs: Any) -> None:
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.source_node_changed = core.Callback[model_base.PropertyChange[BasePipelineGraphNode]]()
|
||||
self.source_port_changed = core.Callback[model_base.PropertyChange[str]]()
|
||||
self.dest_node_changed = core.Callback[model_base.PropertyChange[BasePipelineGraphNode]]()
|
||||
self.dest_port_changed = core.Callback[model_base.PropertyChange[str]]()
|
||||
@property
|
||||
def description(self) -> node_db.NodeDescription:
|
||||
return node_db.Builtins.SampleTrackDescription
|
||||
|
||||
|
||||
class Metadata(ProjectChild):
|
||||
class Spec(model_base.ObjectSpec):
|
||||
class MetadataSpec(model_base.ObjectSpec):
|
||||
proto_type = 'metadata'
|
||||
proto_ext = project_pb2.metadata # type: ignore
|
||||
|
||||
|
@ -813,13 +612,11 @@ class Metadata(ProjectChild):
|
|||
|
||||
|
||||
class Project(ObjectBase):
|
||||
class Spec(model_base.ObjectSpec):
|
||||
class ProjectSpec(model_base.ObjectSpec):
|
||||
proto_type = 'project'
|
||||
proto_ext = project_pb2.project # type: ignore
|
||||
|
||||
metadata = model_base.ObjectProperty(Metadata)
|
||||
master_group = model_base.ObjectProperty(MasterTrackGroup)
|
||||
property_track = model_base.ObjectProperty(PropertyTrack)
|
||||
pipeline_graph_nodes = model_base.ObjectListProperty(BasePipelineGraphNode)
|
||||
pipeline_graph_connections = model_base.ObjectListProperty(PipelineGraphConnection)
|
||||
samples = model_base.ObjectListProperty(Sample)
|
||||
|
@ -828,12 +625,7 @@ class Project(ObjectBase):
|
|||
def __init__(self, **kwargs: Any) -> None:
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.__master_group_listener = None # type: core.Listener
|
||||
self.__duration = None # type: audioproc.MusicalDuration
|
||||
|
||||
self.master_group_changed = core.Callback[model_base.PropertyChange[MasterTrackGroup]]()
|
||||
self.metadata_changed = core.Callback[model_base.PropertyChange[Metadata]]()
|
||||
self.property_track_changed = core.Callback[model_base.PropertyChange[PropertyTrack]]()
|
||||
self.pipeline_graph_nodes_changed = \
|
||||
core.Callback[model_base.PropertyListChange[BasePipelineGraphNode]]()
|
||||
self.pipeline_graph_connections_changed = \
|
||||
|
@ -845,57 +637,13 @@ class Project(ObjectBase):
|
|||
core.Callback[model_base.PropertyChange[audioproc.MusicalDuration]]()
|
||||
self.pipeline_mutation = core.Callback[audioproc.Mutation]()
|
||||
|
||||
def setup(self) -> None:
|
||||
super().setup()
|
||||
self.master_group_changed.add(self.__on_master_group_changed)
|
||||
|
||||
try:
|
||||
master_group = self.master_group
|
||||
except model_base.ValueNotSetError:
|
||||
pass
|
||||
else:
|
||||
self.__master_group_listener = master_group.duration_changed.add(
|
||||
self.__update_duration)
|
||||
self.__update_duration()
|
||||
|
||||
def __update_duration(self, _: None = None) -> None:
|
||||
try:
|
||||
new_duration = self.master_group.duration
|
||||
except model_base.ValueNotSetError:
|
||||
return
|
||||
|
||||
if self.__duration is None or new_duration != self.__duration:
|
||||
old_duration = self.__duration
|
||||
self.__duration = new_duration
|
||||
self.duration_changed.call(
|
||||
model_base.PropertyValueChange(self, 'duration', old_duration, new_duration))
|
||||
|
||||
def __on_master_group_changed(
|
||||
self, change: model_base.PropertyValueChange[MasterTrackGroup]) -> None:
|
||||
if self.__master_group_listener is not None:
|
||||
self.__master_group_listener.remove()
|
||||
self.__master_group_listener = None
|
||||
|
||||
if change.new_value is not None:
|
||||
self.__master_group_listener = change.new_value.duration_changed.add(
|
||||
self.__update_duration)
|
||||
self.__update_duration()
|
||||
|
||||
@property
|
||||
def master_group(self) -> MasterTrackGroup:
|
||||
return self.get_property_value('master_group')
|
||||
|
||||
@property
|
||||
def property_track(self) -> PropertyTrack:
|
||||
return self.get_property_value('property_track')
|
||||
|
||||
@property
|
||||
def bpm(self) -> int:
|
||||
return self.get_property_value('bpm')
|
||||
|
||||
@property
|
||||
def duration(self) -> audioproc.MusicalDuration:
|
||||
return self.__duration
|
||||
return audioproc.MusicalDuration(2 * 120, 4) # 2min * 120bpm
|
||||
|
||||
@property
|
||||
def project(self) -> 'Project':
|
||||
|
@ -905,22 +653,16 @@ class Project(ObjectBase):
|
|||
def attached_to_project(self) -> bool:
|
||||
return True
|
||||
|
||||
@property
|
||||
def all_tracks(self) -> Sequence[Track]:
|
||||
tracks = [] # type: List[Track]
|
||||
tracks.append(self.property_track)
|
||||
tracks.extend(self.master_group.walk_tracks())
|
||||
return tracks
|
||||
|
||||
def get_bpm(self, measure_idx: int, tick: int) -> int: # pylint: disable=unused-argument
|
||||
return self.bpm
|
||||
|
||||
def get_time_signature(self, measure_idx: int) -> time_signature_lib.TimeSignature:
|
||||
# TODO: this is called with an incorrect measure_idx (index of the measure within the
|
||||
# measure_heap), so always use the time signature from the first measure, which
|
||||
# is also wrong, but at least doesn't crash.
|
||||
return cast(PropertyMeasure, self.property_track.measure_list[0].measure).time_signature
|
||||
@property
|
||||
def audio_out_node(self) -> AudioOutPipelineGraphNode:
|
||||
for node in self.get_property_value('pipeline_graph_nodes'):
|
||||
if isinstance(node, AudioOutPipelineGraphNode):
|
||||
return node
|
||||
|
||||
raise ValueError("No audio out node found.")
|
||||
|
||||
def get_node_description(self, uri: str) -> node_db.NodeDescription:
|
||||
raise NotImplementedError
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
# @begin:license
|
||||
#
|
||||
# Copyright (c) 2015-2018, 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 . import project_pb2
|
||||
from . import model_base
|
||||
|
||||
|
||||
class SizeF(model_base.ProtoValue):
|
||||
def __init__(self, width: float, height: float) -> None:
|
||||
self.__width = float(width)
|
||||
self.__height = float(height)
|
||||
|
||||
def to_proto(self) -> project_pb2.SizeF:
|
||||
return project_pb2.SizeF(width=self.__width, height=self.__height)
|
||||
|
||||
@classmethod
|
||||
def from_proto(cls, pb: protobuf.Message) -> 'SizeF':
|
||||
if not isinstance(pb, project_pb2.SizeF):
|
||||
raise TypeError(type(pb).__name__)
|
||||
return SizeF(pb.width, pb.height)
|
||||
|
||||
@property
|
||||
def width(self) -> float:
|
||||
return self.__width
|
||||
|
||||
@property
|
||||
def height(self) -> float:
|
||||
return self.__height
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
if not isinstance(other, SizeF):
|
||||
return False
|
||||
|
||||
return self.__width == other.__width and self.__height == other.__height
|
||||
|
||||
def __add__(self, other: 'SizeF') -> 'SizeF':
|
||||
if not isinstance(other, SizeF):
|
||||
raise TypeError("Expected SizeF, got %s" % type(other).__name__)
|
||||
|
||||
return self.__class__(self.__width + other.__width, self.__height + other.__height)
|
||||
|
||||
def __sub__(self, other: 'SizeF') -> 'SizeF':
|
||||
if not isinstance(other, SizeF):
|
||||
raise TypeError("Expected SizeF, got %s" % type(other).__name__)
|
||||
|
||||
return self.__class__(self.__width - other.__width, self.__height - other.__height)
|
|
@ -48,10 +48,6 @@ add_python_package(
|
|||
sample_track_test.py
|
||||
score_track.py
|
||||
score_track_test.py
|
||||
property_track.py
|
||||
property_track_test.py
|
||||
track_group.py
|
||||
track_group_test.py
|
||||
)
|
||||
|
||||
add_cython_module(rms CXX)
|
||||
|
|
|
@ -28,15 +28,11 @@ from .project_client import (
|
|||
MeasureReference,
|
||||
MeasuredTrack,
|
||||
Note,
|
||||
TrackGroup,
|
||||
MasterTrackGroup,
|
||||
ScoreMeasure,
|
||||
ScoreTrack,
|
||||
Beat,
|
||||
BeatMeasure,
|
||||
BeatTrack,
|
||||
PropertyMeasure,
|
||||
PropertyTrack,
|
||||
ControlPoint,
|
||||
ControlTrack,
|
||||
SampleRef,
|
||||
|
@ -45,10 +41,6 @@ from .project_client import (
|
|||
BasePipelineGraphNode,
|
||||
PipelineGraphNode,
|
||||
AudioOutPipelineGraphNode,
|
||||
TrackMixerPipelineGraphNode,
|
||||
PianoRollPipelineGraphNode,
|
||||
CVGeneratorPipelineGraphNode,
|
||||
SampleScriptPipelineGraphNode,
|
||||
InstrumentPipelineGraphNode,
|
||||
PipelineGraphConnection,
|
||||
Sample,
|
||||
|
@ -65,22 +57,18 @@ from .commands_pb2 import (
|
|||
Command,
|
||||
|
||||
UpdateProjectProperties,
|
||||
AddTrack,
|
||||
RemoveTrack,
|
||||
InsertMeasure,
|
||||
RemoveMeasure,
|
||||
MoveTrack,
|
||||
ReparentTrack,
|
||||
UpdateTrackProperties,
|
||||
SetNumMeasures,
|
||||
ClearMeasures,
|
||||
PasteMeasures,
|
||||
UpdateTrack,
|
||||
AddPipelineGraphNode,
|
||||
RemovePipelineGraphNode,
|
||||
AddPipelineGraphConnection,
|
||||
RemovePipelineGraphConnection,
|
||||
SetTimeSignature,
|
||||
SetInstrument,
|
||||
ChangeNote,
|
||||
InsertNote,
|
||||
DeleteNote,
|
||||
|
@ -97,12 +85,11 @@ from .commands_pb2 import (
|
|||
RemoveSample,
|
||||
MoveSample,
|
||||
RenderSample,
|
||||
SetBeatTrackInstrument,
|
||||
SetBeatTrackPitch,
|
||||
SetBeatVelocity,
|
||||
AddBeat,
|
||||
RemoveBeat,
|
||||
SetPipelineGraphNodePos,
|
||||
ChangePipelineGraphNode,
|
||||
SetPipelineGraphControlValue,
|
||||
SetPipelineGraphPluginState,
|
||||
PipelineGraphNodeToPreset,
|
||||
|
|
|
@ -30,64 +30,14 @@ from noisicaa.core.typing_extra import down_cast
|
|||
from noisicaa import audioproc
|
||||
from noisicaa import model
|
||||
from noisicaa import core # pylint: disable=unused-import
|
||||
from . import pmodel
|
||||
from . import pipeline_graph
|
||||
from . import pmodel
|
||||
from . import commands
|
||||
from . import commands_pb2
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MoveTrack(commands.Command):
|
||||
proto_type = 'move_track'
|
||||
|
||||
def run(self, project: pmodel.Project, pool: pmodel.Pool, pb: protobuf.Message) -> None:
|
||||
pb = down_cast(commands_pb2.MoveTrack, pb)
|
||||
track = down_cast(pmodel.Track, pool[self.proto.command.target])
|
||||
|
||||
assert not track.is_master_group
|
||||
parent = down_cast(pmodel.TrackGroup, track.parent)
|
||||
|
||||
if pb.direction == 0:
|
||||
raise ValueError("No direction given.")
|
||||
|
||||
if pb.direction < 0:
|
||||
if track.index == 0:
|
||||
raise ValueError("Can't move first track up.")
|
||||
new_pos = track.index - 1
|
||||
del parent.tracks[track.index]
|
||||
parent.tracks.insert(new_pos, track)
|
||||
|
||||
elif pb.direction > 0:
|
||||
if track.index == len(parent.tracks) - 1:
|
||||
raise ValueError("Can't move last track down.")
|
||||
new_pos = track.index + 1
|
||||
del parent.tracks[track.index]
|
||||
parent.tracks.insert(new_pos, track)
|
||||
|
||||
commands.Command.register_command(MoveTrack)
|
||||
|
||||
|
||||
class ReparentTrack(commands.Command):
|
||||
proto_type = 'reparent_track'
|
||||
|
||||
def run(self, project: pmodel.Project, pool: pmodel.Pool, pb: protobuf.Message) -> None:
|
||||
pb = down_cast(commands_pb2.ReparentTrack, pb)
|
||||
track = down_cast(pmodel.Track, pool[self.proto.command.target])
|
||||
|
||||
old_parent = down_cast(pmodel.TrackGroup, track.parent)
|
||||
new_parent = down_cast(pmodel.TrackGroup, pool[pb.new_parent])
|
||||
assert new_parent.is_child_of(track.project)
|
||||
assert isinstance(new_parent, pmodel.TrackGroup)
|
||||
|
||||
assert 0 <= pb.index <= len(new_parent.tracks)
|
||||
|
||||
del old_parent.tracks[track.index]
|
||||
new_parent.tracks.insert(pb.index, track)
|
||||
|
||||
commands.Command.register_command(ReparentTrack)
|
||||
|
||||
|
||||
class UpdateTrackProperties(commands.Command):
|
||||
proto_type = 'update_track_properties'
|
||||
|
||||
|
@ -95,25 +45,6 @@ class UpdateTrackProperties(commands.Command):
|
|||
pb = down_cast(commands_pb2.UpdateTrackProperties, pb)
|
||||
track = down_cast(pmodel.Track, pool[self.proto.command.target])
|
||||
|
||||
if pb.HasField('name'):
|
||||
track.name = pb.name
|
||||
|
||||
if pb.HasField('visible'):
|
||||
track.visible = pb.visible
|
||||
|
||||
# TODO: broken, needs to increment generation
|
||||
# if pb.HasField('muted'):
|
||||
# track.muted = pb.muted
|
||||
# track.mixer_node.set_control_value('muted', float(pb.muted))
|
||||
|
||||
# if pb.HasField('gain'):
|
||||
# track.gain = pb.gain
|
||||
# track.mixer_node.set_control_value('gain', pb.gain)
|
||||
|
||||
# if pb.HasField('pan'):
|
||||
# track.pan = pb.pan
|
||||
# track.mixer_node.set_control_value('pan', pb.pan)
|
||||
|
||||
if pb.HasField('transpose_octaves'):
|
||||
assert isinstance(track, pmodel.ScoreTrack)
|
||||
track.transpose_octaves = pb.transpose_octaves
|
||||
|
@ -121,6 +52,45 @@ class UpdateTrackProperties(commands.Command):
|
|||
commands.Command.register_command(UpdateTrackProperties)
|
||||
|
||||
|
||||
class UpdateTrack(commands.Command):
|
||||
proto_type = 'update_track'
|
||||
|
||||
def run(self, project: pmodel.Project, pool: pmodel.Pool, pb: protobuf.Message) -> None:
|
||||
pb = down_cast(commands_pb2.UpdateTrack, pb)
|
||||
track = down_cast(pmodel.Track, pool[self.proto.command.target])
|
||||
|
||||
if pb.HasField('visible'):
|
||||
track.visible = pb.visible
|
||||
|
||||
if pb.HasField('list_position'):
|
||||
track.list_position = pb.list_position
|
||||
|
||||
commands.Command.register_command(UpdateTrack)
|
||||
|
||||
|
||||
class InsertMeasure(commands.Command):
|
||||
proto_type = 'insert_measure'
|
||||
|
||||
def run(self, project: pmodel.Project, pool: pmodel.Pool, pb: protobuf.Message) -> None:
|
||||
pb = down_cast(commands_pb2.InsertMeasure, pb)
|
||||
track = down_cast(pmodel.MeasuredTrack, pool[self.proto.command.target])
|
||||
|
||||
track.insert_measure(pb.pos)
|
||||
|
||||
commands.Command.register_command(InsertMeasure)
|
||||
|
||||
|
||||
class RemoveMeasure(commands.Command):
|
||||
proto_type = 'remove_measure'
|
||||
|
||||
def run(self, project: pmodel.Project, pool: pmodel.Pool, pb: protobuf.Message) -> None:
|
||||
pb = down_cast(commands_pb2.RemoveMeasure, pb)
|
||||
track = down_cast(pmodel.MeasuredTrack, pool[self.proto.command.target])
|
||||
track.remove_measure(pb.pos)
|
||||
|
||||
commands.Command.register_command(RemoveMeasure)
|
||||
|
||||
|
||||
class TrackConnector(pmodel.TrackConnector):
|
||||
def __init__(
|
||||
self, *, track: 'Track', message_cb: Callable[[audioproc.ProcessorMessage], None]
|
||||
|
@ -154,65 +124,14 @@ class TrackConnector(pmodel.TrackConnector):
|
|||
pass
|
||||
|
||||
|
||||
class Track(pmodel.Track): # pylint: disable=abstract-method
|
||||
def create(self, *, name: Optional[str] = None, **kwargs: Any) -> None:
|
||||
super().create(**kwargs)
|
||||
if name is not None:
|
||||
self.name = name
|
||||
|
||||
@property
|
||||
def parent_audio_sink_name(self) -> str:
|
||||
return down_cast(Track, self.parent).mixer_name
|
||||
|
||||
@property
|
||||
def parent_audio_sink_node(self) -> pmodel.BasePipelineGraphNode:
|
||||
return down_cast(Track, self.parent).mixer_node
|
||||
|
||||
class Track(pmodel.Track, pipeline_graph.BasePipelineGraphNode): # pylint: disable=abstract-method
|
||||
# TODO: the following are common to MeasuredTrack and TrackGroup, but not really
|
||||
# generic for all track types.
|
||||
|
||||
@property
|
||||
def mixer_name(self) -> str:
|
||||
return '%s-track-mixer' % self.id
|
||||
|
||||
@property
|
||||
def relative_position_to_parent_audio_out(self) -> model.Pos2F:
|
||||
return model.Pos2F(-200, self.index * 100)
|
||||
|
||||
@property
|
||||
def default_mixer_name(self) -> str:
|
||||
return "Track Mixer"
|
||||
|
||||
def add_pipeline_nodes(self) -> None:
|
||||
parent_audio_sink_node = self.parent_audio_sink_node
|
||||
|
||||
project = down_cast(pmodel.Project, self.project)
|
||||
|
||||
mixer_node = self._pool.create(
|
||||
pipeline_graph.TrackMixerPipelineGraphNode,
|
||||
name=self.default_mixer_name,
|
||||
graph_pos=parent_audio_sink_node.graph_pos + self.relative_position_to_parent_audio_out,
|
||||
track=self)
|
||||
project.add_pipeline_graph_node(mixer_node)
|
||||
self.mixer_node = mixer_node
|
||||
|
||||
conn = self._pool.create(
|
||||
pipeline_graph.PipelineGraphConnection,
|
||||
source_node=mixer_node, source_port='out:left',
|
||||
dest_node=parent_audio_sink_node, dest_port='in:left')
|
||||
project.add_pipeline_graph_connection(conn)
|
||||
|
||||
conn = self._pool.create(
|
||||
pipeline_graph.PipelineGraphConnection,
|
||||
source_node=mixer_node, source_port='out:right',
|
||||
dest_node=parent_audio_sink_node, dest_port='in:right')
|
||||
project.add_pipeline_graph_connection(conn)
|
||||
|
||||
def remove_pipeline_nodes(self) -> None:
|
||||
project = down_cast(pmodel.Project, self.project)
|
||||
project.remove_pipeline_graph_node(self.mixer_node)
|
||||
self.mixer_node = None
|
||||
|
||||
|
||||
class Measure(pmodel.Measure):
|
||||
@property
|
||||
|
@ -262,12 +181,12 @@ class PianoRollInterval(object):
|
|||
class MeasuredTrackConnector(TrackConnector):
|
||||
_track = None # type: MeasuredTrack
|
||||
|
||||
def __init__(self, *, node_id: str, **kwargs: Any) -> None:
|
||||
def __init__(self, **kwargs: Any) -> None:
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self._listeners = {} # type: Dict[str, core.Listener]
|
||||
|
||||
self.__node_id = node_id
|
||||
self.__node_id = self._track.pipeline_node_id
|
||||
self.__measure_events = {} # type: Dict[int, List[PianoRollInterval]]
|
||||
|
||||
def _init_internal(self) -> None:
|
||||
|
|
|
@ -21,142 +21,61 @@
|
|||
# @end:license
|
||||
|
||||
import logging
|
||||
from typing import Type # pylint: disable=unused-import
|
||||
|
||||
from noisidev import unittest
|
||||
from . import commands_pb2
|
||||
from . import commands_test
|
||||
from . import project_client
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BaseTrackTest(commands_test.CommandsTestBase):
|
||||
async def test_move_track(self):
|
||||
await self.client.send_command(commands_pb2.Command(
|
||||
class TrackTestMixin(commands_test.CommandsTestMixin):
|
||||
node_uri = None # type: str
|
||||
track_cls = None # type: Type[project_client.Track]
|
||||
|
||||
async def test_add_remove(self) -> None:
|
||||
node_id = await self.client.send_command(commands_pb2.Command(
|
||||
target=self.project.id,
|
||||
add_track=commands_pb2.AddTrack(
|
||||
track_type='score',
|
||||
parent_group_id=self.project.master_group.id)))
|
||||
await self.client.send_command(commands_pb2.Command(
|
||||
target=self.project.id,
|
||||
add_track=commands_pb2.AddTrack(
|
||||
track_type='score',
|
||||
parent_group_id=self.project.master_group.id)))
|
||||
track1 = self.project.master_group.tracks[0]
|
||||
track2 = self.project.master_group.tracks[1]
|
||||
add_pipeline_graph_node=commands_pb2.AddPipelineGraphNode(
|
||||
uri=self.node_uri)))
|
||||
node = self.pool[node_id]
|
||||
assert isinstance(node, self.track_cls)
|
||||
|
||||
await self.client.send_command(commands_pb2.Command(
|
||||
target=track1.id,
|
||||
move_track=commands_pb2.MoveTrack(
|
||||
direction=1)))
|
||||
self.assertEqual(track1.index, 1)
|
||||
self.assertEqual(track2.index, 0)
|
||||
|
||||
async def test_reparent_track(self):
|
||||
await self.client.send_command(commands_pb2.Command(
|
||||
target=self.project.id,
|
||||
add_track=commands_pb2.AddTrack(
|
||||
track_type='score',
|
||||
parent_group_id=self.project.master_group.id)))
|
||||
await self.client.send_command(commands_pb2.Command(
|
||||
target=self.project.id,
|
||||
add_track=commands_pb2.AddTrack(
|
||||
track_type='group',
|
||||
parent_group_id=self.project.master_group.id)))
|
||||
track = self.project.master_group.tracks[0]
|
||||
grp = self.project.master_group.tracks[1]
|
||||
remove_pipeline_graph_node=commands_pb2.RemovePipelineGraphNode(
|
||||
node_id=node.id)))
|
||||
|
||||
async def _add_track(self) -> project_client.Track:
|
||||
node_id = await self.client.send_command(commands_pb2.Command(
|
||||
target=self.project.id,
|
||||
add_pipeline_graph_node=commands_pb2.AddPipelineGraphNode(
|
||||
uri=self.node_uri)))
|
||||
return self.pool[node_id]
|
||||
|
||||
|
||||
class BaseTrackTest(TrackTestMixin, unittest.AsyncTestCase):
|
||||
node_uri = 'builtin://score_track'
|
||||
track_cls = project_client.ScoreTrack
|
||||
|
||||
async def test_update_track_visible(self):
|
||||
track = await self._add_track()
|
||||
|
||||
self.assertTrue(track.visible)
|
||||
await self.client.send_command(commands_pb2.Command(
|
||||
target=track.id,
|
||||
reparent_track=commands_pb2.ReparentTrack(
|
||||
new_parent=grp.id,
|
||||
index=0)))
|
||||
self.assertEqual(len(self.project.master_group.tracks), 1)
|
||||
self.assertEqual(len(grp.tracks), 1)
|
||||
self.assertIs(track.parent, grp)
|
||||
|
||||
async def test_update_track_properties_name(self):
|
||||
await self.client.send_command(commands_pb2.Command(
|
||||
target=self.project.id,
|
||||
add_track=commands_pb2.AddTrack(
|
||||
track_type='score',
|
||||
parent_group_id=self.project.master_group.id)))
|
||||
track = self.project.master_group.tracks[0]
|
||||
|
||||
await self.client.send_command(commands_pb2.Command(
|
||||
target=track.id,
|
||||
update_track_properties=commands_pb2.UpdateTrackProperties(
|
||||
name='Lead')))
|
||||
self.assertEqual(track.name, 'Lead')
|
||||
|
||||
async def test_update_track_properties_visible(self):
|
||||
await self.client.send_command(commands_pb2.Command(
|
||||
target=self.project.id,
|
||||
add_track=commands_pb2.AddTrack(
|
||||
track_type='score',
|
||||
parent_group_id=self.project.master_group.id)))
|
||||
track = self.project.master_group.tracks[0]
|
||||
|
||||
await self.client.send_command(commands_pb2.Command(
|
||||
target=track.id,
|
||||
update_track_properties=commands_pb2.UpdateTrackProperties(
|
||||
update_track=commands_pb2.UpdateTrack(
|
||||
visible=False)))
|
||||
self.assertFalse(track.visible)
|
||||
|
||||
@unittest.skip("Implementation broken")
|
||||
async def test_update_track_properties_muted(self):
|
||||
await self.client.send_command(commands_pb2.Command(
|
||||
target=self.project.id,
|
||||
add_track=commands_pb2.AddTrack(
|
||||
track_type='score',
|
||||
parent_group_id=self.project.master_group.id)))
|
||||
track = self.project.master_group.tracks[0]
|
||||
async def test_update_track_list_position(self):
|
||||
track = await self._add_track()
|
||||
|
||||
self.assertEqual(track.list_position, 0)
|
||||
await self.client.send_command(commands_pb2.Command(
|
||||
target=track.id,
|
||||
update_track_properties=commands_pb2.UpdateTrackProperties(
|
||||
muted=True)))
|
||||
self.assertTrue(track.muted)
|
||||
|
||||
@unittest.skip("Implementation broken")
|
||||
async def test_update_track_properties_gain(self):
|
||||
await self.client.send_command(commands_pb2.Command(
|
||||
target=self.project.id,
|
||||
add_track=commands_pb2.AddTrack(
|
||||
track_type='score',
|
||||
parent_group_id=self.project.master_group.id)))
|
||||
track = self.project.master_group.tracks[0]
|
||||
|
||||
await self.client.send_command(commands_pb2.Command(
|
||||
target=track.id,
|
||||
update_track_properties=commands_pb2.UpdateTrackProperties(
|
||||
gain=-4.0)))
|
||||
self.assertAlmostEqual(track.gain, -4.0)
|
||||
|
||||
@unittest.skip("Implementation broken")
|
||||
async def test_update_track_properties_pan(self):
|
||||
await self.client.send_command(commands_pb2.Command(
|
||||
target=self.project.id,
|
||||
add_track=commands_pb2.AddTrack(
|
||||
track_type='score',
|
||||
parent_group_id=self.project.master_group.id)))
|
||||
track = self.project.master_group.tracks[0]
|
||||
|
||||
await self.client.send_command(commands_pb2.Command(
|
||||
target=track.id,
|
||||
update_track_properties=commands_pb2.UpdateTrackProperties(
|
||||
pan=-0.7)))
|
||||
self.assertAlmostEqual(track.pan, -0.7)
|
||||
|
||||
async def test_update_track_properties_transport_octaves(self):
|
||||
await self.client.send_command(commands_pb2.Command(
|
||||
target=self.project.id,
|
||||
add_track=commands_pb2.AddTrack(
|
||||
track_type='score',
|
||||
parent_group_id=self.project.master_group.id)))
|
||||
track = self.project.master_group.tracks[0]
|
||||
|
||||
await self.client.send_command(commands_pb2.Command(
|
||||
target=track.id,
|
||||
update_track_properties=commands_pb2.UpdateTrackProperties(
|
||||
transpose_octaves=1)))
|
||||
self.assertEqual(track.transpose_octaves, 1)
|
||||
update_track=commands_pb2.UpdateTrack(
|
||||
list_position=2)))
|
||||
self.assertEqual(track.list_position, 2)
|
||||
|
|
|
@ -29,7 +29,6 @@ from noisicaa.core.typing_extra import down_cast
|
|||
from noisicaa import audioproc
|
||||
from noisicaa import model
|
||||
from . import pmodel
|
||||
from . import pipeline_graph
|
||||
from . import base_track
|
||||
from . import commands
|
||||
from . import commands_pb2
|
||||
|
@ -37,21 +36,6 @@ from . import commands_pb2
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SetBeatTrackInstrument(commands.Command):
|
||||
proto_type = 'set_beat_track_instrument'
|
||||
|
||||
def run(self, project: pmodel.Project, pool: pmodel.Pool, pb: protobuf.Message) -> None:
|
||||
pb = down_cast(commands_pb2.SetBeatTrackInstrument, pb)
|
||||
track = down_cast(pmodel.BeatTrack, pool[self.proto.command.target])
|
||||
|
||||
track.instrument = pb.instrument
|
||||
|
||||
for mutation in track.instrument_node.get_update_mutations():
|
||||
project.handle_pipeline_mutation(mutation)
|
||||
|
||||
commands.Command.register_command(SetBeatTrackInstrument)
|
||||
|
||||
|
||||
class SetBeatTrackPitch(commands.Command):
|
||||
proto_type = 'set_beat_track_pitch'
|
||||
|
||||
|
@ -160,15 +144,10 @@ class BeatTrack(pmodel.BeatTrack, base_track.MeasuredTrack):
|
|||
|
||||
def create(
|
||||
self, *,
|
||||
instrument: Optional[str] = None, pitch: Optional[model.Pitch] = None,
|
||||
pitch: Optional[model.Pitch] = None,
|
||||
num_measures: int = 1, **kwargs: Any) -> None:
|
||||
super().create(**kwargs)
|
||||
|
||||
if instrument is None:
|
||||
self.instrument = 'sf2:/usr/share/sounds/sf2/FluidR3_GM.sf2?bank=128&preset=0'
|
||||
else:
|
||||
self.instrument = instrument
|
||||
|
||||
if pitch is None:
|
||||
self.pitch = model.Pitch('B2')
|
||||
else:
|
||||
|
@ -178,57 +157,16 @@ class BeatTrack(pmodel.BeatTrack, base_track.MeasuredTrack):
|
|||
self.append_measure()
|
||||
|
||||
def create_track_connector(self, **kwargs: Any) -> BeatTrackConnector:
|
||||
return BeatTrackConnector(
|
||||
track=self,
|
||||
node_id=self.event_source_name,
|
||||
**kwargs)
|
||||
return BeatTrackConnector(track=self, **kwargs)
|
||||
|
||||
@property
|
||||
def event_source_name(self) -> str:
|
||||
return '%016x-events' % self.id
|
||||
|
||||
@property
|
||||
def instr_name(self) -> str:
|
||||
return '%016x-instr' % self.id
|
||||
def get_add_mutations(self) -> Iterator[audioproc.Mutation]:
|
||||
yield audioproc.AddNode(
|
||||
description=self.description,
|
||||
id=self.pipeline_node_id,
|
||||
name=self.name)
|
||||
|
||||
def add_pipeline_nodes(self) -> None:
|
||||
super().add_pipeline_nodes()
|
||||
yield from self.get_initial_parameter_mutations()
|
||||
|
||||
mixer_node = self.mixer_node
|
||||
|
||||
instrument_node = self._pool.create(
|
||||
pipeline_graph.InstrumentPipelineGraphNode,
|
||||
name="Track Instrument",
|
||||
graph_pos=mixer_node.graph_pos - model.Pos2F(200, 0),
|
||||
track=self)
|
||||
self.project.add_pipeline_graph_node(instrument_node)
|
||||
self.instrument_node = instrument_node
|
||||
|
||||
self.project.add_pipeline_graph_connection(self._pool.create(
|
||||
pipeline_graph.PipelineGraphConnection,
|
||||
source_node=instrument_node, source_port='out:left',
|
||||
dest_node=self.mixer_node, dest_port='in:left'))
|
||||
self.project.add_pipeline_graph_connection(self._pool.create(
|
||||
pipeline_graph.PipelineGraphConnection,
|
||||
source_node=instrument_node, source_port='out:right',
|
||||
dest_node=self.mixer_node, dest_port='in:right'))
|
||||
|
||||
event_source_node = self._pool.create(
|
||||
pipeline_graph.PianoRollPipelineGraphNode,
|
||||
name="Track Events",
|
||||
graph_pos=instrument_node.graph_pos - model.Pos2F(200, 0),
|
||||
track=self)
|
||||
self.project.add_pipeline_graph_node(event_source_node)
|
||||
self.event_source_node = event_source_node
|
||||
|
||||
self.project.add_pipeline_graph_connection(self._pool.create(
|
||||
pipeline_graph.PipelineGraphConnection,
|
||||
source_node=event_source_node, source_port='out',
|
||||
dest_node=instrument_node, dest_port='in'))
|
||||
|
||||
def remove_pipeline_nodes(self) -> None:
|
||||
self.project.remove_pipeline_graph_node(self.event_source_node)
|
||||
self.event_source_node = None
|
||||
self.project.remove_pipeline_graph_node(self.instrument_node)
|
||||
self.instrument_node = None
|
||||
super().remove_pipeline_nodes()
|
||||
def get_remove_mutations(self) -> Iterator[audioproc.Mutation]:
|
||||
yield audioproc.RemoveNode(self.pipeline_node_id)
|
||||
|
|
|
@ -22,63 +22,26 @@
|
|||
|
||||
import logging
|
||||
|
||||
from noisidev import unittest
|
||||
from noisicaa import audioproc
|
||||
from noisicaa import model
|
||||
from . import project_client
|
||||
from . import commands_pb2
|
||||
from . import commands_test
|
||||
from . import base_track_test
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BeatTrackTest(commands_test.CommandsTestBase):
|
||||
async def test_add_remove(self):
|
||||
insert_index = await self.client.send_command(commands_pb2.Command(
|
||||
target=self.project.id,
|
||||
add_track=commands_pb2.AddTrack(
|
||||
track_type='beat',
|
||||
parent_group_id=self.project.master_group.id)))
|
||||
self.assertEqual(insert_index, 0)
|
||||
|
||||
track = self.project.master_group.tracks[insert_index]
|
||||
self.assertIsInstance(track, project_client.BeatTrack)
|
||||
|
||||
await self.client.send_command(commands_pb2.Command(
|
||||
target=self.project.id,
|
||||
remove_track=commands_pb2.RemoveTrack(
|
||||
track_id=track.id)))
|
||||
self.assertEqual(len(self.project.master_group.tracks), 0)
|
||||
|
||||
async def _add_track(self):
|
||||
insert_index = await self.client.send_command(commands_pb2.Command(
|
||||
target=self.project.id,
|
||||
add_track=commands_pb2.AddTrack(
|
||||
track_type='beat',
|
||||
parent_group_id=self.project.master_group.id)))
|
||||
return self.project.master_group.tracks[insert_index]
|
||||
|
||||
async def test_set_num_measures(self):
|
||||
track = await self._add_track()
|
||||
|
||||
await self.client.send_command(commands_pb2.Command(
|
||||
target=self.project.id,
|
||||
set_num_measures=commands_pb2.SetNumMeasures(
|
||||
num_measures=10)))
|
||||
self.assertEqual(len(track.measure_list), 10)
|
||||
self.assertEqual(self.project.duration, audioproc.MusicalDuration(10 * 4, 4))
|
||||
|
||||
await self.client.send_command(commands_pb2.Command(
|
||||
target=self.project.id,
|
||||
set_num_measures=commands_pb2.SetNumMeasures(
|
||||
num_measures=5)))
|
||||
self.assertEqual(len(track.measure_list), 5)
|
||||
class BeatTrackTest(base_track_test.TrackTestMixin, unittest.AsyncTestCase):
|
||||
node_uri = 'builtin://beat_track'
|
||||
track_cls = project_client.BeatTrack
|
||||
|
||||
async def test_insert_measure(self):
|
||||
track = await self._add_track()
|
||||
self.assertEqual(len(track.measure_list), 1)
|
||||
|
||||
await self.client.send_command(commands_pb2.Command(
|
||||
target=self.project.id,
|
||||
target=track.id,
|
||||
insert_measure=commands_pb2.InsertMeasure(
|
||||
pos=0,
|
||||
tracks=[track.id])))
|
||||
|
@ -89,7 +52,7 @@ class BeatTrackTest(commands_test.CommandsTestBase):
|
|||
self.assertEqual(len(track.measure_list), 1)
|
||||
|
||||
await self.client.send_command(commands_pb2.Command(
|
||||
target=self.project.id,
|
||||
target=track.id,
|
||||
insert_measure=commands_pb2.InsertMeasure(
|
||||
tracks=[])))
|
||||
self.assertEqual(len(track.measure_list), 2)
|
||||
|
@ -99,7 +62,7 @@ class BeatTrackTest(commands_test.CommandsTestBase):
|
|||
self.assertEqual(len(track.measure_list), 1)
|
||||
|
||||
await self.client.send_command(commands_pb2.Command(
|
||||
target=self.project.id,
|
||||
target=track.id,
|
||||
remove_measure=commands_pb2.RemoveMeasure(
|
||||
pos=0,
|
||||
tracks=[])))
|
||||
|
@ -116,16 +79,6 @@ class BeatTrackTest(commands_test.CommandsTestBase):
|
|||
measure_ids=[track.measure_list[0].id])))
|
||||
self.assertIsNot(old_measure, track.measure_list[0].measure)
|
||||
|
||||
async def test_set_beat_track_instrument(self):
|
||||
track = await self._add_track()
|
||||
|
||||
await self.client.send_command(commands_pb2.Command(
|
||||
target=track.id,
|
||||
set_beat_track_instrument=commands_pb2.SetBeatTrackInstrument(
|
||||
instrument='sf2:/usr/share/sounds/sf2/FluidR3_GM.sf2?bank=0&preset=2')))
|
||||
self.assertEqual(
|
||||
track.instrument, 'sf2:/usr/share/sounds/sf2/FluidR3_GM.sf2?bank=0&preset=2')
|
||||
|
||||
async def test_set_beat_track_pitch(self):
|
||||
track = await self._add_track()
|
||||
|
||||
|
|
|
@ -34,16 +34,6 @@ message UpdateProjectProperties {
|
|||
optional uint32 bpm = 1;
|
||||
}
|
||||
|
||||
message AddTrack {
|
||||
optional string track_type = 1;
|
||||
optional uint64 parent_group_id = 2;
|
||||
optional int32 insert_index = 3 [default = -1];
|
||||
}
|
||||
|
||||
message RemoveTrack {
|
||||
optional uint64 track_id = 1;
|
||||
}
|
||||
|
||||
message InsertMeasure {
|
||||
repeated uint64 tracks = 1;
|
||||
optional int32 pos = 2;
|
||||
|
@ -54,22 +44,8 @@ message RemoveMeasure {
|
|||
optional int32 pos = 2;
|
||||
}
|
||||
|
||||
message MoveTrack {
|
||||
optional int32 direction = 1;
|
||||
}
|
||||
|
||||
message ReparentTrack {
|
||||
optional uint64 new_parent = 1;
|
||||
optional uint32 index = 2;
|
||||
}
|
||||
|
||||
message UpdateTrackProperties {
|
||||
optional string name = 1;
|
||||
optional bool visible = 2;
|
||||
optional bool muted = 3;
|
||||
optional float gain = 4;
|
||||
optional float pan = 5;
|
||||
optional int32 transpose_octaves = 6;
|
||||
optional int32 transpose_octaves = 1;
|
||||
}
|
||||
|
||||
message SetNumMeasures {
|
||||
|
@ -86,9 +62,17 @@ message PasteMeasures {
|
|||
repeated uint64 target_ids = 3;
|
||||
}
|
||||
|
||||
message UpdateTrack {
|
||||
optional bool visible = 1;
|
||||
optional uint32 list_position = 2;
|
||||
}
|
||||
|
||||
message AddPipelineGraphNode {
|
||||
optional string uri = 1;
|
||||
optional Pos2F graph_pos = 2;
|
||||
optional string name = 2;
|
||||
optional Pos2F graph_pos = 3;
|
||||
optional SizeF graph_size = 4;
|
||||
optional Color graph_color = 5;
|
||||
}
|
||||
|
||||
message RemovePipelineGraphNode {
|
||||
|
@ -218,8 +202,11 @@ message RemoveBeat {
|
|||
optional uint64 beat_id = 1;
|
||||
}
|
||||
|
||||
message SetPipelineGraphNodePos {
|
||||
message ChangePipelineGraphNode {
|
||||
optional Pos2F graph_pos = 1;
|
||||
optional SizeF graph_size = 2;
|
||||
optional Color graph_color = 4;
|
||||
optional string name = 3;
|
||||
}
|
||||
|
||||
message SetPipelineGraphControlValue {
|
||||
|
@ -244,12 +231,8 @@ message Command {
|
|||
|
||||
oneof command {
|
||||
UpdateProjectProperties update_project_properties = 100;
|
||||
AddTrack add_track = 101;
|
||||
RemoveTrack remove_track = 102;
|
||||
InsertMeasure insert_measure = 103;
|
||||
RemoveMeasure remove_measure = 104;
|
||||
MoveTrack move_track = 105;
|
||||
ReparentTrack reparent_track = 106;
|
||||
UpdateTrackProperties update_track_properties = 107;
|
||||
SetNumMeasures set_num_measures = 108;
|
||||
ClearMeasures clear_measures = 109;
|
||||
|
@ -259,7 +242,6 @@ message Command {
|
|||
AddPipelineGraphConnection add_pipeline_graph_connection = 113;
|
||||
RemovePipelineGraphConnection remove_pipeline_graph_connection = 114;
|
||||
SetTimeSignature set_time_signature = 115;
|
||||
SetInstrument set_instrument = 116;
|
||||
ChangeNote change_note = 117;
|
||||
InsertNote insert_note = 118;
|
||||
DeleteNote delete_note = 119;
|
||||
|
@ -276,16 +258,16 @@ message Command {
|
|||
RemoveSample remove_sample = 130;
|
||||
MoveSample move_sample = 131;
|
||||
RenderSample render_sample = 132;
|
||||
SetBeatTrackInstrument set_beat_track_instrument = 133;
|
||||
SetBeatTrackPitch set_beat_track_pitch = 134;
|
||||
SetBeatVelocity set_beat_velocity = 135;
|
||||
AddBeat add_beat = 136;
|
||||
RemoveBeat remove_beat = 137;
|
||||
SetPipelineGraphNodePos set_pipeline_graph_node_pos = 138;
|
||||
ChangePipelineGraphNode change_pipeline_graph_node = 138;
|
||||
SetPipelineGraphControlValue set_pipeline_graph_control_value = 139;
|
||||
SetPipelineGraphPluginState set_pipeline_graph_plugin_state = 140;
|
||||
PipelineGraphNodeToPreset pipeline_graph_node_to_preset = 141;
|
||||
PipelineGraphNodeFromPreset pipeline_graph_node_from_preset = 142;
|
||||
UpdateTrack update_track = 143;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -24,7 +24,6 @@ import logging
|
|||
import os.path
|
||||
import uuid
|
||||
|
||||
from noisidev import unittest
|
||||
from noisidev import unittest_mixins
|
||||
from noisicaa.constants import TEST_OPTS
|
||||
from noisicaa import model # pylint: disable=unused-import
|
||||
|
@ -33,7 +32,7 @@ from . import project_client
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CommandsTestBase(unittest_mixins.ProcessManagerMixin, unittest.AsyncTestCase):
|
||||
class CommandsTestMixin(unittest_mixins.ProcessManagerMixin):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
@ -57,7 +56,6 @@ class CommandsTestBase(unittest_mixins.ProcessManagerMixin, unittest.AsyncTestCa
|
|||
await self.client.create(path)
|
||||
self.project = self.client.project
|
||||
self.pool = self.project._pool
|
||||
self.assertEqual(len(self.project.master_group.tracks), 0)
|
||||
|
||||
logger.info("Testcase setup complete")
|
||||
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
|
||||
import logging
|
||||
import random
|
||||
from typing import cast, Any, Dict, Optional # pylint: disable=unused-import
|
||||
from typing import cast, Any, Dict, Optional, Iterator # pylint: disable=unused-import
|
||||
|
||||
from google.protobuf import message as protobuf
|
||||
|
||||
|
@ -32,7 +32,6 @@ from noisicaa import model
|
|||
from noisicaa import core # pylint: disable=unused-import
|
||||
from . import pmodel
|
||||
from . import base_track
|
||||
from . import pipeline_graph
|
||||
from . import commands
|
||||
from . import commands_pb2
|
||||
|
||||
|
@ -123,10 +122,10 @@ class ControlPoint(pmodel.ControlPoint):
|
|||
class ControlTrackConnector(base_track.TrackConnector):
|
||||
_track = None # type: ControlTrack
|
||||
|
||||
def __init__(self, *, node_id: str, **kwargs: Any) -> None:
|
||||
def __init__(self, **kwargs: Any) -> None:
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.__node_id = node_id
|
||||
self.__node_id = self._track.pipeline_node_id
|
||||
self.__listeners = {} # type: Dict[str, core.Listener]
|
||||
self.__point_ids = {} # type: Dict[int, int]
|
||||
|
||||
|
@ -202,36 +201,15 @@ class ControlTrackConnector(base_track.TrackConnector):
|
|||
|
||||
class ControlTrack(pmodel.ControlTrack, base_track.Track):
|
||||
def create_track_connector(self, **kwargs: Any) -> ControlTrackConnector:
|
||||
return ControlTrackConnector(
|
||||
track=self,
|
||||
node_id=self.generator_name,
|
||||
**kwargs)
|
||||
return ControlTrackConnector(track=self, **kwargs)
|
||||
|
||||
@property
|
||||
def mixer_name(self) -> str:
|
||||
return self.parent_audio_sink_name
|
||||
def get_add_mutations(self) -> Iterator[audioproc.Mutation]:
|
||||
yield audioproc.AddNode(
|
||||
description=self.description,
|
||||
id=self.pipeline_node_id,
|
||||
name=self.name)
|
||||
|
||||
@property
|
||||
def mixer_node(self) -> pmodel.BasePipelineGraphNode:
|
||||
return self.parent_audio_sink_node
|
||||
yield from self.get_initial_parameter_mutations()
|
||||
|
||||
@mixer_node.setter
|
||||
def mixer_node(self, value: pmodel.TrackMixerPipelineGraphNode) -> None:
|
||||
raise RuntimeError
|
||||
|
||||
@property
|
||||
def generator_name(self) -> str:
|
||||
return '%016x-generator' % self.id
|
||||
|
||||
def add_pipeline_nodes(self) -> None:
|
||||
generator_node = self._pool.create(
|
||||
pipeline_graph.CVGeneratorPipelineGraphNode,
|
||||
name="Control Value",
|
||||
graph_pos=self.parent_audio_sink_node.graph_pos - model.Pos2F(200, 0),
|
||||
track=self)
|
||||
self.project.add_pipeline_graph_node(generator_node)
|
||||
self.generator_node = generator_node
|
||||
|
||||
def remove_pipeline_nodes(self) -> None:
|
||||
self.project.remove_pipeline_graph_node(self.generator_node)
|
||||
self.generator_node = None
|
||||
def get_remove_mutations(self) -> Iterator[audioproc.Mutation]:
|
||||
yield audioproc.RemoveNode(self.pipeline_node_id)
|
||||
|
|
|
@ -28,23 +28,23 @@ from noisidev import demo_project
|
|||
from noisicaa import audioproc
|
||||
from . import project
|
||||
from . import control_track
|
||||
from . import commands_test
|
||||
from . import commands_pb2
|
||||
from . import project_client
|
||||
from . import base_track_test
|
||||
|
||||
|
||||
class ControlTrackConnectorTest(unittest_mixins.NodeDBMixin, unittest.AsyncTestCase):
|
||||
async def setup_testcase(self):
|
||||
self.pool = project.Pool()
|
||||
|
||||
self.project = demo_project.basic(self.pool, project.BaseProject, node_db=self.node_db)
|
||||
self.project = demo_project.empty(self.pool, project.BaseProject, node_db=self.node_db)
|
||||
self.track = self.pool.create(control_track.ControlTrack, name='test')
|
||||
self.project.master_group.tracks.append(self.track)
|
||||
self.project.pipeline_graph_nodes.append(self.track)
|
||||
|
||||
self.messages = [] # type: List[str]
|
||||
|
||||
def message_cb(self, msg):
|
||||
self.assertEqual(msg.node_id, self.track.generator_name)
|
||||
self.assertEqual(int(msg.node_id, 16), self.track.id)
|
||||
# TODO: track the messages themselves and inspect their contents as well.
|
||||
self.messages.append(msg.WhichOneof('msg'))
|
||||
|
||||
|
@ -117,31 +117,9 @@ class ControlTrackConnectorTest(unittest_mixins.NodeDBMixin, unittest.AsyncTestC
|
|||
connector.close()
|
||||
|
||||
|
||||
class ControlTrackTest(commands_test.CommandsTestBase):
|
||||
async def test_add_remove(self):
|
||||
insert_index = await self.client.send_command(commands_pb2.Command(
|
||||
target=self.project.id,
|
||||
add_track=commands_pb2.AddTrack(
|
||||
track_type='control',
|
||||
parent_group_id=self.project.master_group.id)))
|
||||
self.assertEqual(insert_index, 0)
|
||||
|
||||
track = self.project.master_group.tracks[insert_index]
|
||||
self.assertIsInstance(track, project_client.ControlTrack)
|
||||
|
||||
await self.client.send_command(commands_pb2.Command(
|
||||
target=self.project.id,
|
||||
remove_track=commands_pb2.RemoveTrack(
|
||||
track_id=track.id)))
|
||||
self.assertEqual(len(self.project.master_group.tracks), 0)
|
||||
|
||||
async def _add_track(self):
|
||||
insert_index = await self.client.send_command(commands_pb2.Command(
|
||||
target=self.project.id,
|
||||
add_track=commands_pb2.AddTrack(
|
||||
track_type='control',
|
||||
parent_group_id=self.project.master_group.id)))
|
||||
return self.project.master_group.tracks[insert_index]
|
||||
class ControlTrackTest(base_track_test.TrackTestMixin, unittest.AsyncTestCase):
|
||||
node_uri = 'builtin://control_track'
|
||||
track_cls = project_client.ControlTrack
|
||||
|
||||
async def test_add_control_point(self):
|
||||
track = await self._add_track()
|
||||
|
|
|
@ -89,6 +89,8 @@ message MutationList {
|
|||
TimeSignature time_signature = 105;
|
||||
Clef clef = 106;
|
||||
Pos2F pos2f = 107;
|
||||
SizeF sizef = 109;
|
||||
Color color = 110;
|
||||
ControlValue control_value = 108;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -83,6 +83,10 @@ class MutationList(object):
|
|||
return model.Clef.from_proto(slot.clef)
|
||||
elif vtype == 'pos2f':
|
||||
return model.Pos2F.from_proto(slot.pos2f)
|
||||
elif vtype == 'sizef':
|
||||
return model.SizeF.from_proto(slot.sizef)
|
||||
elif vtype == 'color':
|
||||
return model.Color.from_proto(slot.color)
|
||||
elif vtype == 'control_value':
|
||||
return copy.deepcopy(slot.control_value)
|
||||
|
||||
|
@ -273,6 +277,10 @@ class MutationCollector(object):
|
|||
slot.clef.CopyFrom(value.to_proto())
|
||||
elif isinstance(value, model.Pos2F):
|
||||
slot.pos2f.CopyFrom(value.to_proto())
|
||||
elif isinstance(value, model.SizeF):
|
||||
slot.sizef.CopyFrom(value.to_proto())
|
||||
elif isinstance(value, model.Color):
|
||||
slot.color.CopyFrom(value.to_proto())
|
||||
elif isinstance(value, model.ControlValue):
|
||||
slot.control_value.CopyFrom(value)
|
||||
|
||||
|
|
|
@ -46,16 +46,26 @@ class NotAPresetError(PresetLoadError):
|
|||
pass
|
||||
|
||||
|
||||
class SetPipelineGraphNodePos(commands.Command):
|
||||
proto_type = 'set_pipeline_graph_node_pos'
|
||||
class ChangePipelineGraphNode(commands.Command):
|
||||
proto_type = 'change_pipeline_graph_node'
|
||||
|
||||
def run(self, project: pmodel.Project, pool: pmodel.Pool, pb: protobuf.Message) -> None:
|
||||
pb = down_cast(commands_pb2.SetPipelineGraphNodePos, pb)
|
||||
pb = down_cast(commands_pb2.ChangePipelineGraphNode, pb)
|
||||
node = down_cast(pmodel.BasePipelineGraphNode, pool[self.proto.command.target])
|
||||
|
||||
node.graph_pos = model.Pos2F.from_proto(pb.graph_pos)
|
||||
if pb.HasField('graph_pos'):
|
||||
node.graph_pos = model.Pos2F.from_proto(pb.graph_pos)
|
||||
|
||||
commands.Command.register_command(SetPipelineGraphNodePos)
|
||||
if pb.HasField('graph_size'):
|
||||
node.graph_size = model.SizeF.from_proto(pb.graph_size)
|
||||
|
||||
if pb.HasField('graph_color'):
|
||||
node.graph_color = model.Color.from_proto(pb.graph_color)
|
||||
|
||||
if pb.HasField('name'):
|
||||
node.name = pb.name
|
||||
|
||||
commands.Command.register_command(ChangePipelineGraphNode)
|
||||
|
||||
|
||||
class SetPipelineGraphControlValue(commands.Command):
|
||||
|
@ -125,12 +135,21 @@ class PipelineGraphControlValue(pmodel.PipelineGraphControlValue):
|
|||
class BasePipelineGraphNode(pmodel.BasePipelineGraphNode): # pylint: disable=abstract-method
|
||||
def create(
|
||||
self, *,
|
||||
name: Optional[str] = None, graph_pos: model.Pos2F = model.Pos2F(0, 0),
|
||||
name: Optional[str] = None,
|
||||
graph_pos: model.Pos2F = model.Pos2F(0, 0),
|
||||
graph_size: model.SizeF = model.SizeF(140, 100),
|
||||
graph_color: model.Color = model.Color(0.8, 0.8, 0.8),
|
||||
**kwargs: Any) -> None:
|
||||
super().create(**kwargs)
|
||||
|
||||
self.name = name
|
||||
self.graph_pos = graph_pos
|
||||
self.graph_size = graph_size
|
||||
self.graph_color = graph_color
|
||||
|
||||
@property
|
||||
def pipeline_node_id(self) -> str:
|
||||
return '%016x' % self.id
|
||||
|
||||
def get_initial_parameter_mutations(self) -> Iterator[audioproc.Mutation]:
|
||||
for port in self.description.ports:
|
||||
|
@ -201,10 +220,6 @@ class PipelineGraphNode(pmodel.PipelineGraphNode, BasePipelineGraphNode):
|
|||
# "Mismatching node_uri (Expected %s, got %s)."
|
||||
# % (self.node_uri, preset.node_uri))
|
||||
|
||||
@property
|
||||
def pipeline_node_id(self) -> str:
|
||||
return '%016x' % self.id
|
||||
|
||||
def get_add_mutations(self) -> Iterator[audioproc.Mutation]:
|
||||
yield audioproc.AddNode(
|
||||
description=self.description,
|
||||
|
@ -232,103 +247,11 @@ class AudioOutPipelineGraphNode(pmodel.AudioOutPipelineGraphNode, BasePipelineGr
|
|||
yield from []
|
||||
|
||||
|
||||
class TrackMixerPipelineGraphNode(pmodel.TrackMixerPipelineGraphNode, BasePipelineGraphNode):
|
||||
def create(self, *, track: Optional[pmodel.Track] = None, **kwargs: Any) -> None:
|
||||
super().create(**kwargs)
|
||||
|
||||
self.track = track
|
||||
|
||||
@property
|
||||
def pipeline_node_id(self) -> str:
|
||||
return self.track.mixer_name
|
||||
|
||||
def get_add_mutations(self) -> Iterator[audioproc.Mutation]:
|
||||
yield audioproc.AddNode(
|
||||
description=self.description,
|
||||
id=self.pipeline_node_id,
|
||||
name=self.name)
|
||||
|
||||
yield from self.get_initial_parameter_mutations()
|
||||
|
||||
def get_remove_mutations(self) -> Iterator[audioproc.Mutation]:
|
||||
yield audioproc.RemoveNode(self.pipeline_node_id)
|
||||
|
||||
|
||||
class CVGeneratorPipelineGraphNode(pmodel.CVGeneratorPipelineGraphNode, BasePipelineGraphNode):
|
||||
def create(self, *, track: Optional[pmodel.Track] = None, **kwargs: Any) -> None:
|
||||
super().create(**kwargs)
|
||||
|
||||
self.track = track
|
||||
|
||||
@property
|
||||
def pipeline_node_id(self) -> str:
|
||||
return down_cast(pmodel.ControlTrack, self.track).generator_name
|
||||
|
||||
def get_add_mutations(self) -> Iterator[audioproc.Mutation]:
|
||||
yield audioproc.AddNode(
|
||||
description=self.description,
|
||||
id=self.pipeline_node_id,
|
||||
name=self.name)
|
||||
|
||||
yield from self.get_initial_parameter_mutations()
|
||||
|
||||
def get_remove_mutations(self) -> Iterator[audioproc.Mutation]:
|
||||
yield audioproc.RemoveNode(self.pipeline_node_id)
|
||||
|
||||
|
||||
class SampleScriptPipelineGraphNode(pmodel.SampleScriptPipelineGraphNode, BasePipelineGraphNode):
|
||||
def create(self, *, track: Optional[pmodel.Track] = None, **kwargs: Any) -> None:
|
||||
super().create(**kwargs)
|
||||
|
||||
self.track = track
|
||||
|
||||
@property
|
||||
def pipeline_node_id(self) -> str:
|
||||
return down_cast(pmodel.SampleTrack, self.track).sample_script_name
|
||||
|
||||
def get_add_mutations(self) -> Iterator[audioproc.Mutation]:
|
||||
yield audioproc.AddNode(
|
||||
description=self.description,
|
||||
id=self.pipeline_node_id,
|
||||
name=self.name)
|
||||
|
||||
yield from self.get_initial_parameter_mutations()
|
||||
|
||||
def get_remove_mutations(self) -> Iterator[audioproc.Mutation]:
|
||||
yield audioproc.RemoveNode(self.pipeline_node_id)
|
||||
|
||||
|
||||
class PianoRollPipelineGraphNode(pmodel.PianoRollPipelineGraphNode, BasePipelineGraphNode):
|
||||
def create(self, *, track: Optional[pmodel.Track] = None, **kwargs: Any) -> None:
|
||||
super().create(**kwargs)
|
||||
|
||||
self.track = track
|
||||
|
||||
@property
|
||||
def pipeline_node_id(self) -> str:
|
||||
return cast(Union[pmodel.ScoreTrack, pmodel.BeatTrack], self.track).event_source_name
|
||||
|
||||
def get_add_mutations(self) -> Iterator[audioproc.Mutation]:
|
||||
yield audioproc.AddNode(
|
||||
description=self.description,
|
||||
id=self.pipeline_node_id,
|
||||
name=self.name)
|
||||
|
||||
yield from self.get_initial_parameter_mutations()
|
||||
|
||||
def get_remove_mutations(self) -> Iterator[audioproc.Mutation]:
|
||||
yield audioproc.RemoveNode(self.pipeline_node_id)
|
||||
|
||||
|
||||
class InstrumentPipelineGraphNode(pmodel.InstrumentPipelineGraphNode, BasePipelineGraphNode):
|
||||
def create(self, *, track: Optional[pmodel.Track] = None, **kwargs: Any) -> None:
|
||||
def create(self, *, instrument_uri: Optional[str] = None, **kwargs: Any) -> None:
|
||||
super().create(**kwargs)
|
||||
|
||||
self.track = track
|
||||
|
||||
@property
|
||||
def pipeline_node_id(self) -> str:
|
||||
return cast(Union[pmodel.ScoreTrack, pmodel.BeatTrack], self.track).instr_name
|
||||
self.instrument_uri = instrument_uri
|
||||
|
||||
def get_update_mutations(self) -> Iterator[audioproc.Mutation]:
|
||||
connections = [] # type: List[pmodel.PipelineGraphConnection]
|
||||
|
@ -345,8 +268,7 @@ class InstrumentPipelineGraphNode(pmodel.InstrumentPipelineGraphNode, BasePipeli
|
|||
|
||||
def get_add_mutations(self) -> Iterator[audioproc.Mutation]:
|
||||
node_description = instrument_db.parse_uri(
|
||||
cast(Union[pmodel.ScoreTrack, pmodel.BeatTrack], self.track).instrument,
|
||||
self.project.get_node_description)
|
||||
self.instrument_uri, self.project.get_node_description)
|
||||
yield audioproc.AddNode(
|
||||
description=node_description,
|
||||
id=self.pipeline_node_id,
|
||||
|
|
|
@ -31,9 +31,9 @@ from . import commands_test
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PipelineGraphTest(commands_test.CommandsTestBase):
|
||||
class PipelineGraphTest(commands_test.CommandsTestMixin, unittest.AsyncTestCase):
|
||||
async def test_basic(self):
|
||||
mixer_node = self.project.master_group.mixer_node
|
||||
audio_out = self.project.audio_out_node
|
||||
|
||||
node_id = await self.client.send_command(commands_pb2.Command(
|
||||
target=self.project.id,
|
||||
|
@ -47,7 +47,7 @@ class PipelineGraphTest(commands_test.CommandsTestBase):
|
|||
target=self.project.id,
|
||||
add_pipeline_graph_connection=commands_pb2.AddPipelineGraphConnection(
|
||||
source_node_id=node_id, source_port_name='out:left',
|
||||
dest_node_id=mixer_node.id, dest_port_name='in:left')))
|
||||
dest_node_id=audio_out.id, dest_port_name='in:left')))
|
||||
conn1 = self.pool[conn1_id]
|
||||
self.assertIs(conn1, self.project.pipeline_graph_connections[conn1.index])
|
||||
|
||||
|
@ -55,7 +55,7 @@ class PipelineGraphTest(commands_test.CommandsTestBase):
|
|||
target=self.project.id,
|
||||
add_pipeline_graph_connection=commands_pb2.AddPipelineGraphConnection(
|
||||
source_node_id=node_id, source_port_name='out:right',
|
||||
dest_node_id=mixer_node.id, dest_port_name='in:right')))
|
||||
dest_node_id=audio_out.id, dest_port_name='in:right')))
|
||||
conn2 = self.pool[conn2_id]
|
||||
self.assertIs(conn2, self.project.pipeline_graph_connections[conn2.index])
|
||||
|
||||
|
@ -75,7 +75,7 @@ class PipelineGraphTest(commands_test.CommandsTestBase):
|
|||
self.assertNotIn(conn1, self.project.pipeline_graph_connections)
|
||||
self.assertNotIn(conn2, self.project.pipeline_graph_connections)
|
||||
|
||||
async def test_set_pipeline_graph_node_pos(self):
|
||||
async def test_change_graph_pos(self):
|
||||
node_id = await self.client.send_command(commands_pb2.Command(
|
||||
target=self.project.id,
|
||||
add_pipeline_graph_node=commands_pb2.AddPipelineGraphNode(
|
||||
|
@ -85,7 +85,7 @@ class PipelineGraphTest(commands_test.CommandsTestBase):
|
|||
|
||||
await self.client.send_command(commands_pb2.Command(
|
||||
target=node.id,
|
||||
set_pipeline_graph_node_pos=commands_pb2.SetPipelineGraphNodePos(
|
||||
change_pipeline_graph_node=commands_pb2.ChangePipelineGraphNode(
|
||||
graph_pos=model.Pos2F(100, 300).to_proto())))
|
||||
self.assertEqual(node.graph_pos, model.Pos2F(100, 300))
|
||||
|
||||
|
|
|
@ -32,7 +32,6 @@ from noisicaa import audioproc
|
|||
from noisicaa import model
|
||||
|
||||
from . import pmodel
|
||||
from . import track_group
|
||||
from . import base_track # pylint: disable=unused-import
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -89,10 +88,12 @@ class Player(object):
|
|||
duration=self.project.duration)
|
||||
|
||||
messages = audioproc.ProcessorMessageList()
|
||||
messages.messages.extend(self.add_track(self.project.master_group))
|
||||
await self.audioproc_client.send_node_messages(
|
||||
self.realm, messages)
|
||||
for node in self.project.pipeline_graph_nodes:
|
||||
messages.messages.extend(self.add_node(node))
|
||||
await self.audioproc_client.send_node_messages(self.realm, messages)
|
||||
|
||||
self.__listeners['project:nodes'] = self.project.pipeline_graph_nodes_changed.add(
|
||||
self.__on_project_nodes_changed)
|
||||
self.__listeners['project:bpm'] = self.project.bpm_changed.add(
|
||||
self.__on_project_bpm_changed)
|
||||
self.__listeners['project:duration'] = self.project.duration_changed.add(
|
||||
|
@ -163,36 +164,28 @@ class Player(object):
|
|||
if exc is not None:
|
||||
logger.error("PLAYER_STATUS failed with exception: %s", exc)
|
||||
|
||||
def tracks_changed(self, change: model.PropertyChange) -> None:
|
||||
def __on_project_nodes_changed(self, change: model.PropertyChange) -> None:
|
||||
if isinstance(change, model.PropertyListInsert):
|
||||
messages = audioproc.ProcessorMessageList()
|
||||
messages.messages.extend(self.add_track(change.new_value))
|
||||
messages.messages.extend(self.add_node(change.new_value))
|
||||
self.send_node_messages(messages)
|
||||
|
||||
elif isinstance(change, model.PropertyListDelete):
|
||||
self.remove_track(change.old_value)
|
||||
self.remove_node(change.old_value)
|
||||
else:
|
||||
raise TypeError("Unsupported change type %s" % type(change))
|
||||
|
||||
def add_track(self, track: pmodel.Track) -> Iterator[audioproc.ProcessorMessage]:
|
||||
for t in track.walk_tracks(groups=True, tracks=True):
|
||||
if isinstance(t, track_group.TrackGroup):
|
||||
self.__listeners['track_group:%s' % t.id] = t.tracks_changed.add(
|
||||
self.tracks_changed)
|
||||
else:
|
||||
assert isinstance(t, base_track.Track)
|
||||
connector = cast(
|
||||
base_track.TrackConnector,
|
||||
t.create_track_connector(message_cb=self.send_node_message))
|
||||
yield from connector.init()
|
||||
self.track_connectors[t.id] = connector
|
||||
def add_node(self, node: pmodel.BasePipelineGraphNode) -> Iterator[audioproc.ProcessorMessage]:
|
||||
if isinstance(node, base_track.Track):
|
||||
connector = cast(
|
||||
base_track.TrackConnector,
|
||||
node.create_track_connector(message_cb=self.send_node_message))
|
||||
yield from connector.init()
|
||||
self.track_connectors[node.id] = connector
|
||||
|
||||
def remove_track(self, track: pmodel.Track) -> None:
|
||||
for t in track.walk_tracks(groups=True, tracks=True):
|
||||
if isinstance(t, pmodel.TrackGroup):
|
||||
self.__listeners.pop('track_group:%s' % t.id).remove()
|
||||
else:
|
||||
self.track_connectors.pop(t.id).close()
|
||||
def remove_node(self, node: pmodel.BasePipelineGraphNode) -> None:
|
||||
if isinstance(node, base_track.Track):
|
||||
self.track_connectors.pop(node.id).close()
|
||||
|
||||
def handle_pipeline_mutation(self, mutation: audioproc.Mutation) -> None:
|
||||
self.event_loop.create_task(self.publish_pipeline_mutation(mutation))
|
||||
|
|
|
@ -32,7 +32,6 @@ import os.path
|
|||
from noisidev import unittest
|
||||
from noisidev import unittest_mixins
|
||||
from noisidev import demo_project
|
||||
from noisidev import perf_stats
|
||||
from noisicaa.constants import TEST_OPTS
|
||||
from noisicaa import core
|
||||
from noisicaa import audioproc
|
||||
|
@ -74,6 +73,7 @@ class CallbackServer(ipc.Server):
|
|||
log_level=logging.DEBUG)
|
||||
|
||||
async def handle_player_update(self, player_id, kwargs):
|
||||
logging.info("XXX %s %s", player_id, kwargs)
|
||||
self.player_status_calls.put_nowait(kwargs)
|
||||
|
||||
async def wait_for(self, name, value=UNSET):
|
||||
|
@ -130,9 +130,12 @@ class PlayerTest(
|
|||
await self.audioproc_client_main.setup()
|
||||
await self.audioproc_client_main.connect(
|
||||
self.audioproc_address_main, flags={'perf_data'})
|
||||
await self.audioproc_client_main.create_realm(name='root', enable_player=True)
|
||||
await self.audioproc_client_main.set_backend(TEST_OPTS.PLAYBACK_BACKEND)
|
||||
|
||||
self.project = demo_project.complex(project.BaseProject, node_db=self.node_db_client)
|
||||
self.pool = project.Pool()
|
||||
self.project = demo_project.complex(
|
||||
self.pool, project.BaseProject, node_db=self.node_db_client)
|
||||
|
||||
logger.info("Testcase setup complete.")
|
||||
|
||||
|
@ -148,39 +151,42 @@ class PlayerTest(
|
|||
|
||||
@contextlib.contextmanager
|
||||
def track_frame_stats(self, testname):
|
||||
frame_times = []
|
||||
yield
|
||||
return
|
||||
|
||||
def log_stats(spans, parent_id, indent):
|
||||
for span in spans:
|
||||
if span.parentId == parent_id:
|
||||
logger.debug(
|
||||
"%-40s: %10.3fµs",
|
||||
' ' * indent + span.name,
|
||||
(span.endTimeNSec - span.startTimeNSec) / 1000.0)
|
||||
log_stats(spans, span.id, indent+1)
|
||||
# frame_times = []
|
||||
|
||||
def cb(status):
|
||||
perf_data = status.get('perf_data', None)
|
||||
if perf_data:
|
||||
topspan = perf_data.spans[0]
|
||||
assert topspan.parentId == 0
|
||||
assert topspan.name == 'frame'
|
||||
duration = (topspan.endTimeNSec - topspan.startTimeNSec) / 1000.0
|
||||
frame_times.append(duration)
|
||||
log_stats(sorted(perf_data.spans, key=lambda s: s.startTimeNSec), 0, 0)
|
||||
# def log_stats(spans, parent_id, indent):
|
||||
# for span in spans:
|
||||
# if span.parentId == parent_id:
|
||||
# logger.debug(
|
||||
# "%-40s: %10.3fµs",
|
||||
# ' ' * indent + span.name,
|
||||
# (span.endTimeNSec - span.startTimeNSec) / 1000.0)
|
||||
# log_stats(spans, span.id, indent+1)
|
||||
|
||||
listener = self.audioproc_client_main.listeners.add('pipeline_status', cb)
|
||||
try:
|
||||
yield
|
||||
# def cb(status):
|
||||
# perf_data = status.get('perf_data', None)
|
||||
# if perf_data:
|
||||
# topspan = perf_data.spans[0]
|
||||
# assert topspan.parentId == 0
|
||||
# assert topspan.name == 'frame'
|
||||
# duration = (topspan.endTimeNSec - topspan.startTimeNSec) / 1000.0
|
||||
# frame_times.append(duration)
|
||||
# log_stats(sorted(perf_data.spans, key=lambda s: s.startTimeNSec), 0, 0)
|
||||
|
||||
perf_stats.write_frame_stats(
|
||||
os.path.splitext(os.path.basename(__file__))[0],
|
||||
testname, frame_times)
|
||||
# listener = self.audioproc_client_main.listeners.add('pipeline_status', cb)
|
||||
# try:
|
||||
# yield
|
||||
|
||||
finally:
|
||||
listener.remove()
|
||||
# perf_stats.write_frame_stats(
|
||||
# os.path.splitext(os.path.basename(__file__))[0],
|
||||
# testname, frame_times)
|
||||
|
||||
@unittest.skip("TODO: async status updates are flaky")
|
||||
# finally:
|
||||
# listener.remove()
|
||||
|
||||
#@unittest.skip("TODO: async status updates are flaky")
|
||||
@unittest.tag('integration')
|
||||
async def test_playback_demo(self):
|
||||
p = player.Player(
|
||||
|
@ -195,12 +201,10 @@ class PlayerTest(
|
|||
logger.info("Player setup complete.")
|
||||
|
||||
logger.info("Wait until audioproc is ready...")
|
||||
self.assertEqual(
|
||||
await self.callback_server.wait_for('pipeline_state'),
|
||||
'starting')
|
||||
self.assertEqual(
|
||||
await self.callback_server.wait_for('pipeline_state'),
|
||||
'running')
|
||||
# self.assertEqual(
|
||||
# await self.callback_server.wait_for('pipeline_state'), 'starting')
|
||||
# self.assertEqual(
|
||||
# await self.callback_server.wait_for('pipeline_state'), 'running')
|
||||
|
||||
with self.track_frame_stats('playback_demo'):
|
||||
logger.info("Start playback...")
|
||||
|
|
|
@ -31,6 +31,7 @@ from noisicaa.core import ipc
|
|||
from noisicaa.constants import TEST_OPTS
|
||||
from . import project
|
||||
from . import player
|
||||
from . import score_track
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -98,5 +99,9 @@ class PlayerTest(unittest.AsyncTestCase):
|
|||
try:
|
||||
await p.setup()
|
||||
|
||||
track1 = self.pool.create(score_track.ScoreTrack, name="Track 1")
|
||||
self.project.add_pipeline_graph_node(track1)
|
||||
|
||||
|
||||
finally:
|
||||
await p.cleanup()
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
#
|
||||
# @end:license
|
||||
|
||||
from typing import cast, Any, Iterator, MutableSequence, Sequence
|
||||
from typing import Any, Optional, Iterator, MutableSequence
|
||||
|
||||
from noisicaa.core.typing_extra import down_cast
|
||||
from noisicaa import audioproc
|
||||
|
@ -49,373 +49,6 @@ class TrackConnector(object):
|
|||
pass
|
||||
|
||||
|
||||
class Track(ProjectChild, model.Track, ObjectBase):
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return self.get_property_value('name')
|
||||
|
||||
@name.setter
|
||||
def name(self, value: str) -> None:
|
||||
self.set_property_value('name', value)
|
||||
|
||||
@property
|
||||
def visible(self) -> bool:
|
||||
return self.get_property_value('visible')
|
||||
|
||||
@visible.setter
|
||||
def visible(self, value: bool) -> None:
|
||||
self.set_property_value('visible', value)
|
||||
|
||||
@property
|
||||
def muted(self) -> bool:
|
||||
return self.get_property_value('muted')
|
||||
|
||||
@muted.setter
|
||||
def muted(self, value: bool) -> None:
|
||||
self.set_property_value('muted', value)
|
||||
|
||||
@property
|
||||
def gain(self) -> float:
|
||||
return self.get_property_value('gain')
|
||||
|
||||
@gain.setter
|
||||
def gain(self, value: float) -> None:
|
||||
self.set_property_value('gain', value)
|
||||
|
||||
@property
|
||||
def pan(self) -> float:
|
||||
return self.get_property_value('pan')
|
||||
|
||||
@pan.setter
|
||||
def pan(self, value: float) -> None:
|
||||
self.set_property_value('pan', value)
|
||||
|
||||
@property
|
||||
def mixer_node(self) -> 'BasePipelineGraphNode':
|
||||
return self.get_property_value('mixer_node')
|
||||
|
||||
@mixer_node.setter
|
||||
def mixer_node(self, value: 'BasePipelineGraphNode') -> None:
|
||||
self.set_property_value('mixer_node', value)
|
||||
|
||||
@property
|
||||
def mixer_name(self) -> str:
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def parent_audio_sink_name(self) -> str:
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def parent_audio_sink_node(self) -> 'BasePipelineGraphNode':
|
||||
raise NotImplementedError
|
||||
|
||||
def create_track_connector(self, **kwargs: Any) -> TrackConnector:
|
||||
raise NotImplementedError
|
||||
|
||||
def add_pipeline_nodes(self) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
def remove_pipeline_nodes(self) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class Measure(ProjectChild, model.Measure, ObjectBase):
|
||||
pass
|
||||
|
||||
|
||||
class MeasureReference(ProjectChild, model.MeasureReference, ObjectBase):
|
||||
@property
|
||||
def measure(self) -> Measure:
|
||||
return self.get_property_value('measure')
|
||||
|
||||
@measure.setter
|
||||
def measure(self, value: Measure) -> None:
|
||||
self.set_property_value('measure', value)
|
||||
|
||||
|
||||
class MeasuredTrack(Track, model.MeasuredTrack, ObjectBase):
|
||||
@property
|
||||
def measure_list(self) -> MutableSequence[MeasureReference]:
|
||||
return self.get_property_value('measure_list')
|
||||
|
||||
@property
|
||||
def measure_heap(self) -> MutableSequence[Measure]:
|
||||
return self.get_property_value('measure_heap')
|
||||
|
||||
def garbage_collect_measures(self) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class Note(ProjectChild, model.Note, ObjectBase):
|
||||
@property
|
||||
def pitches(self) -> MutableSequence[model.Pitch]:
|
||||
return self.get_property_value('pitches')
|
||||
|
||||
@property
|
||||
def base_duration(self) -> audioproc.MusicalDuration:
|
||||
return self.get_property_value('base_duration')
|
||||
|
||||
@base_duration.setter
|
||||
def base_duration(self, value: audioproc.MusicalDuration) -> None:
|
||||
self.set_property_value('base_duration', value)
|
||||
|
||||
@property
|
||||
def dots(self) -> int:
|
||||
return self.get_property_value('dots')
|
||||
|
||||
@dots.setter
|
||||
def dots(self, value: int) -> None:
|
||||
self.set_property_value('dots', value)
|
||||
|
||||
@property
|
||||
def tuplet(self) -> int:
|
||||
return self.get_property_value('tuplet')
|
||||
|
||||
@tuplet.setter
|
||||
def tuplet(self, value: int) -> None:
|
||||
self.set_property_value('tuplet', value)
|
||||
|
||||
@property
|
||||
def measure(self) -> 'ScoreMeasure':
|
||||
return down_cast(ScoreMeasure, super().measure)
|
||||
|
||||
|
||||
class TrackGroup(Track, model.TrackGroup, ObjectBase):
|
||||
@property
|
||||
def tracks(self) -> MutableSequence[Track]:
|
||||
return self.get_property_value('tracks')
|
||||
|
||||
|
||||
class MasterTrackGroup(TrackGroup, model.MasterTrackGroup, ObjectBase):
|
||||
pass
|
||||
|
||||
|
||||
class ScoreMeasure(Measure, model.ScoreMeasure, ObjectBase):
|
||||
@property
|
||||
def clef(self) -> model.Clef:
|
||||
return self.get_property_value('clef')
|
||||
|
||||
@clef.setter
|
||||
def clef(self, value: model.Clef) -> None:
|
||||
self.set_property_value('clef', value)
|
||||
|
||||
@property
|
||||
def key_signature(self) -> model.KeySignature:
|
||||
return self.get_property_value('key_signature')
|
||||
|
||||
@key_signature.setter
|
||||
def key_signature(self, value: model.KeySignature) -> None:
|
||||
self.set_property_value('key_signature', value)
|
||||
|
||||
@property
|
||||
def notes(self) -> MutableSequence[Note]:
|
||||
return self.get_property_value('notes')
|
||||
|
||||
@property
|
||||
def track(self) -> 'ScoreTrack':
|
||||
return down_cast(ScoreTrack, super().track)
|
||||
|
||||
|
||||
class ScoreTrack(MeasuredTrack, model.ScoreTrack, ObjectBase):
|
||||
@property
|
||||
def instrument(self) -> str:
|
||||
return self.get_property_value('instrument')
|
||||
|
||||
@instrument.setter
|
||||
def instrument(self, value: str) -> None:
|
||||
self.set_property_value('instrument', value)
|
||||
|
||||
@property
|
||||
def transpose_octaves(self) -> int:
|
||||
return self.get_property_value('transpose_octaves')
|
||||
|
||||
@transpose_octaves.setter
|
||||
def transpose_octaves(self, value: int) -> None:
|
||||
self.set_property_value('transpose_octaves', value)
|
||||
|
||||
@property
|
||||
def instrument_node(self) -> 'InstrumentPipelineGraphNode':
|
||||
return self.get_property_value('instrument_node')
|
||||
|
||||
@instrument_node.setter
|
||||
def instrument_node(self, value: 'InstrumentPipelineGraphNode') -> None:
|
||||
self.set_property_value('instrument_node', value)
|
||||
|
||||
@property
|
||||
def event_source_node(self) -> 'PianoRollPipelineGraphNode':
|
||||
return self.get_property_value('event_source_node')
|
||||
|
||||
@event_source_node.setter
|
||||
def event_source_node(self, value: 'PianoRollPipelineGraphNode') -> None:
|
||||
self.set_property_value('event_source_node', value)
|
||||
|
||||
@property
|
||||
def event_source_name(self) -> str:
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def instr_name(self) -> str:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class Beat(ProjectChild, model.Beat, ObjectBase):
|
||||
@property
|
||||
def time(self) -> audioproc.MusicalDuration:
|
||||
return audioproc.MusicalDuration.from_proto(self.get_property_value('time'))
|
||||
|
||||
@time.setter
|
||||
def time(self, value: audioproc.MusicalDuration) -> None:
|
||||
self.set_property_value('time', value.to_proto())
|
||||
|
||||
@property
|
||||
def velocity(self) -> int:
|
||||
return self.get_property_value('velocity')
|
||||
|
||||
@velocity.setter
|
||||
def velocity(self, value: int) -> None:
|
||||
self.set_property_value('velocity', value)
|
||||
|
||||
@property
|
||||
def measure(self) -> 'BeatMeasure':
|
||||
return down_cast(BeatMeasure, super().measure)
|
||||
|
||||
|
||||
class BeatMeasure(Measure, model.BeatMeasure, ObjectBase):
|
||||
@property
|
||||
def beats(self) -> MutableSequence[Beat]:
|
||||
return self.get_property_value('beats')
|
||||
|
||||
|
||||
class BeatTrack(MeasuredTrack, model.BeatTrack, ObjectBase):
|
||||
@property
|
||||
def instrument(self) -> str:
|
||||
return self.get_property_value('instrument')
|
||||
|
||||
@instrument.setter
|
||||
def instrument(self, value: str) -> None:
|
||||
self.set_property_value('instrument', value)
|
||||
|
||||
@property
|
||||
def pitch(self) -> model.Pitch:
|
||||
return self.get_property_value('pitch')
|
||||
|
||||
@pitch.setter
|
||||
def pitch(self, value: model.Pitch) -> None:
|
||||
self.set_property_value('pitch', value)
|
||||
|
||||
@property
|
||||
def instrument_node(self) -> 'InstrumentPipelineGraphNode':
|
||||
return self.get_property_value('instrument_node')
|
||||
|
||||
@instrument_node.setter
|
||||
def instrument_node(self, value: 'InstrumentPipelineGraphNode') -> None:
|
||||
self.set_property_value('instrument_node', value)
|
||||
|
||||
@property
|
||||
def event_source_node(self) -> 'PianoRollPipelineGraphNode':
|
||||
return self.get_property_value('event_source_node')
|
||||
|
||||
@event_source_node.setter
|
||||
def event_source_node(self, value: 'PianoRollPipelineGraphNode') -> None:
|
||||
self.set_property_value('event_source_node', value)
|
||||
|
||||
@property
|
||||
def event_source_name(self) -> str:
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def instr_name(self) -> str:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class PropertyMeasure(Measure, model.PropertyMeasure, ObjectBase):
|
||||
@property
|
||||
def time_signature(self) -> model.TimeSignature:
|
||||
return self.get_property_value('time_signature')
|
||||
|
||||
@time_signature.setter
|
||||
def time_signature(self, value: model.TimeSignature) -> None:
|
||||
self.set_property_value('time_signature', value)
|
||||
|
||||
|
||||
class PropertyTrack(MeasuredTrack, model.PropertyTrack):
|
||||
pass
|
||||
|
||||
|
||||
class ControlPoint(ProjectChild, model.ControlPoint, ObjectBase):
|
||||
@property
|
||||
def time(self) -> audioproc.MusicalTime:
|
||||
return self.get_property_value('time')
|
||||
|
||||
@time.setter
|
||||
def time(self, value: audioproc.MusicalTime) -> None:
|
||||
self.set_property_value('time', value)
|
||||
|
||||
@property
|
||||
def value(self) -> float:
|
||||
return self.get_property_value('value')
|
||||
|
||||
@value.setter
|
||||
def value(self, value: float) -> None:
|
||||
self.set_property_value('value', value)
|
||||
|
||||
|
||||
class ControlTrack(Track, model.ControlTrack):
|
||||
@property
|
||||
def points(self) -> MutableSequence[ControlPoint]:
|
||||
return self.get_property_value('points')
|
||||
|
||||
@property
|
||||
def generator_node(self) -> 'CVGeneratorPipelineGraphNode':
|
||||
return self.get_property_value('generator_node')
|
||||
|
||||
@generator_node.setter
|
||||
def generator_node(self, value: 'CVGeneratorPipelineGraphNode') -> None:
|
||||
self.set_property_value('generator_node', value)
|
||||
|
||||
@property
|
||||
def generator_name(self) -> str:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class SampleRef(ProjectChild, model.SampleRef, ObjectBase):
|
||||
@property
|
||||
def time(self) -> audioproc.MusicalTime:
|
||||
return self.get_property_value('time')
|
||||
|
||||
@time.setter
|
||||
def time(self, value: audioproc.MusicalTime) -> None:
|
||||
self.set_property_value('time', value)
|
||||
|
||||
@property
|
||||
def sample(self) -> 'Sample':
|
||||
return self.get_property_value('sample')
|
||||
|
||||
@sample.setter
|
||||
def sample(self, value: 'Sample') -> None:
|
||||
self.set_property_value('sample', value)
|
||||
|
||||
|
||||
class SampleTrack(Track, model.SampleTrack, ObjectBase):
|
||||
@property
|
||||
def samples(self) -> MutableSequence[SampleRef]:
|
||||
return self.get_property_value('samples')
|
||||
|
||||
@property
|
||||
def sample_script_node(self) -> 'SampleScriptPipelineGraphNode':
|
||||
return self.get_property_value('sample_script_node')
|
||||
|
||||
@sample_script_node.setter
|
||||
def sample_script_node(self, value: 'SampleScriptPipelineGraphNode') -> None:
|
||||
self.set_property_value('sample_script_node', value)
|
||||
|
||||
@property
|
||||
def sample_script_name(self) -> str:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
|
||||
class PipelineGraphControlValue(ProjectChild, model.PipelineGraphControlValue, ObjectBase):
|
||||
@property
|
||||
def name(self) -> str:
|
||||
|
@ -451,6 +84,22 @@ class BasePipelineGraphNode(ProjectChild, model.BasePipelineGraphNode, ObjectBas
|
|||
def graph_pos(self, value: model.Pos2F) -> None:
|
||||
self.set_property_value('graph_pos', value)
|
||||
|
||||
@property
|
||||
def graph_size(self) -> model.SizeF:
|
||||
return self.get_property_value('graph_size')
|
||||
|
||||
@graph_size.setter
|
||||
def graph_size(self, value: model.SizeF) -> None:
|
||||
self.set_property_value('graph_size', value)
|
||||
|
||||
@property
|
||||
def graph_color(self) -> model.Color:
|
||||
return self.get_property_value('graph_color')
|
||||
|
||||
@graph_color.setter
|
||||
def graph_color(self, value: model.Color) -> None:
|
||||
self.set_property_value('graph_color', value)
|
||||
|
||||
@property
|
||||
def control_values(self) -> MutableSequence[PipelineGraphControlValue]:
|
||||
return self.get_property_value('control_values')
|
||||
|
@ -505,59 +154,237 @@ class AudioOutPipelineGraphNode(
|
|||
pass
|
||||
|
||||
|
||||
class TrackMixerPipelineGraphNode(
|
||||
BasePipelineGraphNode, model.TrackMixerPipelineGraphNode, ObjectBase):
|
||||
class Track(BasePipelineGraphNode, model.Track, ObjectBase):
|
||||
@property
|
||||
def track(self) -> Track:
|
||||
return self.get_property_value('track')
|
||||
def visible(self) -> bool:
|
||||
return self.get_property_value('visible')
|
||||
|
||||
@track.setter
|
||||
def track(self, value: Track) -> None:
|
||||
self.set_property_value('track', value)
|
||||
@visible.setter
|
||||
def visible(self, value: bool) -> None:
|
||||
self.set_property_value('visible', value)
|
||||
|
||||
|
||||
class PianoRollPipelineGraphNode(
|
||||
BasePipelineGraphNode, model.PianoRollPipelineGraphNode, ObjectBase):
|
||||
@property
|
||||
def track(self) -> Track:
|
||||
return self.get_property_value('track')
|
||||
def list_position(self) -> int:
|
||||
return self.get_property_value('list_position')
|
||||
|
||||
@track.setter
|
||||
def track(self, value: Track) -> None:
|
||||
self.set_property_value('track', value)
|
||||
@list_position.setter
|
||||
def list_position(self, value: int) -> None:
|
||||
self.set_property_value('list_position', value)
|
||||
|
||||
def create_track_connector(self, **kwargs: Any) -> TrackConnector:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class CVGeneratorPipelineGraphNode(
|
||||
BasePipelineGraphNode, model.CVGeneratorPipelineGraphNode, ObjectBase):
|
||||
class Measure(ProjectChild, model.Measure, ObjectBase):
|
||||
@property
|
||||
def track(self) -> Track:
|
||||
return self.get_property_value('track')
|
||||
def time_signature(self) -> model.TimeSignature:
|
||||
return self.get_property_value('time_signature')
|
||||
|
||||
@track.setter
|
||||
def track(self, value: Track) -> None:
|
||||
self.set_property_value('track', value)
|
||||
@time_signature.setter
|
||||
def time_signature(self, value: model.TimeSignature) -> None:
|
||||
self.set_property_value('time_signature', value)
|
||||
|
||||
|
||||
class SampleScriptPipelineGraphNode(
|
||||
BasePipelineGraphNode, model.SampleScriptPipelineGraphNode, ObjectBase):
|
||||
class MeasureReference(ProjectChild, model.MeasureReference, ObjectBase):
|
||||
@property
|
||||
def track(self) -> Track:
|
||||
return self.get_property_value('track')
|
||||
def measure(self) -> Measure:
|
||||
return self.get_property_value('measure')
|
||||
|
||||
@track.setter
|
||||
def track(self, value: Track) -> None:
|
||||
self.set_property_value('track', value)
|
||||
@measure.setter
|
||||
def measure(self, value: Measure) -> None:
|
||||
self.set_property_value('measure', value)
|
||||
|
||||
|
||||
class MeasuredTrack(Track, model.MeasuredTrack, ObjectBase):
|
||||
@property
|
||||
def measure_list(self) -> MutableSequence[MeasureReference]:
|
||||
return self.get_property_value('measure_list')
|
||||
|
||||
@property
|
||||
def measure_heap(self) -> MutableSequence[Measure]:
|
||||
return self.get_property_value('measure_heap')
|
||||
|
||||
def append_measure(self) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
def insert_measure(self, idx: int) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
def remove_measure(self, idx: int) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
def create_empty_measure(self, ref: Optional[Measure]) -> Measure:
|
||||
raise NotImplementedError
|
||||
|
||||
def garbage_collect_measures(self) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class ControlPoint(ProjectChild, model.ControlPoint, ObjectBase):
|
||||
@property
|
||||
def time(self) -> audioproc.MusicalTime:
|
||||
return self.get_property_value('time')
|
||||
|
||||
@time.setter
|
||||
def time(self, value: audioproc.MusicalTime) -> None:
|
||||
self.set_property_value('time', value)
|
||||
|
||||
@property
|
||||
def value(self) -> float:
|
||||
return self.get_property_value('value')
|
||||
|
||||
@value.setter
|
||||
def value(self, value: float) -> None:
|
||||
self.set_property_value('value', value)
|
||||
|
||||
|
||||
class ControlTrack(Track, model.ControlTrack, ObjectBase):
|
||||
@property
|
||||
def points(self) -> MutableSequence[ControlPoint]:
|
||||
return self.get_property_value('points')
|
||||
|
||||
|
||||
class Note(ProjectChild, model.Note, ObjectBase):
|
||||
@property
|
||||
def pitches(self) -> MutableSequence[model.Pitch]:
|
||||
return self.get_property_value('pitches')
|
||||
|
||||
@property
|
||||
def base_duration(self) -> audioproc.MusicalDuration:
|
||||
return self.get_property_value('base_duration')
|
||||
|
||||
@base_duration.setter
|
||||
def base_duration(self, value: audioproc.MusicalDuration) -> None:
|
||||
self.set_property_value('base_duration', value)
|
||||
|
||||
@property
|
||||
def dots(self) -> int:
|
||||
return self.get_property_value('dots')
|
||||
|
||||
@dots.setter
|
||||
def dots(self, value: int) -> None:
|
||||
self.set_property_value('dots', value)
|
||||
|
||||
@property
|
||||
def tuplet(self) -> int:
|
||||
return self.get_property_value('tuplet')
|
||||
|
||||
@tuplet.setter
|
||||
def tuplet(self, value: int) -> None:
|
||||
self.set_property_value('tuplet', value)
|
||||
|
||||
@property
|
||||
def measure(self) -> 'ScoreMeasure':
|
||||
return down_cast(ScoreMeasure, super().measure)
|
||||
|
||||
|
||||
class ScoreMeasure(Measure, model.ScoreMeasure, ObjectBase):
|
||||
@property
|
||||
def clef(self) -> model.Clef:
|
||||
return self.get_property_value('clef')
|
||||
|
||||
@clef.setter
|
||||
def clef(self, value: model.Clef) -> None:
|
||||
self.set_property_value('clef', value)
|
||||
|
||||
@property
|
||||
def key_signature(self) -> model.KeySignature:
|
||||
return self.get_property_value('key_signature')
|
||||
|
||||
@key_signature.setter
|
||||
def key_signature(self, value: model.KeySignature) -> None:
|
||||
self.set_property_value('key_signature', value)
|
||||
|
||||
@property
|
||||
def notes(self) -> MutableSequence[Note]:
|
||||
return self.get_property_value('notes')
|
||||
|
||||
@property
|
||||
def track(self) -> 'ScoreTrack':
|
||||
return down_cast(ScoreTrack, super().track)
|
||||
|
||||
|
||||
class ScoreTrack(MeasuredTrack, model.ScoreTrack, ObjectBase):
|
||||
@property
|
||||
def transpose_octaves(self) -> int:
|
||||
return self.get_property_value('transpose_octaves')
|
||||
|
||||
@transpose_octaves.setter
|
||||
def transpose_octaves(self, value: int) -> None:
|
||||
self.set_property_value('transpose_octaves', value)
|
||||
|
||||
|
||||
class Beat(ProjectChild, model.Beat, ObjectBase):
|
||||
@property
|
||||
def time(self) -> audioproc.MusicalDuration:
|
||||
return audioproc.MusicalDuration.from_proto(self.get_property_value('time'))
|
||||
|
||||
@time.setter
|
||||
def time(self, value: audioproc.MusicalDuration) -> None:
|
||||
self.set_property_value('time', value.to_proto())
|
||||
|
||||
@property
|
||||
def velocity(self) -> int:
|
||||
return self.get_property_value('velocity')
|
||||
|
||||
@velocity.setter
|
||||
def velocity(self, value: int) -> None:
|
||||
self.set_property_value('velocity', value)
|
||||
|
||||
@property
|
||||
def measure(self) -> 'BeatMeasure':
|
||||
return down_cast(BeatMeasure, super().measure)
|
||||
|
||||
|
||||
class BeatMeasure(Measure, model.BeatMeasure, ObjectBase):
|
||||
@property
|
||||
def beats(self) -> MutableSequence[Beat]:
|
||||
return self.get_property_value('beats')
|
||||
|
||||
|
||||
class BeatTrack(MeasuredTrack, model.BeatTrack, ObjectBase):
|
||||
@property
|
||||
def pitch(self) -> model.Pitch:
|
||||
return self.get_property_value('pitch')
|
||||
|
||||
@pitch.setter
|
||||
def pitch(self, value: model.Pitch) -> None:
|
||||
self.set_property_value('pitch', value)
|
||||
|
||||
|
||||
class SampleRef(ProjectChild, model.SampleRef, ObjectBase):
|
||||
@property
|
||||
def time(self) -> audioproc.MusicalTime:
|
||||
return self.get_property_value('time')
|
||||
|
||||
@time.setter
|
||||
def time(self, value: audioproc.MusicalTime) -> None:
|
||||
self.set_property_value('time', value)
|
||||
|
||||
@property
|
||||
def sample(self) -> 'Sample':
|
||||
return self.get_property_value('sample')
|
||||
|
||||
@sample.setter
|
||||
def sample(self, value: 'Sample') -> None:
|
||||
self.set_property_value('sample', value)
|
||||
|
||||
|
||||
class SampleTrack(Track, model.SampleTrack, ObjectBase):
|
||||
@property
|
||||
def samples(self) -> MutableSequence[SampleRef]:
|
||||
return self.get_property_value('samples')
|
||||
|
||||
|
||||
class InstrumentPipelineGraphNode(
|
||||
BasePipelineGraphNode, model.InstrumentPipelineGraphNode, ObjectBase):
|
||||
@property
|
||||
def track(self) -> Track:
|
||||
return self.get_property_value('track')
|
||||
def instrument_uri(self) -> str:
|
||||
return self.get_property_value('instrument_uri')
|
||||
|
||||
@track.setter
|
||||
def track(self, value: Track) -> None:
|
||||
self.set_property_value('track', value)
|
||||
@instrument_uri.setter
|
||||
def instrument_uri(self, value: str) -> None:
|
||||
self.set_property_value('instrument_uri', value)
|
||||
|
||||
def get_update_mutations(self) -> Iterator[audioproc.Mutation]:
|
||||
raise NotImplementedError
|
||||
|
@ -648,14 +475,6 @@ class Metadata(ProjectChild, model.Metadata, ObjectBase):
|
|||
|
||||
|
||||
class Project(model.Project, ObjectBase):
|
||||
@property
|
||||
def master_group(self) -> MasterTrackGroup:
|
||||
return self.get_property_value('master_group')
|
||||
|
||||
@master_group.setter
|
||||
def master_group(self, value: MasterTrackGroup) -> None:
|
||||
self.set_property_value('master_group', value)
|
||||
|
||||
@property
|
||||
def metadata(self) -> Metadata:
|
||||
return self.get_property_value('metadata')
|
||||
|
@ -664,14 +483,6 @@ class Project(model.Project, ObjectBase):
|
|||
def metadata(self, value: Metadata) -> None:
|
||||
self.set_property_value('metadata', value)
|
||||
|
||||
@property
|
||||
def property_track(self) -> PropertyTrack:
|
||||
return self.get_property_value('property_track')
|
||||
|
||||
@property_track.setter
|
||||
def property_track(self, value: PropertyTrack) -> None:
|
||||
self.set_property_value('property_track', value)
|
||||
|
||||
@property
|
||||
def pipeline_graph_nodes(self) -> MutableSequence[BasePipelineGraphNode]:
|
||||
return self.get_property_value('pipeline_graph_nodes')
|
||||
|
@ -692,26 +503,16 @@ class Project(model.Project, ObjectBase):
|
|||
def bpm(self, value: int) -> None:
|
||||
self.set_property_value('bpm', value)
|
||||
|
||||
@property
|
||||
def all_tracks(self) -> Sequence[Track]:
|
||||
return cast(Sequence[Track], super().all_tracks)
|
||||
|
||||
@property
|
||||
def project(self) -> 'Project':
|
||||
return down_cast(Project, super().project)
|
||||
|
||||
def add_track(self, parent_group: TrackGroup, insert_index: int, track: Track) -> None:
|
||||
raise NotImplementedError # pragma: no coverage
|
||||
|
||||
def remove_track(self, parent_group: TrackGroup, track: Track) -> None:
|
||||
raise NotImplementedError # pragma: no coverage
|
||||
|
||||
def handle_pipeline_mutation(self, mutation: audioproc.Mutation) -> None:
|
||||
raise NotImplementedError # pragma: no coverage
|
||||
|
||||
@property
|
||||
def audio_out_node(self) -> BasePipelineGraphNode:
|
||||
raise NotImplementedError # pragma: no coverage
|
||||
def audio_out_node(self) -> AudioOutPipelineGraphNode:
|
||||
return down_cast(AudioOutPipelineGraphNode, super().audio_out_node)
|
||||
|
||||
def add_pipeline_graph_node(self, node: BasePipelineGraphNode) -> None:
|
||||
raise NotImplementedError # pragma: no coverage
|
||||
|
|
|
@ -36,8 +36,6 @@ class ModelTest(unittest.TestCase):
|
|||
def setup_testcase(self):
|
||||
self.pool = pmodel.Pool()
|
||||
self.pool.register_class(pmodel.Project)
|
||||
self.pool.register_class(pmodel.TrackGroup)
|
||||
self.pool.register_class(pmodel.MasterTrackGroup)
|
||||
self.pool.register_class(pmodel.MeasureReference)
|
||||
self.pool.register_class(pmodel.ScoreMeasure)
|
||||
self.pool.register_class(pmodel.ScoreTrack)
|
||||
|
@ -48,18 +46,12 @@ class ModelTest(unittest.TestCase):
|
|||
self.pool.register_class(pmodel.SampleTrack)
|
||||
self.pool.register_class(pmodel.ControlPoint)
|
||||
self.pool.register_class(pmodel.ControlTrack)
|
||||
self.pool.register_class(pmodel.PropertyMeasure)
|
||||
self.pool.register_class(pmodel.PropertyTrack)
|
||||
self.pool.register_class(pmodel.Metadata)
|
||||
self.pool.register_class(pmodel.Sample)
|
||||
self.pool.register_class(pmodel.Note)
|
||||
self.pool.register_class(pmodel.PipelineGraphConnection)
|
||||
self.pool.register_class(pmodel.PipelineGraphNode)
|
||||
self.pool.register_class(pmodel.InstrumentPipelineGraphNode)
|
||||
self.pool.register_class(pmodel.TrackMixerPipelineGraphNode)
|
||||
self.pool.register_class(pmodel.SampleScriptPipelineGraphNode)
|
||||
self.pool.register_class(pmodel.CVGeneratorPipelineGraphNode)
|
||||
self.pool.register_class(pmodel.PianoRollPipelineGraphNode)
|
||||
self.pool.register_class(pmodel.AudioOutPipelineGraphNode)
|
||||
self.pool.register_class(pmodel.PipelineGraphControlValue)
|
||||
|
||||
|
@ -71,13 +63,6 @@ class ProjectTest(ModelTest):
|
|||
pr.bpm = 140
|
||||
self.assertEqual(pr.bpm, 140)
|
||||
|
||||
def test_master_group(self):
|
||||
pr = self.pool.create(pmodel.Project)
|
||||
with self.assertRaises(ValueError):
|
||||
pr.master_group # pylint: disable=pointless-statement
|
||||
pr.master_group = self.pool.create(pmodel.MasterTrackGroup)
|
||||
self.assertIsInstance(pr.master_group, pmodel.MasterTrackGroup)
|
||||
|
||||
def test_metadata(self):
|
||||
pr = self.pool.create(pmodel.Project)
|
||||
with self.assertRaises(ValueError):
|
||||
|
@ -85,13 +70,6 @@ class ProjectTest(ModelTest):
|
|||
pr.metadata = self.pool.create(pmodel.Metadata)
|
||||
self.assertIsInstance(pr.metadata, pmodel.Metadata)
|
||||
|
||||
def test_property_track(self):
|
||||
pr = self.pool.create(pmodel.Project)
|
||||
with self.assertRaises(ValueError):
|
||||
pr.property_track # pylint: disable=pointless-statement
|
||||
pr.property_track = self.pool.create(pmodel.PropertyTrack)
|
||||
self.assertIsInstance(pr.property_track, pmodel.PropertyTrack)
|
||||
|
||||
def test_samples(self):
|
||||
pr = self.pool.create(pmodel.Project)
|
||||
self.assertEqual(len(pr.samples), 0)
|
||||
|
@ -177,6 +155,18 @@ class BasePipelineGraphNodeMixin(object):
|
|||
node.graph_pos = model.Pos2F(12, 14)
|
||||
self.assertEqual(node.graph_pos, model.Pos2F(12, 14))
|
||||
|
||||
def test_graph_size(self):
|
||||
node = self.pool.create(self.cls)
|
||||
|
||||
node.graph_size = model.SizeF(20, 32)
|
||||
self.assertEqual(node.graph_size, model.SizeF(20, 32))
|
||||
|
||||
def test_graph_color(self):
|
||||
node = self.pool.create(self.cls)
|
||||
|
||||
node.graph_color = model.Color(0.5, 0.4, 0.3, 0.1)
|
||||
self.assertEqual(node.graph_color, model.Color(0.5, 0.4, 0.3, 0.1))
|
||||
|
||||
def test_plugin_state(self):
|
||||
node = self.pool.create(self.cls)
|
||||
|
||||
|
@ -205,53 +195,8 @@ class InstrumentPipelineGraphNodeTest(BasePipelineGraphNodeMixin, ModelTest):
|
|||
def test_instrument_pipeline_graph_node(self):
|
||||
node = self.pool.create(self.cls)
|
||||
|
||||
track = self.pool.create(pmodel.ScoreTrack)
|
||||
node.track = track
|
||||
self.assertIs(node.track, track)
|
||||
|
||||
|
||||
class SampleScriptPipelineGraphNodeTest(BasePipelineGraphNodeMixin, ModelTest):
|
||||
cls = pmodel.SampleScriptPipelineGraphNode
|
||||
|
||||
def test_sample_script_pipeline_graph_node(self):
|
||||
node = self.pool.create(self.cls)
|
||||
|
||||
track = self.pool.create(pmodel.ScoreTrack)
|
||||
node.track = track
|
||||
self.assertIs(node.track, track)
|
||||
|
||||
|
||||
class PianoRollPipelineGraphNodeTest(BasePipelineGraphNodeMixin, ModelTest):
|
||||
cls = pmodel.PianoRollPipelineGraphNode
|
||||
|
||||
def test_pianoroll_pipeline_graph_node(self):
|
||||
node = self.pool.create(self.cls)
|
||||
|
||||
track = self.pool.create(pmodel.ScoreTrack)
|
||||
node.track = track
|
||||
self.assertIs(node.track, track)
|
||||
|
||||
|
||||
class CVGeneratorPipelineGraphNodeTest(BasePipelineGraphNodeMixin, ModelTest):
|
||||
cls = pmodel.CVGeneratorPipelineGraphNode
|
||||
|
||||
def test_cvgenerator_pipeline_graph_node(self):
|
||||
node = self.pool.create(self.cls)
|
||||
|
||||
track = self.pool.create(pmodel.ScoreTrack)
|
||||
node.track = track
|
||||
self.assertIs(node.track, track)
|
||||
|
||||
|
||||
class TrackMixerPipelineGraphNodeTest(BasePipelineGraphNodeMixin, ModelTest):
|
||||
cls = pmodel.TrackMixerPipelineGraphNode
|
||||
|
||||
def test_track_mixer_pipeline_graph_node(self):
|
||||
node = self.pool.create(self.cls)
|
||||
|
||||
track = self.pool.create(pmodel.ScoreTrack)
|
||||
node.track = track
|
||||
self.assertIs(node.track, track)
|
||||
node.instrument_uri = 'sf2:/path.sf2?bank=1&preset=3'
|
||||
self.assertEqual(node.instrument_uri, 'sf2:/path.sf2?bank=1&preset=3')
|
||||
|
||||
|
||||
class PipelineGraphNodeTest(BasePipelineGraphNodeMixin, ModelTest):
|
||||
|
@ -280,50 +225,9 @@ class PipelineGraphControlValueTest(ModelTest):
|
|||
model.ControlValue(value=12, generation=1))
|
||||
|
||||
|
||||
class TrackMixin(object):
|
||||
class TrackMixin(BasePipelineGraphNodeMixin):
|
||||
cls = None # type: Type[pmodel.Track]
|
||||
|
||||
def test_name(self):
|
||||
track = self.pool.create(self.cls)
|
||||
|
||||
track.name = 'track 1'
|
||||
self.assertEqual(track.name, 'track 1')
|
||||
|
||||
def test_visible(self):
|
||||
track = self.pool.create(self.cls)
|
||||
|
||||
self.assertTrue(track.visible)
|
||||
track.visible = False
|
||||
self.assertFalse(track.visible)
|
||||
|
||||
def test_muted(self):
|
||||
track = self.pool.create(self.cls)
|
||||
|
||||
self.assertFalse(track.muted)
|
||||
track.muted = True
|
||||
self.assertTrue(track.muted)
|
||||
|
||||
def test_gain(self):
|
||||
track = self.pool.create(self.cls)
|
||||
|
||||
self.assertEqual(track.gain, 0.0)
|
||||
track.gain = 1.0
|
||||
self.assertEqual(track.gain, 1.0)
|
||||
|
||||
def test_pan(self):
|
||||
track = self.pool.create(self.cls)
|
||||
|
||||
self.assertEqual(track.pan, 0.0)
|
||||
track.pan = 1.0
|
||||
self.assertEqual(track.pan, 1.0)
|
||||
|
||||
def test_track_mixer_node(self):
|
||||
track = self.pool.create(self.cls)
|
||||
|
||||
node = self.pool.create(pmodel.TrackMixerPipelineGraphNode)
|
||||
track.mixer_node = node
|
||||
self.assertIs(track.mixer_node, node)
|
||||
|
||||
|
||||
class MeasuredTrackMixin(TrackMixin):
|
||||
cls = None # type: Type[pmodel.MeasuredTrack]
|
||||
|
@ -354,13 +258,6 @@ class SampleTrackTest(TrackMixin, ModelTest):
|
|||
track.samples.append(smpl)
|
||||
self.assertIs(track.samples[0], smpl)
|
||||
|
||||
def test_sample_script_node(self):
|
||||
track = self.pool.create(self.cls)
|
||||
|
||||
node = self.pool.create(pmodel.SampleScriptPipelineGraphNode)
|
||||
track.sample_script_node = node
|
||||
self.assertIs(track.sample_script_node, node)
|
||||
|
||||
|
||||
class SampleRefTest(ModelTest):
|
||||
def test_time(self):
|
||||
|
@ -381,33 +278,13 @@ class ScoreTrackTest(MeasuredTrackMixin, ModelTest):
|
|||
cls = pmodel.ScoreTrack
|
||||
measure_cls = pmodel.ScoreMeasure
|
||||
|
||||
def test_instrument(self):
|
||||
track = self.pool.create(self.cls)
|
||||
|
||||
track.instrument = 'piano'
|
||||
self.assertEqual(track.instrument, 'piano')
|
||||
|
||||
def test_transport_octaves(self):
|
||||
def test_transpose_octaves(self):
|
||||
track = self.pool.create(self.cls)
|
||||
|
||||
self.assertEqual(track.transpose_octaves, 0)
|
||||
track.transpose_octaves = -2
|
||||
self.assertEqual(track.transpose_octaves, -2)
|
||||
|
||||
def test_instrument_node(self):
|
||||
track = self.pool.create(self.cls)
|
||||
|
||||
node = self.pool.create(pmodel.InstrumentPipelineGraphNode)
|
||||
track.instrument_node = node
|
||||
self.assertIs(track.instrument_node, node)
|
||||
|
||||
def test_event_source_node(self):
|
||||
track = self.pool.create(self.cls)
|
||||
|
||||
node = self.pool.create(pmodel.PianoRollPipelineGraphNode)
|
||||
track.event_source_node = node
|
||||
self.assertIs(track.event_source_node, node)
|
||||
|
||||
|
||||
class ScoreMeasureTest(ModelTest):
|
||||
def test_clef(self):
|
||||
|
@ -424,6 +301,13 @@ class ScoreMeasureTest(ModelTest):
|
|||
measure.key_signature = model.KeySignature('F minor')
|
||||
self.assertEqual(measure.key_signature, model.KeySignature('F minor'))
|
||||
|
||||
def test_time_signature(self):
|
||||
measure = self.pool.create(pmodel.ScoreMeasure)
|
||||
|
||||
self.assertEqual(measure.time_signature, model.TimeSignature(4, 4))
|
||||
measure.time_signature = model.TimeSignature(3, 4)
|
||||
self.assertEqual(measure.time_signature, model.TimeSignature(3, 4))
|
||||
|
||||
def test_notes(self):
|
||||
measure = self.pool.create(pmodel.ScoreMeasure)
|
||||
|
||||
|
@ -436,32 +320,12 @@ class BeatTrackTest(MeasuredTrackMixin, ModelTest):
|
|||
cls = pmodel.BeatTrack
|
||||
measure_cls = pmodel.BeatMeasure
|
||||
|
||||
def test_instrument(self):
|
||||
track = self.pool.create(self.cls)
|
||||
|
||||
track.instrument = 'piano'
|
||||
self.assertEqual(track.instrument, 'piano')
|
||||
|
||||
def test_pitch(self):
|
||||
track = self.pool.create(self.cls)
|
||||
|
||||
track.pitch = model.Pitch('F4')
|
||||
self.assertEqual(track.pitch, model.Pitch('F4'))
|
||||
|
||||
def test_instrument_node(self):
|
||||
track = self.pool.create(self.cls)
|
||||
|
||||
node = self.pool.create(pmodel.InstrumentPipelineGraphNode)
|
||||
track.instrument_node = node
|
||||
self.assertIs(track.instrument_node, node)
|
||||
|
||||
def test_event_source_node(self):
|
||||
track = self.pool.create(self.cls)
|
||||
|
||||
node = self.pool.create(pmodel.PianoRollPipelineGraphNode)
|
||||
track.event_source_node = node
|
||||
self.assertIs(track.event_source_node, node)
|
||||
|
||||
|
||||
class BeatMeasureTest(ModelTest):
|
||||
def test_beats(self):
|
||||
|
@ -497,26 +361,6 @@ class ControlTrackTest(TrackMixin, ModelTest):
|
|||
track.points.append(pnt)
|
||||
self.assertIs(track.points[0], pnt)
|
||||
|
||||
def test_generator_node(self):
|
||||
track = self.pool.create(self.cls)
|
||||
|
||||
node = self.pool.create(pmodel.CVGeneratorPipelineGraphNode)
|
||||
track.generator_node = node
|
||||
self.assertIs(track.generator_node, node)
|
||||
|
||||
|
||||
class PropertyTrackTest(TrackMixin, ModelTest):
|
||||
cls = pmodel.PropertyTrack
|
||||
|
||||
|
||||
class PropertyMeasureTest(ModelTest):
|
||||
def test_time_signature(self):
|
||||
measure = self.pool.create(pmodel.PropertyMeasure)
|
||||
|
||||
self.assertEqual(measure.time_signature, model.TimeSignature(4, 4))
|
||||
measure.time_signature = model.TimeSignature(3, 4)
|
||||
self.assertEqual(measure.time_signature, model.TimeSignature(3, 4))
|
||||
|
||||
|
||||
class MeasureReferenceTest(ModelTest):
|
||||
def test_measure(self):
|
||||
|
@ -527,38 +371,6 @@ class MeasureReferenceTest(ModelTest):
|
|||
self.assertIs(ref.measure, measure)
|
||||
|
||||
|
||||
class TrackGroupTest(TrackMixin, ModelTest):
|
||||
cls = pmodel.TrackGroup
|
||||
|
||||
def test_tracks(self):
|
||||
grp = self.pool.create(self.cls)
|
||||
|
||||
child = self.pool.create(pmodel.SampleTrack)
|
||||
grp.tracks.append(child)
|
||||
self.assertIs(grp.tracks[0], child)
|
||||
|
||||
|
||||
class MasterTrackGroupTest(ModelTest):
|
||||
def test_master_track_group(self):
|
||||
grp = self.pool.create(pmodel.MasterTrackGroup)
|
||||
self.assertEqual(len(grp.tracks), 0)
|
||||
|
||||
grp.tracks.append(self.pool.create(pmodel.ScoreTrack))
|
||||
grp.tracks.append(self.pool.create(pmodel.BeatTrack))
|
||||
self.assertEqual(len(grp.tracks), 2)
|
||||
self.assertIsInstance(grp.tracks[0], pmodel.ScoreTrack)
|
||||
self.assertIsInstance(grp.tracks[1], pmodel.BeatTrack)
|
||||
|
||||
del grp.tracks[0]
|
||||
self.assertEqual(len(grp.tracks), 1)
|
||||
self.assertIsInstance(grp.tracks[0], pmodel.BeatTrack)
|
||||
|
||||
grp.tracks.insert(0, self.pool.create(pmodel.ScoreTrack))
|
||||
self.assertEqual(len(grp.tracks), 2)
|
||||
self.assertIsInstance(grp.tracks[0], pmodel.ScoreTrack)
|
||||
self.assertIsInstance(grp.tracks[1], pmodel.BeatTrack)
|
||||
|
||||
|
||||
class NoteTest(ModelTest):
|
||||
def test_pitches(self):
|
||||
note = self.pool.create(pmodel.Note)
|
||||
|
|
|
@ -33,8 +33,6 @@ from noisicaa import model
|
|||
from noisicaa import node_db as node_db_lib
|
||||
from . import pmodel
|
||||
from . import pipeline_graph
|
||||
from . import property_track
|
||||
from . import track_group
|
||||
from . import commands
|
||||
from . import commands_pb2
|
||||
from . import score_track
|
||||
|
@ -58,122 +56,23 @@ class UpdateProjectProperties(commands.Command):
|
|||
commands.Command.register_command(UpdateProjectProperties)
|
||||
|
||||
|
||||
class AddTrack(commands.Command):
|
||||
proto_type = 'add_track'
|
||||
|
||||
def run(self, project: pmodel.Project, pool: pmodel.Pool, pb: protobuf.Message) -> int:
|
||||
pb = down_cast(commands_pb2.AddTrack, pb)
|
||||
|
||||
parent_group = pool[pb.parent_group_id]
|
||||
assert parent_group.is_child_of(project)
|
||||
assert isinstance(parent_group, track_group.TrackGroup)
|
||||
|
||||
if pb.insert_index == -1:
|
||||
insert_index = len(parent_group.tracks)
|
||||
else:
|
||||
insert_index = pb.insert_index
|
||||
assert 0 <= insert_index <= len(parent_group.tracks)
|
||||
|
||||
track_name = "Track %d" % (len(parent_group.tracks) + 1)
|
||||
track_cls_map = {
|
||||
'score': score_track.ScoreTrack,
|
||||
'beat': beat_track.BeatTrack,
|
||||
'control': control_track.ControlTrack,
|
||||
'sample': sample_track.SampleTrack,
|
||||
'group': track_group.TrackGroup,
|
||||
}
|
||||
track_cls = track_cls_map[pb.track_type]
|
||||
|
||||
kwargs = {}
|
||||
if issubclass(track_cls, pmodel.MeasuredTrack):
|
||||
num_measures = 1
|
||||
for track in parent_group.walk_tracks():
|
||||
if isinstance(track, pmodel.MeasuredTrack):
|
||||
num_measures = max(num_measures, len(track.measure_list))
|
||||
kwargs['num_measures'] = num_measures
|
||||
|
||||
track = pool.create(track_cls, name=track_name, **kwargs)
|
||||
|
||||
project.add_track(parent_group, insert_index, track)
|
||||
|
||||
return insert_index
|
||||
|
||||
commands.Command.register_command(AddTrack)
|
||||
|
||||
|
||||
class RemoveTrack(commands.Command):
|
||||
proto_type = 'remove_track'
|
||||
|
||||
def run(self, project: pmodel.Project, pool: pmodel.Pool, pb: protobuf.Message) -> None:
|
||||
pb = down_cast(commands_pb2.RemoveTrack, pb)
|
||||
|
||||
track = cast(pmodel.Track, pool[pb.track_id])
|
||||
assert track.is_child_of(project)
|
||||
parent_group = cast(pmodel.TrackGroup, track.parent)
|
||||
|
||||
project.remove_track(parent_group, track)
|
||||
|
||||
commands.Command.register_command(RemoveTrack)
|
||||
|
||||
|
||||
class InsertMeasure(commands.Command):
|
||||
proto_type = 'insert_measure'
|
||||
|
||||
def run(self, project: pmodel.Project, pool: pmodel.Pool, pb: protobuf.Message) -> None:
|
||||
pb = down_cast(commands_pb2.InsertMeasure, pb)
|
||||
|
||||
if not pb.tracks:
|
||||
cast(property_track.PropertyTrack, project.property_track).insert_measure(pb.pos)
|
||||
else:
|
||||
cast(property_track.PropertyTrack, project.property_track).append_measure()
|
||||
|
||||
for track in project.master_group.walk_tracks():
|
||||
if not isinstance(track, base_track.MeasuredTrack):
|
||||
continue
|
||||
|
||||
if not pb.tracks or track.id in pb.tracks:
|
||||
track.insert_measure(pb.pos)
|
||||
else:
|
||||
track.append_measure()
|
||||
|
||||
commands.Command.register_command(InsertMeasure)
|
||||
|
||||
|
||||
class RemoveMeasure(commands.Command):
|
||||
proto_type = 'remove_measure'
|
||||
|
||||
def run(self, project: pmodel.Project, pool: pmodel.Pool, pb: protobuf.Message) -> None:
|
||||
pb = down_cast(commands_pb2.RemoveMeasure, pb)
|
||||
|
||||
if not pb.tracks:
|
||||
cast(property_track.PropertyTrack, project.property_track).remove_measure(pb.pos)
|
||||
|
||||
for idx, track in enumerate(project.master_group.tracks):
|
||||
track = cast(base_track.MeasuredTrack, track)
|
||||
if not pb.tracks or idx in pb.tracks:
|
||||
track.remove_measure(pb.pos)
|
||||
if pb.tracks:
|
||||
track.append_measure()
|
||||
|
||||
commands.Command.register_command(RemoveMeasure)
|
||||
|
||||
|
||||
class SetNumMeasures(commands.Command):
|
||||
proto_type = 'set_num_measures'
|
||||
|
||||
def run(self, project: pmodel.Project, pool: pmodel.Pool, pb: protobuf.Message) -> None:
|
||||
pb = down_cast(commands_pb2.SetNumMeasures, pb)
|
||||
|
||||
for track in project.all_tracks:
|
||||
if not isinstance(track, pmodel.MeasuredTrack):
|
||||
continue
|
||||
track = cast(base_track.MeasuredTrack, track)
|
||||
raise NotImplementedError
|
||||
# for track in project.all_tracks:
|
||||
# if not isinstance(track, pmodel.MeasuredTrack):
|
||||
# continue
|
||||
# track = cast(base_track.MeasuredTrack, track)
|
||||
|
||||
while len(track.measure_list) < pb.num_measures:
|
||||
track.append_measure()
|
||||
# while len(track.measure_list) < pb.num_measures:
|
||||
# track.append_measure()
|
||||
|
||||
while len(track.measure_list) > pb.num_measures:
|
||||
track.remove_measure(len(track.measure_list) - 1)
|
||||
# while len(track.measure_list) > pb.num_measures:
|
||||
# track.remove_measure(len(track.measure_list) - 1)
|
||||
|
||||
commands.Command.register_command(SetNumMeasures)
|
||||
|
||||
|
@ -252,11 +151,26 @@ class AddPipelineGraphNode(commands.Command):
|
|||
|
||||
node_desc = project.get_node_description(pb.uri)
|
||||
|
||||
node = pool.create(
|
||||
pipeline_graph.PipelineGraphNode,
|
||||
name=node_desc.display_name,
|
||||
node_uri=pb.uri,
|
||||
graph_pos=model.Pos2F.from_proto(pb.graph_pos))
|
||||
kwargs = {
|
||||
'name': pb.name or node_desc.display_name,
|
||||
'graph_pos': model.Pos2F.from_proto(pb.graph_pos),
|
||||
'graph_size': model.SizeF.from_proto(pb.graph_size),
|
||||
'graph_color': model.Color.from_proto(pb.graph_color),
|
||||
}
|
||||
|
||||
track_cls_map = {
|
||||
'builtin://score_track': score_track.ScoreTrack,
|
||||
'builtin://beat_track': beat_track.BeatTrack,
|
||||
'builtin://control_track': control_track.ControlTrack,
|
||||
'builtin://sample_track': sample_track.SampleTrack,
|
||||
} # type: Dict[str, Type[pipeline_graph.BasePipelineGraphNode]]
|
||||
try:
|
||||
node_cls = track_cls_map[pb.uri]
|
||||
except KeyError:
|
||||
node_cls = pipeline_graph.PipelineGraphNode
|
||||
kwargs['node_uri'] = pb.uri
|
||||
|
||||
node = pool.create(node_cls, id=None, **kwargs)
|
||||
project.add_pipeline_graph_node(node)
|
||||
return node.id
|
||||
|
||||
|
@ -325,14 +239,11 @@ class BaseProject(pmodel.Project):
|
|||
super().create(**kwargs)
|
||||
self.node_db = node_db
|
||||
self.metadata = self._pool.create(Metadata)
|
||||
self.master_group = self._pool.create(track_group.MasterTrackGroup, name="Master")
|
||||
self.property_track = self._pool.create(property_track.PropertyTrack, name="Time")
|
||||
|
||||
audio_out_node = self._pool.create(
|
||||
pipeline_graph.AudioOutPipelineGraphNode,
|
||||
name="Audio Out", graph_pos=model.Pos2F(200, 0))
|
||||
self.add_pipeline_graph_node(audio_out_node)
|
||||
self.master_group.add_pipeline_nodes()
|
||||
|
||||
def close(self) -> None:
|
||||
pass
|
||||
|
@ -348,26 +259,9 @@ class BaseProject(pmodel.Project):
|
|||
logger.info("Executed command %s (%d operations)", cmd, cmd.num_log_ops)
|
||||
return result
|
||||
|
||||
def add_track(
|
||||
self, parent_group: pmodel.TrackGroup, insert_index: int, track: pmodel.Track) -> None:
|
||||
parent_group.tracks.insert(insert_index, track)
|
||||
track.add_pipeline_nodes()
|
||||
|
||||
def remove_track(self, parent_group: pmodel.TrackGroup, track: pmodel.Track) -> None:
|
||||
track.remove_pipeline_nodes()
|
||||
del parent_group.tracks[track.index]
|
||||
|
||||
def handle_pipeline_mutation(self, mutation: audioproc.Mutation) -> None:
|
||||
self.pipeline_mutation.call(mutation)
|
||||
|
||||
@property
|
||||
def audio_out_node(self) -> pmodel.AudioOutPipelineGraphNode:
|
||||
for node in self.pipeline_graph_nodes:
|
||||
if isinstance(node, pipeline_graph.AudioOutPipelineGraphNode):
|
||||
return node
|
||||
|
||||
raise ValueError("No audio out node found.")
|
||||
|
||||
def add_pipeline_graph_node(self, node: pmodel.BasePipelineGraphNode) -> None:
|
||||
self.pipeline_graph_nodes.append(node)
|
||||
for mutation in node.get_add_mutations():
|
||||
|
@ -590,21 +484,13 @@ class Pool(pmodel.Pool):
|
|||
self.register_class(control_track.ControlPoint)
|
||||
self.register_class(control_track.ControlTrack)
|
||||
self.register_class(pipeline_graph.AudioOutPipelineGraphNode)
|
||||
self.register_class(pipeline_graph.CVGeneratorPipelineGraphNode)
|
||||
self.register_class(pipeline_graph.InstrumentPipelineGraphNode)
|
||||
self.register_class(pipeline_graph.PianoRollPipelineGraphNode)
|
||||
self.register_class(pipeline_graph.PipelineGraphConnection)
|
||||
self.register_class(pipeline_graph.PipelineGraphControlValue)
|
||||
self.register_class(pipeline_graph.PipelineGraphNode)
|
||||
self.register_class(pipeline_graph.SampleScriptPipelineGraphNode)
|
||||
self.register_class(pipeline_graph.TrackMixerPipelineGraphNode)
|
||||
self.register_class(property_track.PropertyMeasure)
|
||||
self.register_class(property_track.PropertyTrack)
|
||||
self.register_class(sample_track.Sample)
|
||||
self.register_class(sample_track.SampleRef)
|
||||
self.register_class(sample_track.SampleTrack)
|
||||
self.register_class(score_track.Note)
|
||||
self.register_class(score_track.ScoreMeasure)
|
||||
self.register_class(score_track.ScoreTrack)
|
||||
self.register_class(track_group.MasterTrackGroup)
|
||||
self.register_class(track_group.TrackGroup)
|
||||
|
|
|
@ -53,38 +53,68 @@ class ProjectChild(model.ProjectChild, ObjectBase):
|
|||
return down_cast(Project, super().project)
|
||||
|
||||
|
||||
class Track(ProjectChild, model.Track, ObjectBase):
|
||||
class PipelineGraphControlValue(ProjectChild, model.PipelineGraphControlValue, ObjectBase):
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return self.get_property_value('name')
|
||||
|
||||
@property
|
||||
def value(self) -> model.ControlValue:
|
||||
return self.get_property_value('value')
|
||||
|
||||
|
||||
class BasePipelineGraphNode(ProjectChild, model.BasePipelineGraphNode, ObjectBase): # pylint: disable=abstract-method
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return self.get_property_value('name')
|
||||
|
||||
@property
|
||||
def graph_pos(self) -> model.Pos2F:
|
||||
return self.get_property_value('graph_pos')
|
||||
|
||||
@property
|
||||
def graph_size(self) -> model.SizeF:
|
||||
return self.get_property_value('graph_size')
|
||||
|
||||
@property
|
||||
def graph_color(self) -> model.Color:
|
||||
return self.get_property_value('graph_color')
|
||||
|
||||
@property
|
||||
def control_values(self) -> Sequence[PipelineGraphControlValue]:
|
||||
return self.get_property_value('control_values')
|
||||
|
||||
@property
|
||||
def plugin_state(self) -> audioproc.PluginState:
|
||||
return self.get_property_value('plugin_state')
|
||||
|
||||
|
||||
|
||||
class PipelineGraphNode(BasePipelineGraphNode, model.PipelineGraphNode, ObjectBase):
|
||||
@property
|
||||
def node_uri(self) -> str:
|
||||
return self.get_property_value('node_uri')
|
||||
|
||||
|
||||
class AudioOutPipelineGraphNode(
|
||||
BasePipelineGraphNode, model.AudioOutPipelineGraphNode, ObjectBase):
|
||||
pass
|
||||
|
||||
|
||||
class Track(BasePipelineGraphNode, model.Track, ObjectBase): # pylint: disable=abstract-method
|
||||
@property
|
||||
def visible(self) -> bool:
|
||||
return self.get_property_value('visible')
|
||||
|
||||
@property
|
||||
def muted(self) -> bool:
|
||||
return self.get_property_value('muted')
|
||||
|
||||
@property
|
||||
def gain(self) -> float:
|
||||
return self.get_property_value('gain')
|
||||
|
||||
@property
|
||||
def pan(self) -> float:
|
||||
return self.get_property_value('pan')
|
||||
|
||||
@property
|
||||
def mixer_node(self) -> 'BasePipelineGraphNode':
|
||||
return self.get_property_value('mixer_node')
|
||||
|
||||
def walk_tracks(self, groups: bool = False, tracks: bool = True) -> Iterator['Track']:
|
||||
for track in super().walk_tracks(groups, tracks):
|
||||
yield down_cast(Track, track)
|
||||
def list_position(self) -> int:
|
||||
return self.get_property_value('list_position')
|
||||
|
||||
|
||||
class Measure(ProjectChild, model.Measure, ObjectBase):
|
||||
pass
|
||||
@property
|
||||
def time_signature(self) -> model.TimeSignature:
|
||||
return self.get_property_value('time_signature')
|
||||
|
||||
|
||||
class MeasureReference(ProjectChild, model.MeasureReference, ObjectBase):
|
||||
|
@ -93,7 +123,7 @@ class MeasureReference(ProjectChild, model.MeasureReference, ObjectBase):
|
|||
return self.get_property_value('measure')
|
||||
|
||||
|
||||
class MeasuredTrack(Track, model.MeasuredTrack, ObjectBase):
|
||||
class MeasuredTrack(Track, model.MeasuredTrack, ObjectBase): # pylint: disable=abstract-method
|
||||
@property
|
||||
def measure_list(self) -> Sequence[MeasureReference]:
|
||||
return self.get_property_value('measure_list')
|
||||
|
@ -103,6 +133,22 @@ class MeasuredTrack(Track, model.MeasuredTrack, ObjectBase):
|
|||
return self.get_property_value('measure_heap')
|
||||
|
||||
|
||||
class ControlPoint(ProjectChild, model.ControlPoint, ObjectBase):
|
||||
@property
|
||||
def time(self) -> audioproc.MusicalTime:
|
||||
return self.get_property_value('time')
|
||||
|
||||
@property
|
||||
def value(self) -> float:
|
||||
return self.get_property_value('value')
|
||||
|
||||
|
||||
class ControlTrack(Track, model.ControlTrack):
|
||||
@property
|
||||
def points(self) -> Sequence[ControlPoint]:
|
||||
return self.get_property_value('points')
|
||||
|
||||
|
||||
class Note(ProjectChild, model.Note, ObjectBase):
|
||||
@property
|
||||
def pitches(self) -> Sequence[model.Pitch]:
|
||||
|
@ -125,16 +171,6 @@ class Note(ProjectChild, model.Note, ObjectBase):
|
|||
return down_cast(ScoreMeasure, super().measure)
|
||||
|
||||
|
||||
class TrackGroup(Track, model.TrackGroup, ObjectBase):
|
||||
@property
|
||||
def tracks(self) -> Sequence[Track]:
|
||||
return self.get_property_value('tracks')
|
||||
|
||||
|
||||
class MasterTrackGroup(TrackGroup, model.MasterTrackGroup, ObjectBase):
|
||||
pass
|
||||
|
||||
|
||||
class ScoreMeasure(Measure, model.ScoreMeasure, ObjectBase):
|
||||
@property
|
||||
def clef(self) -> model.Clef:
|
||||
|
@ -154,22 +190,10 @@ class ScoreMeasure(Measure, model.ScoreMeasure, ObjectBase):
|
|||
|
||||
|
||||
class ScoreTrack(MeasuredTrack, model.ScoreTrack, ObjectBase):
|
||||
@property
|
||||
def instrument(self) -> str:
|
||||
return self.get_property_value('instrument')
|
||||
|
||||
@property
|
||||
def transpose_octaves(self) -> int:
|
||||
return self.get_property_value('transpose_octaves')
|
||||
|
||||
@property
|
||||
def instrument_node(self) -> 'InstrumentPipelineGraphNode':
|
||||
return self.get_property_value('instrument_node')
|
||||
|
||||
@property
|
||||
def event_source_node(self) -> 'PianoRollPipelineGraphNode':
|
||||
return self.get_property_value('event_source_node')
|
||||
|
||||
|
||||
class Beat(ProjectChild, model.Beat, ObjectBase):
|
||||
@property
|
||||
|
@ -192,52 +216,10 @@ class BeatMeasure(Measure, model.BeatMeasure, ObjectBase):
|
|||
|
||||
|
||||
class BeatTrack(MeasuredTrack, model.BeatTrack, ObjectBase):
|
||||
@property
|
||||
def instrument(self) -> str:
|
||||
return self.get_property_value('instrument')
|
||||
|
||||
@property
|
||||
def pitch(self) -> model.Pitch:
|
||||
return self.get_property_value('pitch')
|
||||
|
||||
@property
|
||||
def instrument_node(self) -> 'InstrumentPipelineGraphNode':
|
||||
return self.get_property_value('instrument_node')
|
||||
|
||||
@property
|
||||
def event_source_node(self) -> 'PianoRollPipelineGraphNode':
|
||||
return self.get_property_value('event_source_node')
|
||||
|
||||
|
||||
class PropertyMeasure(Measure, model.PropertyMeasure, ObjectBase):
|
||||
@property
|
||||
def time_signature(self) -> model.TimeSignature:
|
||||
return self.get_property_value('time_signature')
|
||||
|
||||
|
||||
class PropertyTrack(MeasuredTrack, model.PropertyTrack):
|
||||
pass
|
||||
|
||||
|
||||
class ControlPoint(ProjectChild, model.ControlPoint, ObjectBase):
|
||||
@property
|
||||
def time(self) -> audioproc.MusicalTime:
|
||||
return self.get_property_value('time')
|
||||
|
||||
@property
|
||||
def value(self) -> float:
|
||||
return self.get_property_value('value')
|
||||
|
||||
|
||||
class ControlTrack(Track, model.ControlTrack):
|
||||
@property
|
||||
def points(self) -> Sequence[ControlPoint]:
|
||||
return self.get_property_value('points')
|
||||
|
||||
@property
|
||||
def generator_node(self) -> 'CVGeneratorPipelineGraphNode':
|
||||
return self.get_property_value('generator_node')
|
||||
|
||||
|
||||
class SampleRef(ProjectChild, model.SampleRef, ObjectBase):
|
||||
@property
|
||||
|
@ -254,85 +236,12 @@ class SampleTrack(Track, model.SampleTrack, ObjectBase):
|
|||
def samples(self) -> Sequence[SampleRef]:
|
||||
return self.get_property_value('samples')
|
||||
|
||||
@property
|
||||
def sample_script_node(self) -> 'SampleScriptPipelineGraphNode':
|
||||
return self.get_property_value('sample_script_node')
|
||||
|
||||
|
||||
|
||||
class PipelineGraphControlValue(ProjectChild, model.PipelineGraphControlValue, ObjectBase):
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return self.get_property_value('name')
|
||||
|
||||
@property
|
||||
def value(self) -> model.ControlValue:
|
||||
return self.get_property_value('value')
|
||||
|
||||
|
||||
class BasePipelineGraphNode(ProjectChild, model.BasePipelineGraphNode, ObjectBase): # pylint: disable=abstract-method
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return self.get_property_value('name')
|
||||
|
||||
@property
|
||||
def graph_pos(self) -> model.Pos2F:
|
||||
return self.get_property_value('graph_pos')
|
||||
|
||||
@property
|
||||
def control_values(self) -> Sequence[PipelineGraphControlValue]:
|
||||
return self.get_property_value('control_values')
|
||||
|
||||
@property
|
||||
def plugin_state(self) -> audioproc.PluginState:
|
||||
return self.get_property_value('plugin_state')
|
||||
|
||||
|
||||
|
||||
class PipelineGraphNode(BasePipelineGraphNode, model.PipelineGraphNode, ObjectBase):
|
||||
@property
|
||||
def node_uri(self) -> str:
|
||||
return self.get_property_value('node_uri')
|
||||
|
||||
|
||||
class AudioOutPipelineGraphNode(
|
||||
BasePipelineGraphNode, model.AudioOutPipelineGraphNode, ObjectBase):
|
||||
pass
|
||||
|
||||
|
||||
class TrackMixerPipelineGraphNode(
|
||||
BasePipelineGraphNode, model.TrackMixerPipelineGraphNode, ObjectBase):
|
||||
@property
|
||||
def track(self) -> Track:
|
||||
return self.get_property_value('track')
|
||||
|
||||
|
||||
class PianoRollPipelineGraphNode(
|
||||
BasePipelineGraphNode, model.PianoRollPipelineGraphNode, ObjectBase):
|
||||
@property
|
||||
def track(self) -> Track:
|
||||
return self.get_property_value('track')
|
||||
|
||||
|
||||
class CVGeneratorPipelineGraphNode(
|
||||
BasePipelineGraphNode, model.CVGeneratorPipelineGraphNode, ObjectBase):
|
||||
@property
|
||||
def track(self) -> Track:
|
||||
return self.get_property_value('track')
|
||||
|
||||
|
||||
class SampleScriptPipelineGraphNode(
|
||||
BasePipelineGraphNode, model.SampleScriptPipelineGraphNode, ObjectBase):
|
||||
@property
|
||||
def track(self) -> Track:
|
||||
return self.get_property_value('track')
|
||||
|
||||
|
||||
class InstrumentPipelineGraphNode(
|
||||
BasePipelineGraphNode, model.InstrumentPipelineGraphNode, ObjectBase):
|
||||
@property
|
||||
def track(self) -> Track:
|
||||
return self.get_property_value('track')
|
||||
def instrument_uri(self) -> str:
|
||||
return self.get_property_value('instrument_uri')
|
||||
|
||||
|
||||
class PipelineGraphConnection(ProjectChild, model.PipelineGraphConnection, ObjectBase):
|
||||
|
@ -384,18 +293,10 @@ class Project(model.Project, ObjectBase):
|
|||
self.__node_db = None # type: node_db_lib.NodeDBClient
|
||||
self.__time_mapper = None # type: audioproc.TimeMapper
|
||||
|
||||
@property
|
||||
def master_group(self) -> MasterTrackGroup:
|
||||
return self.get_property_value('master_group')
|
||||
|
||||
@property
|
||||
def metadata(self) -> Metadata:
|
||||
return self.get_property_value('metadata')
|
||||
|
||||
@property
|
||||
def property_track(self) -> PropertyTrack:
|
||||
return self.get_property_value('property_track')
|
||||
|
||||
@property
|
||||
def pipeline_graph_nodes(self) -> Sequence[BasePipelineGraphNode]:
|
||||
return self.get_property_value('pipeline_graph_nodes')
|
||||
|
@ -412,10 +313,6 @@ class Project(model.Project, ObjectBase):
|
|||
def bpm(self) -> int:
|
||||
return self.get_property_value('bpm')
|
||||
|
||||
@property
|
||||
def all_tracks(self) -> Sequence[Track]:
|
||||
return cast(Sequence[Track], super().all_tracks)
|
||||
|
||||
@property
|
||||
def project(self) -> 'Project':
|
||||
return down_cast(Project, super().project)
|
||||
|
@ -440,8 +337,6 @@ class Pool(model.Pool[ObjectBase]):
|
|||
super().__init__()
|
||||
|
||||
self.register_class(Project)
|
||||
self.register_class(TrackGroup)
|
||||
self.register_class(MasterTrackGroup)
|
||||
self.register_class(MeasureReference)
|
||||
self.register_class(ScoreMeasure)
|
||||
self.register_class(ScoreTrack)
|
||||
|
@ -452,18 +347,12 @@ class Pool(model.Pool[ObjectBase]):
|
|||
self.register_class(SampleTrack)
|
||||
self.register_class(ControlPoint)
|
||||
self.register_class(ControlTrack)
|
||||
self.register_class(PropertyMeasure)
|
||||
self.register_class(PropertyTrack)
|
||||
self.register_class(Metadata)
|
||||
self.register_class(Sample)
|
||||
self.register_class(Note)
|
||||
self.register_class(PipelineGraphConnection)
|
||||
self.register_class(PipelineGraphNode)
|
||||
self.register_class(InstrumentPipelineGraphNode)
|
||||
self.register_class(TrackMixerPipelineGraphNode)
|
||||
self.register_class(SampleScriptPipelineGraphNode)
|
||||
self.register_class(CVGeneratorPipelineGraphNode)
|
||||
self.register_class(PianoRollPipelineGraphNode)
|
||||
self.register_class(AudioOutPipelineGraphNode)
|
||||
self.register_class(PipelineGraphControlValue)
|
||||
|
||||
|
@ -536,6 +425,9 @@ class ProjectClient(object):
|
|||
await self._stub.close()
|
||||
self._stub = None
|
||||
|
||||
def get_object(self, obj_id: int) -> ObjectBase:
|
||||
return self.__pool[obj_id]
|
||||
|
||||
def handle_project_mutations(self, mutations: mutations_pb2.MutationList) -> None:
|
||||
mutation_list = mutations_lib.MutationList(self.__pool, mutations)
|
||||
mutation_list.apply_forward()
|
||||
|
|
|
@ -90,13 +90,12 @@ class ProjectClientTest(ProjectClientTestBase):
|
|||
async def test_call_command(self):
|
||||
await self.client.create_inmemory()
|
||||
project = self.client.project
|
||||
num_tracks = len(project.master_group.tracks)
|
||||
num_nodes = len(project.pipeline_graph_nodes)
|
||||
await self.client.send_command(commands_pb2.Command(
|
||||
target=project.id,
|
||||
add_track=commands_pb2.AddTrack(
|
||||
track_type='score',
|
||||
parent_group_id=project.master_group.id)))
|
||||
self.assertEqual(len(project.master_group.tracks), num_tracks + 1)
|
||||
add_pipeline_graph_node=commands_pb2.AddPipelineGraphNode(
|
||||
uri='builtin://score_track')))
|
||||
self.assertEqual(len(project.pipeline_graph_nodes), num_nodes + 1)
|
||||
|
||||
|
||||
class RenderTest(ProjectClientTestBase):
|
||||
|
|
|
@ -125,7 +125,6 @@ class ProjectIntegrationTest(unittest_mixins.ProcessManagerMixin, unittest.Async
|
|||
path = os.path.join(TEST_OPTS.TMP_DIR, 'test-project-%s' % uuid.uuid4().hex)
|
||||
await client.create(path)
|
||||
project = client.project
|
||||
self.assertEqual(len(project.master_group.tracks), 0)
|
||||
return project, project._pool, path
|
||||
|
||||
async def open_project(
|
||||
|
@ -160,9 +159,8 @@ class ProjectIntegrationTest(unittest_mixins.ProcessManagerMixin, unittest.Async
|
|||
await self.send_command(
|
||||
client, pool,
|
||||
target=project.id,
|
||||
add_track=commands_pb2.AddTrack(
|
||||
track_type='score',
|
||||
parent_group_id=project.master_group.id))
|
||||
add_pipeline_graph_node=commands_pb2.AddPipelineGraphNode(
|
||||
uri='builtin://score_track'))
|
||||
#track1 = project.master_group.tracks[insert_index]
|
||||
|
||||
# Disconnect from and shutdown process, without calling close().
|
||||
|
|
|
@ -38,7 +38,6 @@ from . import mutations_pb2
|
|||
from . import commands
|
||||
from . import commands_pb2
|
||||
from . import player as player_lib
|
||||
from . import score_track
|
||||
from . import render
|
||||
from . import pmodel # pylint: disable=unused-import
|
||||
from . import render_settings_pb2
|
||||
|
@ -84,7 +83,8 @@ class Session(core.CallbackSessionMixin, core.SessionBase):
|
|||
|
||||
def handle_pipeline_status(self, status: Dict[str, Any]) -> None:
|
||||
if 'node_state' in status:
|
||||
node_id, state = status['node_state']
|
||||
logger.error(status['node_state'])
|
||||
_, node_id, state = status['node_state']
|
||||
if 'broken' in state:
|
||||
self.set_value('pipeline_graph_node/%s/broken' % node_id, state['broken'])
|
||||
|
||||
|
@ -242,10 +242,6 @@ class ProjectProcess(core.SessionHandlerMixin, core.ProcessBase):
|
|||
|
||||
def _create_blank_project(self, project_cls: Type[PROJECT]) -> PROJECT:
|
||||
project = self.__pool.create(project_cls, node_db=self.node_db)
|
||||
project.add_track(
|
||||
project.master_group, 0,
|
||||
self.__pool.create(score_track.ScoreTrack, name="Track 1"))
|
||||
logger.info("...")
|
||||
return project
|
||||
|
||||
async def __close_project(self) -> None:
|
||||
|
|
|
@ -32,7 +32,7 @@ from noisicaa.core import fileutil
|
|||
from noisicaa.core import storage
|
||||
from noisicaa import model
|
||||
from . import project
|
||||
from . import track_group
|
||||
from . import score_track
|
||||
from . import commands_pb2
|
||||
from . import commands_test
|
||||
|
||||
|
@ -52,12 +52,13 @@ class BaseProjectTest(PoolMixin, unittest_mixins.NodeDBMixin, unittest.AsyncTest
|
|||
|
||||
def test_deserialize(self):
|
||||
p = self.pool.create(project.BaseProject, node_db=self.node_db)
|
||||
p.master_group.tracks.append(self.pool.create(track_group.TrackGroup, name='Sub Group'))
|
||||
p.pipeline_graph_nodes.append(self.pool.create(score_track.ScoreTrack, name='Track 1'))
|
||||
num_nodes = len(p.pipeline_graph_nodes)
|
||||
serialized = p.serialize()
|
||||
|
||||
pool2 = project.Pool()
|
||||
p2 = cast(project.BaseProject, pool2.deserialize_tree(serialized))
|
||||
self.assertEqual(len(p2.master_group.tracks), 1)
|
||||
self.assertEqual(len(p2.pipeline_graph_nodes), num_nodes)
|
||||
|
||||
|
||||
class ProjectTest(PoolMixin, unittest_mixins.NodeDBMixin, unittest.AsyncTestCase):
|
||||
|
@ -95,13 +96,12 @@ class ProjectTest(PoolMixin, unittest_mixins.NodeDBMixin, unittest.AsyncTestCase
|
|||
pool=self.pool,
|
||||
node_db=self.node_db)
|
||||
try:
|
||||
self.assertEqual(len(p.master_group.tracks), 0)
|
||||
p.dispatch_command_proto(commands_pb2.Command(
|
||||
target=p.id,
|
||||
add_track=commands_pb2.AddTrack(
|
||||
track_type='score',
|
||||
parent_group_id=p.master_group.id)))
|
||||
track_id = p.master_group.tracks[-1].id
|
||||
add_pipeline_graph_node=commands_pb2.AddPipelineGraphNode(
|
||||
uri='builtin://score_track')))
|
||||
num_nodes = len(p.pipeline_graph_nodes)
|
||||
track_id = p.pipeline_graph_nodes[-1].id
|
||||
finally:
|
||||
p.close()
|
||||
|
||||
|
@ -111,8 +111,8 @@ class ProjectTest(PoolMixin, unittest_mixins.NodeDBMixin, unittest.AsyncTestCase
|
|||
pool=pool,
|
||||
node_db=self.node_db)
|
||||
try:
|
||||
self.assertEqual(len(p.master_group.tracks), 1)
|
||||
self.assertEqual(p.master_group.tracks[-1].id, track_id)
|
||||
self.assertEqual(len(p.pipeline_graph_nodes), num_nodes)
|
||||
self.assertEqual(p.pipeline_graph_nodes[-1].id, track_id)
|
||||
finally:
|
||||
p.close()
|
||||
|
||||
|
@ -130,7 +130,7 @@ class ProjectTest(PoolMixin, unittest_mixins.NodeDBMixin, unittest.AsyncTestCase
|
|||
self.fake_os.path.isfile('/foo.data/checkpoint.000001'))
|
||||
|
||||
|
||||
class ProjectPropertiesTest(commands_test.CommandsTestBase):
|
||||
class ProjectPropertiesTest(commands_test.CommandsTestMixin, unittest.AsyncTestCase):
|
||||
async def test_bpm(self):
|
||||
await self.client.send_command(commands_pb2.Command(
|
||||
target=self.project.id,
|
||||
|
|
|
@ -1,78 +0,0 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
# @begin:license
|
||||
#
|
||||
# Copyright (c) 2015-2018, 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 Any, Optional
|
||||
|
||||
from google.protobuf import message as protobuf
|
||||
|
||||
from noisicaa.core.typing_extra import down_cast
|
||||
from noisicaa import model
|
||||
from . import pmodel
|
||||
from . import base_track
|
||||
from . import commands
|
||||
from . import commands_pb2
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SetTimeSignature(commands.Command):
|
||||
proto_type = 'set_time_signature'
|
||||
|
||||
def run(self, project: pmodel.Project, pool: pmodel.Pool, pb: protobuf.Message) -> None:
|
||||
pb = down_cast(commands_pb2.SetTimeSignature, pb)
|
||||
track = down_cast(pmodel.PropertyTrack, pool[self.proto.command.target])
|
||||
|
||||
for measure_id in pb.measure_ids:
|
||||
measure = down_cast(pmodel.PropertyMeasure, pool[measure_id])
|
||||
assert measure.is_child_of(track)
|
||||
measure.time_signature = model.TimeSignature(pb.upper, pb.lower)
|
||||
|
||||
commands.Command.register_command(SetTimeSignature)
|
||||
|
||||
|
||||
class PropertyMeasure(pmodel.PropertyMeasure, base_track.Measure):
|
||||
@property
|
||||
def empty(self) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
class PropertyTrack(pmodel.PropertyTrack, base_track.MeasuredTrack):
|
||||
measure_cls = PropertyMeasure
|
||||
|
||||
def create(self, num_measures: int = 1, **kwargs: Any) -> None:
|
||||
super().create(**kwargs)
|
||||
|
||||
for _ in range(num_measures):
|
||||
self.append_measure()
|
||||
|
||||
def create_empty_measure(self, ref: Optional[pmodel.Measure]) -> PropertyMeasure:
|
||||
measure = down_cast(PropertyMeasure, super().create_empty_measure(ref))
|
||||
|
||||
if ref is not None:
|
||||
ref = down_cast(PropertyMeasure, ref)
|
||||
measure.time_signature = ref.time_signature
|
||||
|
||||
return measure
|
||||
|
||||
def create_track_connector(self, **kwargs: Any) -> base_track.TrackConnector:
|
||||
raise RuntimeError("No track connector for PropertyTrack")
|
|
@ -23,7 +23,7 @@
|
|||
import fractions
|
||||
import logging
|
||||
import random
|
||||
from typing import cast, Any, List, Optional, Dict # pylint: disable=unused-import
|
||||
from typing import cast, Any, List, Optional, Dict, Iterator # pylint: disable=unused-import
|
||||
|
||||
from google.protobuf import message as protobuf
|
||||
|
||||
|
@ -34,7 +34,6 @@ from noisicaa import core # pylint: disable=unused-import
|
|||
from noisicaa.bindings import sndfile
|
||||
from . import pmodel
|
||||
from . import base_track
|
||||
from . import pipeline_graph
|
||||
from . import commands
|
||||
from . import commands_pb2
|
||||
from . import rms
|
||||
|
@ -171,10 +170,10 @@ class SampleRef(pmodel.SampleRef):
|
|||
class SampleTrackConnector(base_track.TrackConnector):
|
||||
_track = None # type: SampleTrack
|
||||
|
||||
def __init__(self, *, node_id: str, **kwargs: Any) -> None:
|
||||
def __init__(self, **kwargs: Any) -> None:
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.__node_id = node_id
|
||||
self.__node_id = self._track.pipeline_node_id
|
||||
self.__listeners = {} # type: Dict[str, core.Listener]
|
||||
self.__sample_ids = {} # type: Dict[int, int]
|
||||
|
||||
|
@ -250,38 +249,15 @@ class SampleTrackConnector(base_track.TrackConnector):
|
|||
|
||||
class SampleTrack(pmodel.SampleTrack, base_track.Track):
|
||||
def create_track_connector(self, **kwargs: Any) -> SampleTrackConnector:
|
||||
return SampleTrackConnector(
|
||||
track=self,
|
||||
node_id=self.sample_script_name,
|
||||
**kwargs)
|
||||
return SampleTrackConnector(track=self, **kwargs)
|
||||
|
||||
@property
|
||||
def sample_script_name(self) -> str:
|
||||
return '%016x-samplescript' % self.id
|
||||
def get_add_mutations(self) -> Iterator[audioproc.Mutation]:
|
||||
yield audioproc.AddNode(
|
||||
description=self.description,
|
||||
id=self.pipeline_node_id,
|
||||
name=self.name)
|
||||
|
||||
def add_pipeline_nodes(self) -> None:
|
||||
super().add_pipeline_nodes()
|
||||
yield from self.get_initial_parameter_mutations()
|
||||
|
||||
mixer_node = self.mixer_node
|
||||
|
||||
sample_script_node = self._pool.create(
|
||||
pipeline_graph.SampleScriptPipelineGraphNode,
|
||||
name="Sample Script",
|
||||
graph_pos=mixer_node.graph_pos - model.Pos2F(200, 0),
|
||||
track=self)
|
||||
self.project.add_pipeline_graph_node(sample_script_node)
|
||||
self.sample_script_node = sample_script_node
|
||||
|
||||
self.project.add_pipeline_graph_connection(self._pool.create(
|
||||
pipeline_graph.PipelineGraphConnection,
|
||||
source_node=sample_script_node, source_port='out:left',
|
||||
dest_node=mixer_node, dest_port='in:left'))
|
||||
self.project.add_pipeline_graph_connection(self._pool.create(
|
||||
pipeline_graph.PipelineGraphConnection,
|
||||
source_node=sample_script_node, source_port='out:right',
|
||||
dest_node=mixer_node, dest_port='in:right'))
|
||||
|
||||
def remove_pipeline_nodes(self) -> None:
|
||||
self.project.remove_pipeline_graph_node(self.sample_script_node)
|
||||
self.sample_script_node = None
|
||||
super().remove_pipeline_nodes()
|
||||
def get_remove_mutations(self) -> Iterator[audioproc.Mutation]:
|
||||
yield audioproc.RemoveNode(self.pipeline_node_id)
|
||||
|
|
|
@ -30,17 +30,18 @@ from noisicaa import audioproc
|
|||
from noisicaa.model import project_pb2
|
||||
from . import project
|
||||
from . import sample_track
|
||||
from . import commands_test
|
||||
from . import commands_pb2
|
||||
from . import project_client
|
||||
from . import base_track_test
|
||||
|
||||
class ControlTrackConnectorTest(unittest_mixins.NodeDBMixin, unittest.AsyncTestCase):
|
||||
|
||||
class SampleTrackConnectorTest(unittest_mixins.NodeDBMixin, unittest.AsyncTestCase):
|
||||
async def setup_testcase(self):
|
||||
self.pool = project.Pool()
|
||||
|
||||
self.project = demo_project.basic(self.pool, project.BaseProject, node_db=self.node_db)
|
||||
self.project = demo_project.empty(self.pool, project.BaseProject, node_db=self.node_db)
|
||||
self.track = self.pool.create(sample_track.SampleTrack, name='test')
|
||||
self.project.master_group.tracks.append(self.track)
|
||||
self.project.pipeline_graph_nodes.append(self.track)
|
||||
|
||||
self.sample1 = self.pool.create(
|
||||
sample_track.Sample,
|
||||
|
@ -54,7 +55,7 @@ class ControlTrackConnectorTest(unittest_mixins.NodeDBMixin, unittest.AsyncTestC
|
|||
self.messages = [] # type: List[str]
|
||||
|
||||
def message_cb(self, msg):
|
||||
self.assertEqual(msg.node_id, self.track.sample_script_name)
|
||||
self.assertEqual(int(msg.node_id, 16), self.track.id)
|
||||
# TODO: track the messages themselves and inspect their contents as well.
|
||||
self.messages.append(msg.WhichOneof('msg'))
|
||||
|
||||
|
@ -131,31 +132,9 @@ class ControlTrackConnectorTest(unittest_mixins.NodeDBMixin, unittest.AsyncTestC
|
|||
connector.close()
|
||||
|
||||
|
||||
class SampleTrackTest(commands_test.CommandsTestBase):
|
||||
async def test_add_remove(self):
|
||||
insert_index = await self.client.send_command(commands_pb2.Command(
|
||||
target=self.project.id,
|
||||
add_track=commands_pb2.AddTrack(
|
||||
track_type='sample',
|
||||
parent_group_id=self.project.master_group.id)))
|
||||
self.assertEqual(insert_index, 0)
|
||||
|
||||
track = self.project.master_group.tracks[insert_index]
|
||||
self.assertIsInstance(track, project_client.SampleTrack)
|
||||
|
||||
await self.client.send_command(commands_pb2.Command(
|
||||
target=self.project.id,
|
||||
remove_track=commands_pb2.RemoveTrack(
|
||||
track_id=track.id)))
|
||||
self.assertEqual(len(self.project.master_group.tracks), 0)
|
||||
|
||||
async def _add_track(self):
|
||||
insert_index = await self.client.send_command(commands_pb2.Command(
|
||||
target=self.project.id,
|
||||
add_track=commands_pb2.AddTrack(
|
||||
track_type='sample',
|
||||
parent_group_id=self.project.master_group.id)))
|
||||
return self.project.master_group.tracks[insert_index]
|
||||
class SampleTrackTest(base_track_test.TrackTestMixin, unittest.AsyncTestCase):
|
||||
node_uri = 'builtin://sample_track'
|
||||
track_cls = project_client.SampleTrack
|
||||
|
||||
async def test_add_sample(self):
|
||||
track = await self._add_track()
|
||||
|
|
|
@ -30,28 +30,12 @@ from noisicaa import audioproc
|
|||
from noisicaa import model
|
||||
from . import base_track
|
||||
from . import pmodel
|
||||
from . import pipeline_graph
|
||||
from . import commands
|
||||
from . import commands_pb2
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SetInstrument(commands.Command):
|
||||
proto_type = 'set_instrument'
|
||||
|
||||
def run(self, project: pmodel.Project, pool: pmodel.Pool, pb: protobuf.Message) -> None:
|
||||
pb = down_cast(commands_pb2.SetInstrument, pb)
|
||||
track = down_cast(pmodel.ScoreTrack, pool[self.proto.command.target])
|
||||
|
||||
track.instrument = pb.instrument
|
||||
|
||||
for mutation in track.instrument_node.get_update_mutations():
|
||||
project.handle_pipeline_mutation(mutation)
|
||||
|
||||
commands.Command.register_command(SetInstrument)
|
||||
|
||||
|
||||
class ChangeNote(commands.Command):
|
||||
proto_type = 'change_note'
|
||||
|
||||
|
@ -291,16 +275,9 @@ class ScoreTrackConnector(base_track.MeasuredTrackConnector):
|
|||
class ScoreTrack(pmodel.ScoreTrack, base_track.MeasuredTrack):
|
||||
measure_cls = ScoreMeasure
|
||||
|
||||
def create(
|
||||
self, *, instrument: Optional[str] = None, num_measures: int = 1, **kwargs: Any
|
||||
) -> None:
|
||||
def create(self, *, num_measures: int = 1, **kwargs: Any) -> None:
|
||||
super().create(**kwargs)
|
||||
|
||||
if instrument is None:
|
||||
self.instrument = 'sf2:/usr/share/sounds/sf2/FluidR3_GM.sf2?bank=0&preset=0'
|
||||
else:
|
||||
self.instrument = instrument
|
||||
|
||||
for _ in range(num_measures):
|
||||
self.append_measure()
|
||||
|
||||
|
@ -317,55 +294,15 @@ class ScoreTrack(pmodel.ScoreTrack, base_track.MeasuredTrack):
|
|||
def create_track_connector(self, **kwargs: Any) -> ScoreTrackConnector:
|
||||
return ScoreTrackConnector(
|
||||
track=self,
|
||||
node_id=self.event_source_name,
|
||||
**kwargs)
|
||||
|
||||
@property
|
||||
def event_source_name(self) -> str:
|
||||
return '%016x-events' % self.id
|
||||
def get_add_mutations(self) -> Iterator[audioproc.Mutation]:
|
||||
yield audioproc.AddNode(
|
||||
description=self.description,
|
||||
id=self.pipeline_node_id,
|
||||
name=self.name)
|
||||
|
||||
@property
|
||||
def instr_name(self) -> str:
|
||||
return '%016x-instr' % self.id
|
||||
yield from self.get_initial_parameter_mutations()
|
||||
|
||||
def add_pipeline_nodes(self) -> None:
|
||||
super().add_pipeline_nodes()
|
||||
|
||||
mixer_node = self.mixer_node
|
||||
|
||||
instrument_node = self._pool.create(
|
||||
pipeline_graph.InstrumentPipelineGraphNode,
|
||||
name="Track Instrument",
|
||||
graph_pos=mixer_node.graph_pos - model.Pos2F(200, 0),
|
||||
track=self)
|
||||
self.project.add_pipeline_graph_node(instrument_node)
|
||||
self.instrument_node = instrument_node
|
||||
|
||||
self.project.add_pipeline_graph_connection(self._pool.create(
|
||||
pipeline_graph.PipelineGraphConnection,
|
||||
source_node=instrument_node, source_port='out:left',
|
||||
dest_node=self.mixer_node, dest_port='in:left'))
|
||||
self.project.add_pipeline_graph_connection(self._pool.create(
|
||||
pipeline_graph.PipelineGraphConnection,
|
||||
source_node=instrument_node, source_port='out:right',
|
||||
dest_node=self.mixer_node, dest_port='in:right'))
|
||||
|
||||
event_source_node = self._pool.create(
|
||||
pipeline_graph.PianoRollPipelineGraphNode,
|
||||
name="Track Events",
|
||||
graph_pos=instrument_node.graph_pos - model.Pos2F(200, 0),
|
||||
track=self)
|
||||
self.project.add_pipeline_graph_node(event_source_node)
|
||||
self.event_source_node = event_source_node
|
||||
|
||||
self.project.add_pipeline_graph_connection(self._pool.create(
|
||||
pipeline_graph.PipelineGraphConnection,
|
||||
source_node=event_source_node, source_port='out',
|
||||
dest_node=instrument_node, dest_port='in'))
|
||||
|
||||
def remove_pipeline_nodes(self) -> None:
|
||||
self.project.remove_pipeline_graph_node(self.event_source_node)
|
||||
self.event_source_node = None
|
||||
self.project.remove_pipeline_graph_node(self.instrument_node)
|
||||
self.instrument_node = None
|
||||
super().remove_pipeline_nodes()
|
||||
def get_remove_mutations(self) -> Iterator[audioproc.Mutation]:
|
||||
yield audioproc.RemoveNode(self.pipeline_node_id)
|
||||
|
|
|
@ -30,8 +30,8 @@ from noisicaa import audioproc
|
|||
from . import project
|
||||
from . import project_client
|
||||
from . import score_track
|
||||
from . import commands_test
|
||||
from . import commands_pb2
|
||||
from . import base_track_test
|
||||
|
||||
|
||||
class ScoreTrackConnectorTest(unittest_mixins.NodeDBMixin, unittest.AsyncTestCase):
|
||||
|
@ -40,7 +40,7 @@ class ScoreTrackConnectorTest(unittest_mixins.NodeDBMixin, unittest.AsyncTestCas
|
|||
|
||||
def test_foo(self):
|
||||
pr = demo_project.basic(self.pool, project.BaseProject, node_db=self.node_db)
|
||||
tr = pr.master_group.tracks[0]
|
||||
tr = pr.pipeline_graph_nodes[-1]
|
||||
|
||||
messages = [] # type: List[str]
|
||||
|
||||
|
@ -48,7 +48,6 @@ class ScoreTrackConnectorTest(unittest_mixins.NodeDBMixin, unittest.AsyncTestCas
|
|||
try:
|
||||
messages.extend(connector.init())
|
||||
|
||||
pr.property_track.insert_measure(1)
|
||||
tr.insert_measure(1)
|
||||
m = tr.measure_list[1].measure
|
||||
m.notes.append(self.pool.create(score_track.Note, pitches=[model.Pitch('D#4')]))
|
||||
|
@ -59,38 +58,9 @@ class ScoreTrackConnectorTest(unittest_mixins.NodeDBMixin, unittest.AsyncTestCas
|
|||
connector.close()
|
||||
|
||||
|
||||
class ScoreTrackTest(commands_test.CommandsTestBase):
|
||||
async def test_add_remove(self):
|
||||
insert_index = await self.client.send_command(commands_pb2.Command(
|
||||
target=self.project.id,
|
||||
add_track=commands_pb2.AddTrack(
|
||||
track_type='score',
|
||||
parent_group_id=self.project.master_group.id)))
|
||||
self.assertEqual(insert_index, 0)
|
||||
|
||||
track = self.project.master_group.tracks[insert_index]
|
||||
self.assertIsInstance(track, project_client.ScoreTrack)
|
||||
|
||||
await self.client.send_command(commands_pb2.Command(
|
||||
target=self.project.id,
|
||||
remove_track=commands_pb2.RemoveTrack(
|
||||
track_id=track.id)))
|
||||
self.assertEqual(len(self.project.master_group.tracks), 0)
|
||||
|
||||
async def _add_track(self, num_measures=None):
|
||||
insert_index = await self.client.send_command(commands_pb2.Command(
|
||||
target=self.project.id,
|
||||
add_track=commands_pb2.AddTrack(
|
||||
track_type='score',
|
||||
parent_group_id=self.project.master_group.id)))
|
||||
|
||||
if num_measures is not None:
|
||||
await self.client.send_command(commands_pb2.Command(
|
||||
target=self.project.id,
|
||||
set_num_measures=commands_pb2.SetNumMeasures(
|
||||
num_measures=num_measures)))
|
||||
|
||||
return self.project.master_group.tracks[insert_index]
|
||||
class ScoreTrackTest(base_track_test.TrackTestMixin, unittest.AsyncTestCase):
|
||||
node_uri = 'builtin://score_track'
|
||||
track_cls = project_client.ScoreTrack
|
||||
|
||||
async def _fill_measure(self, measure):
|
||||
await self.client.send_command(commands_pb2.Command(
|
||||
|
@ -101,28 +71,12 @@ class ScoreTrackTest(commands_test.CommandsTestBase):
|
|||
duration=audioproc.MusicalDuration(1, 4).to_proto())))
|
||||
self.assertEqual(len(measure.notes), 1)
|
||||
|
||||
async def test_set_num_measures(self):
|
||||
track = await self._add_track()
|
||||
|
||||
await self.client.send_command(commands_pb2.Command(
|
||||
target=self.project.id,
|
||||
set_num_measures=commands_pb2.SetNumMeasures(
|
||||
num_measures=10)))
|
||||
self.assertEqual(len(track.measure_list), 10)
|
||||
self.assertEqual(self.project.duration, audioproc.MusicalDuration(10 * 4, 4))
|
||||
|
||||
await self.client.send_command(commands_pb2.Command(
|
||||
target=self.project.id,
|
||||
set_num_measures=commands_pb2.SetNumMeasures(
|
||||
num_measures=5)))
|
||||
self.assertEqual(len(track.measure_list), 5)
|
||||
|
||||
async def test_insert_measure(self):
|
||||
track = await self._add_track()
|
||||
self.assertEqual(len(track.measure_list), 1)
|
||||
|
||||
await self.client.send_command(commands_pb2.Command(
|
||||
target=self.project.id,
|
||||
target=track.id,
|
||||
insert_measure=commands_pb2.InsertMeasure(
|
||||
pos=0,
|
||||
tracks=[track.id])))
|
||||
|
@ -133,7 +87,7 @@ class ScoreTrackTest(commands_test.CommandsTestBase):
|
|||
self.assertEqual(len(track.measure_list), 1)
|
||||
|
||||
await self.client.send_command(commands_pb2.Command(
|
||||
target=self.project.id,
|
||||
target=track.id,
|
||||
insert_measure=commands_pb2.InsertMeasure(
|
||||
tracks=[])))
|
||||
self.assertEqual(len(track.measure_list), 2)
|
||||
|
@ -143,7 +97,7 @@ class ScoreTrackTest(commands_test.CommandsTestBase):
|
|||
self.assertEqual(len(track.measure_list), 1)
|
||||
|
||||
await self.client.send_command(commands_pb2.Command(
|
||||
target=self.project.id,
|
||||
target=track.id,
|
||||
remove_measure=commands_pb2.RemoveMeasure(
|
||||
pos=0,
|
||||
tracks=[])))
|
||||
|
@ -160,16 +114,6 @@ class ScoreTrackTest(commands_test.CommandsTestBase):
|
|||
measure_ids=[track.measure_list[0].id])))
|
||||
self.assertIsNot(old_measure, track.measure_list[0].measure)
|
||||
|
||||
async def test_set_instrument(self):
|
||||
track = await self._add_track()
|
||||
|
||||
await self.client.send_command(commands_pb2.Command(
|
||||
target=track.id,
|
||||
set_instrument=commands_pb2.SetInstrument(
|
||||
instrument='sf2:/usr/share/sounds/sf2/FluidR3_GM.sf2?bank=0&preset=2')))
|
||||
self.assertEqual(
|
||||
track.instrument, 'sf2:/usr/share/sounds/sf2/FluidR3_GM.sf2?bank=0&preset=2')
|
||||
|
||||
async def test_set_clef(self):
|
||||
track = await self._add_track()
|
||||
measure = track.measure_list[0].measure
|
||||
|
@ -192,6 +136,15 @@ class ScoreTrackTest(commands_test.CommandsTestBase):
|
|||
key_signature=model.KeySignature('D minor').to_proto())))
|
||||
self.assertEqual(measure.key_signature, model.KeySignature('D minor'))
|
||||
|
||||
async def test_transpose_octaves(self):
|
||||
track = await self._add_track()
|
||||
|
||||
await self.client.send_command(commands_pb2.Command(
|
||||
target=track.id,
|
||||
update_track_properties=commands_pb2.UpdateTrackProperties(
|
||||
transpose_octaves=1)))
|
||||
self.assertEqual(track.transpose_octaves, 1)
|
||||
|
||||
async def test_insert_note(self):
|
||||
track = await self._add_track()
|
||||
measure = track.measure_list[0].measure
|
||||
|
@ -340,19 +293,19 @@ class ScoreTrackTest(commands_test.CommandsTestBase):
|
|||
self.assertNotEqual(new_measure.id, measure.id)
|
||||
self.assertEqual(new_measure.notes[0].pitches[0], model.Pitch('F2'))
|
||||
|
||||
async def test_paste_link(self):
|
||||
track = await self._add_track(num_measures=3)
|
||||
# async def test_paste_link(self):
|
||||
# track = await self._add_track(num_measures=3)
|
||||
|
||||
measure = track.measure_list[0].measure
|
||||
await self._fill_measure(measure)
|
||||
# measure = track.measure_list[0].measure
|
||||
# await self._fill_measure(measure)
|
||||
|
||||
clipboard = measure.serialize()
|
||||
# clipboard = measure.serialize()
|
||||
|
||||
await self.client.send_command(commands_pb2.Command(
|
||||
target=self.project.id,
|
||||
paste_measures=commands_pb2.PasteMeasures(
|
||||
mode='link',
|
||||
src_objs=[clipboard],
|
||||
target_ids=[track.measure_list[1].id, track.measure_list[2].id])))
|
||||
self.assertIs(track.measure_list[1].measure, measure)
|
||||
self.assertIs(track.measure_list[2].measure, measure)
|
||||
# await self.client.send_command(commands_pb2.Command(
|
||||
# target=self.project.id,
|
||||
# paste_measures=commands_pb2.PasteMeasures(
|
||||
# mode='link',
|
||||
# src_objs=[clipboard],
|
||||
# target_ids=[track.measure_list[1].id, track.measure_list[2].id])))
|
||||
# self.assertIs(track.measure_list[1].measure, measure)
|
||||
# self.assertIs(track.measure_list[2].measure, measure)
|
||||
|
|
|
@ -1,75 +0,0 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
# @begin:license
|
||||
#
|
||||
# Copyright (c) 2015-2018, 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 Any, Optional, Dict # pylint: disable=unused-import
|
||||
|
||||
from noisicaa import model
|
||||
from noisicaa import core # pylint: disable=unused-import
|
||||
from . import pmodel
|
||||
from . import base_track
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TrackGroup(pmodel.TrackGroup, base_track.Track):
|
||||
def create(self, *, num_measures: Optional[int] = None, **kwargs: Any) -> None:
|
||||
super().create(**kwargs)
|
||||
|
||||
def create_track_connector(self, **kwargs: Any) -> base_track.TrackConnector:
|
||||
raise RuntimeError("No track connector for TrackGroup")
|
||||
|
||||
@property
|
||||
def default_mixer_name(self) -> str:
|
||||
return "Group Mixer"
|
||||
|
||||
def add_pipeline_nodes(self) -> None:
|
||||
super().add_pipeline_nodes()
|
||||
for track in self.tracks:
|
||||
track.add_pipeline_nodes()
|
||||
|
||||
def remove_pipeline_nodes(self) -> None:
|
||||
for track in self.tracks:
|
||||
track.remove_pipeline_nodes()
|
||||
super().remove_pipeline_nodes()
|
||||
|
||||
|
||||
class MasterTrackGroup(pmodel.MasterTrackGroup, TrackGroup):
|
||||
@property
|
||||
def parent_audio_sink_name(self) -> str:
|
||||
return 'sink'
|
||||
|
||||
@property
|
||||
def parent_audio_sink_node(self) -> pmodel.BasePipelineGraphNode:
|
||||
return self.project.audio_out_node
|
||||
|
||||
@property
|
||||
def relative_position_to_parent_audio_out(self) -> model.Pos2F:
|
||||
return model.Pos2F(-200, 0)
|
||||
|
||||
@property
|
||||
def default_mixer_name(self) -> str:
|
||||
return "Master Mixer"
|
||||
|
||||
@property
|
||||
def mixer_name(self) -> str:
|
||||
return '%016x-master-mixer' % self.id
|
|
@ -1,84 +0,0 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
# @begin:license
|
||||
#
|
||||
# Copyright (c) 2015-2018, 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 noisidev import unittest
|
||||
from noisidev import unittest_mixins
|
||||
from . import project
|
||||
from . import track_group
|
||||
from . import beat_track
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Signal(object):
|
||||
def __init__(self):
|
||||
self.__set = False
|
||||
|
||||
def set(self):
|
||||
self.__set = True
|
||||
|
||||
def clear(self):
|
||||
self.__set = False
|
||||
|
||||
@property
|
||||
def is_set(self):
|
||||
return self.__set
|
||||
|
||||
|
||||
class TrackGroupTest(unittest_mixins.NodeDBMixin, unittest.AsyncTestCase):
|
||||
async def setup_testcase(self):
|
||||
self.pool = project.Pool()
|
||||
self.project = self.pool.create(project.BaseProject, node_db=self.node_db)
|
||||
|
||||
def test_duration_changed(self):
|
||||
duration_changed = Signal()
|
||||
self.project.master_group.duration_changed.add(duration_changed.set)
|
||||
|
||||
logger.info("0 -------------")
|
||||
grp = self.pool.create(track_group.TrackGroup, name="group")
|
||||
self.project.master_group.tracks.append(grp)
|
||||
self.assertTrue(duration_changed.is_set)
|
||||
|
||||
logger.info("1 -------------")
|
||||
duration_changed.clear()
|
||||
track = self.pool.create(beat_track.BeatTrack, name="track1")
|
||||
track.append_measure()
|
||||
track.append_measure()
|
||||
grp.tracks.append(track)
|
||||
self.assertTrue(duration_changed.is_set)
|
||||
|
||||
logger.info("2 -------------")
|
||||
duration_changed.clear()
|
||||
track.append_measure()
|
||||
self.assertTrue(duration_changed.is_set)
|
||||
|
||||
logger.info("3 -------------")
|
||||
duration_changed.clear()
|
||||
track.remove_measure(0)
|
||||
self.assertTrue(duration_changed.is_set)
|
||||
|
||||
logger.info("4 -------------")
|
||||
duration_changed.clear()
|
||||
del self.project.master_group.tracks[track.index]
|
||||
self.assertTrue(duration_changed.is_set)
|
|
@ -76,8 +76,35 @@ class Builtins(object):
|
|||
]
|
||||
)
|
||||
|
||||
SampleScriptDescription = node_db.NodeDescription(
|
||||
display_name='Sample Script',
|
||||
EventSourceDescription = node_db.NodeDescription(
|
||||
display_name='Events',
|
||||
type=node_db.NodeDescription.EVENT_SOURCE,
|
||||
ports=[
|
||||
node_db.PortDescription(
|
||||
name='out',
|
||||
direction=node_db.PortDescription.OUTPUT,
|
||||
type=node_db.PortDescription.EVENTS,
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
ControlTrackDescription = node_db.NodeDescription(
|
||||
display_name='Control Track',
|
||||
type=node_db.NodeDescription.PROCESSOR,
|
||||
processor=node_db.ProcessorDescription(
|
||||
type=node_db.ProcessorDescription.CV_GENERATOR,
|
||||
),
|
||||
ports=[
|
||||
node_db.PortDescription(
|
||||
name='out',
|
||||
direction=node_db.PortDescription.OUTPUT,
|
||||
type=node_db.PortDescription.ARATE_CONTROL,
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
SampleTrackDescription = node_db.NodeDescription(
|
||||
display_name='Sample Track',
|
||||
type=node_db.NodeDescription.PROCESSOR,
|
||||
processor=node_db.ProcessorDescription(
|
||||
type=node_db.ProcessorDescription.SAMPLE_SCRIPT,
|
||||
|
@ -96,9 +123,12 @@ class Builtins(object):
|
|||
]
|
||||
)
|
||||
|
||||
EventSourceDescription = node_db.NodeDescription(
|
||||
display_name='Events',
|
||||
type=node_db.NodeDescription.EVENT_SOURCE,
|
||||
BeatTrackDescription = node_db.NodeDescription(
|
||||
display_name='Beat Track',
|
||||
type=node_db.NodeDescription.PROCESSOR,
|
||||
processor=node_db.ProcessorDescription(
|
||||
type=node_db.ProcessorDescription.PIANOROLL,
|
||||
),
|
||||
ports=[
|
||||
node_db.PortDescription(
|
||||
name='out',
|
||||
|
@ -108,23 +138,8 @@ class Builtins(object):
|
|||
]
|
||||
)
|
||||
|
||||
CVGeneratorDescription = node_db.NodeDescription(
|
||||
display_name='Control Value',
|
||||
type=node_db.NodeDescription.PROCESSOR,
|
||||
processor=node_db.ProcessorDescription(
|
||||
type=node_db.ProcessorDescription.CV_GENERATOR,
|
||||
),
|
||||
ports=[
|
||||
node_db.PortDescription(
|
||||
name='out',
|
||||
direction=node_db.PortDescription.OUTPUT,
|
||||
type=node_db.PortDescription.ARATE_CONTROL,
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
PianoRollDescription = node_db.NodeDescription(
|
||||
display_name='Piano Roll',
|
||||
ScoreTrackDescription = node_db.NodeDescription(
|
||||
display_name='Score Track',
|
||||
type=node_db.NodeDescription.PROCESSOR,
|
||||
processor=node_db.ProcessorDescription(
|
||||
type=node_db.ProcessorDescription.PIANOROLL,
|
||||
|
@ -286,10 +301,11 @@ class Builtins(object):
|
|||
class BuiltinScanner(scanner.Scanner):
|
||||
def scan(self) -> Iterator[Tuple[str, node_db.NodeDescription]]:
|
||||
yield ('builtin://track_mixer', Builtins.TrackMixerDescription)
|
||||
yield ('builtin://sample_script', Builtins.SampleScriptDescription)
|
||||
yield ('builtin://event_source', Builtins.EventSourceDescription)
|
||||
yield ('builtin://cvgenerator', Builtins.CVGeneratorDescription)
|
||||
yield ('builtin://pianoroll', Builtins.PianoRollDescription)
|
||||
yield ('builtin://control_track', Builtins.ControlTrackDescription)
|
||||
yield ('builtin://sample_track', Builtins.SampleTrackDescription)
|
||||
yield ('builtin://beat_track', Builtins.BeatTrackDescription)
|
||||
yield ('builtin://score_track', Builtins.ScoreTrackDescription)
|
||||
yield ('builtin://sink', Builtins.RealmSinkDescription)
|
||||
yield ('builtin://child_realm', Builtins.ChildRealmDescription)
|
||||
yield ('builtin://fluidsynth', Builtins.FluidSynthDescription)
|
||||
|
|
|
@ -19,7 +19,6 @@
|
|||
# @end:license
|
||||
|
||||
add_python_package(
|
||||
dock_widget.py
|
||||
editor_app.py
|
||||
editor_window.py
|
||||
flowlayout.py
|
||||
|
@ -30,10 +29,8 @@ add_python_package(
|
|||
mute_button.py
|
||||
piano.py
|
||||
piano_test.py
|
||||
pipeline_graph_view.py
|
||||
pipeline_graph_view_test.py
|
||||
pipeline_perf_monitor.py
|
||||
project_properties_dock.py
|
||||
player_state.py
|
||||
project_registry.py
|
||||
project_view.py
|
||||
project_view_test.py
|
||||
|
@ -44,17 +41,14 @@ add_python_package(
|
|||
selection_set.py
|
||||
session_helpers.py
|
||||
settings.py
|
||||
slots.py
|
||||
stat_monitor.py
|
||||
svg_symbol_filetest.py
|
||||
svg_symbol.py
|
||||
svg_symbol_test.py
|
||||
tool_dock.py
|
||||
tools.py
|
||||
track_properties_dock.py
|
||||
tracks_dock.py
|
||||
tracks_dock_test.py
|
||||
ui_base.py
|
||||
ui_process.py
|
||||
)
|
||||
|
||||
add_subdirectory(track_items)
|
||||
add_subdirectory(track_list)
|
||||
add_subdirectory(pipeline_graph)
|
||||
|
|
|
@ -1,141 +0,0 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
# @begin:license
|
||||
#
|
||||
# Copyright (c) 2015-2018, 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 Any, Union
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5 import QtCore
|
||||
from PyQt5 import QtGui
|
||||
from PyQt5 import QtWidgets
|
||||
|
||||
from . import ui_base
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DockWidget(ui_base.CommonMixin, QtWidgets.QDockWidget):
|
||||
def __init__(
|
||||
self, title: str, identifier: str,
|
||||
allowed_areas: Union[Qt.DockWidgetAreas, Qt.DockWidgetArea] = Qt.AllDockWidgetAreas,
|
||||
initial_area: QtCore.Qt.DockWidgetArea = Qt.RightDockWidgetArea,
|
||||
initial_visible: bool = False,
|
||||
initial_floating: bool = False,
|
||||
initial_pos: QtCore.QPoint = None,
|
||||
**kwargs: Any) -> None:
|
||||
super().__init__(**kwargs)
|
||||
self._identifier = identifier
|
||||
|
||||
self.setWindowTitle(title)
|
||||
self.setObjectName('dock:' + identifier)
|
||||
self.setAllowedAreas(allowed_areas)
|
||||
self.parent().addDockWidget(initial_area, self)
|
||||
self.setVisible(initial_visible)
|
||||
self.setFloating(initial_floating)
|
||||
if initial_floating and initial_pos is not None:
|
||||
self.move(initial_pos)
|
||||
|
||||
name = QtWidgets.QLabel(self)
|
||||
name.setTextFormat(Qt.PlainText)
|
||||
name.setText(self.windowTitle())
|
||||
#self.windowTitleChanged.connect(name.setText)
|
||||
|
||||
self.hide_button = QtWidgets.QToolButton(
|
||||
icon=QtGui.QIcon.fromTheme('list-remove'),
|
||||
autoRaise=True,
|
||||
focusPolicy=Qt.NoFocus)
|
||||
self.hide_button.clicked.connect(self.toggleHide)
|
||||
|
||||
self.float_button = QtWidgets.QToolButton(
|
||||
icon=QtGui.QIcon.fromTheme('view-fullscreen'),
|
||||
checkable=True,
|
||||
autoRaise=True,
|
||||
focusPolicy=Qt.NoFocus)
|
||||
self.float_button.toggled.connect(self.setFloating)
|
||||
|
||||
self.close_button = QtWidgets.QToolButton(
|
||||
icon=QtGui.QIcon.fromTheme('window-close'),
|
||||
autoRaise=True,
|
||||
focusPolicy=Qt.NoFocus)
|
||||
self.close_button.clicked.connect(lambda: self.setVisible(False))
|
||||
|
||||
layout = QtWidgets.QHBoxLayout()
|
||||
layout.setContentsMargins(QtCore.QMargins(0, 0, 0, 0))
|
||||
layout.addWidget(self.hide_button)
|
||||
layout.addWidget(name, 1)
|
||||
layout.addWidget(self.float_button)
|
||||
layout.addWidget(self.close_button)
|
||||
|
||||
self.titlebar = QtWidgets.QWidget(self)
|
||||
self.titlebar.setLayout(layout)
|
||||
self.setTitleBarWidget(self.titlebar)
|
||||
|
||||
self.onFeaturesChanged(self.features())
|
||||
self.featuresChanged.connect(self.onFeaturesChanged)
|
||||
self.onTopLevelChanged(self.isFloating())
|
||||
self.topLevelChanged.connect(self.onTopLevelChanged)
|
||||
|
||||
self.main_widget = None # type: QtWidgets.QWidget
|
||||
self.filler = QtWidgets.QWidget(self)
|
||||
|
||||
self.main_layout = QtWidgets.QVBoxLayout()
|
||||
self.main_layout.setContentsMargins(QtCore.QMargins(0, 0, 0, 0))
|
||||
self.main_layout.setSizeConstraint(QtWidgets.QLayout.SetMinAndMaxSize)
|
||||
self.main_layout.addWidget(self.filler)
|
||||
|
||||
top_widget = QtWidgets.QWidget(self)
|
||||
top_widget.setLayout(self.main_layout)
|
||||
super().setWidget(top_widget)
|
||||
|
||||
def setWidget(self, widget: QtWidgets.QWidget) -> None:
|
||||
if self.main_widget is not None:
|
||||
self.main_widget.setParent(None)
|
||||
self.main_layout.removeWidget(self.main_widget)
|
||||
|
||||
if self.widget is not None:
|
||||
self.main_layout.addWidget(widget)
|
||||
self.filler.setMaximumHeight(0)
|
||||
self.filler.hide()
|
||||
|
||||
self.main_widget = widget
|
||||
|
||||
def onTopLevelChanged(self, top_level: bool) -> None:
|
||||
self.hide_button.setDisabled(top_level)
|
||||
self.float_button.setChecked(top_level)
|
||||
if top_level:
|
||||
if self.widget() is not None:
|
||||
self.widget().show()
|
||||
self.hide_button.setIcon(QtGui.QIcon.fromTheme('list-remove'))
|
||||
|
||||
def onFeaturesChanged(self, features: QtWidgets.QDockWidget.DockWidgetFeatures) -> None:
|
||||
self.float_button.setVisible(features & QtWidgets.QDockWidget.DockWidgetFloatable != 0)
|
||||
self.close_button.setVisible(features & QtWidgets.QDockWidget.DockWidgetClosable != 0)
|
||||
|
||||
def toggleHide(self) -> None:
|
||||
if self.main_widget.isHidden():
|
||||
self.filler.hide()
|
||||
self.main_widget.show()
|
||||
self.hide_button.setIcon(QtGui.QIcon.fromTheme('list-remove'))
|
||||
else:
|
||||
self.main_widget.hide()
|
||||
self.filler.show()
|
||||
self.hide_button.setIcon(QtGui.QIcon.fromTheme('list-add'))
|
|
@ -37,7 +37,6 @@ from .project_view import ProjectView
|
|||
from . import ui_base
|
||||
from . import instrument_library
|
||||
from . import qprogressindicator
|
||||
from . import dock_widget # pylint: disable=unused-import
|
||||
from . import project_registry
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -55,7 +54,6 @@ class EditorWindow(ui_base.AbstractEditorWindow):
|
|||
def __init__(self, **kwargs: Any) -> None:
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self._docks = [] # type: List[dock_widget.DockWidget]
|
||||
self._settings_dialog = SettingsDialog(parent=self, context=self.context)
|
||||
|
||||
self._instrument_library_dialog = instrument_library.InstrumentLibraryDialog(
|
||||
|
@ -184,6 +182,10 @@ class EditorWindow(ui_base.AbstractEditorWindow):
|
|||
self._set_num_measures_action.setStatusTip("Set the number of measures in the project")
|
||||
self._set_num_measures_action.triggered.connect(self.onSetNumMeasures)
|
||||
|
||||
self._set_bpm_action = QtWidgets.QAction("Set BPM", self)
|
||||
self._set_bpm_action.setStatusTip("Set the project's beats per second")
|
||||
self._set_bpm_action.triggered.connect(self.onSetBPM)
|
||||
|
||||
self._restart_action = QtWidgets.QAction("Restart", self)
|
||||
self._restart_action.setShortcut("F5")
|
||||
self._restart_action.setShortcutContext(Qt.ApplicationShortcut)
|
||||
|
@ -302,7 +304,8 @@ class EditorWindow(ui_base.AbstractEditorWindow):
|
|||
self._edit_menu.addAction(self._paste_action)
|
||||
self._edit_menu.addAction(self._paste_as_link_action)
|
||||
self._project_menu.addSeparator()
|
||||
self._edit_menu.addAction(self._set_num_measures_action)
|
||||
#self._edit_menu.addAction(self._set_num_measures_action)
|
||||
self._edit_menu.addAction(self._set_bpm_action)
|
||||
|
||||
self._view_menu = menu_bar.addMenu("View")
|
||||
|
||||
|
@ -330,8 +333,8 @@ class EditorWindow(ui_base.AbstractEditorWindow):
|
|||
self.toolbar.addAction(self._player_loop_action)
|
||||
self.toolbar.addSeparator()
|
||||
self.toolbar.addAction(self._player_move_to_start_action)
|
||||
self.toolbar.addAction(self._player_move_to_prev_action)
|
||||
self.toolbar.addAction(self._player_move_to_next_action)
|
||||
#self.toolbar.addAction(self._player_move_to_prev_action)
|
||||
#self.toolbar.addAction(self._player_move_to_next_action)
|
||||
self.toolbar.addAction(self._player_move_to_end_action)
|
||||
|
||||
self.addToolBar(Qt.TopToolBarArea, self.toolbar)
|
||||
|
@ -558,6 +561,10 @@ class EditorWindow(ui_base.AbstractEditorWindow):
|
|||
view = self._project_tabs.currentWidget()
|
||||
view.onSetNumMeasures()
|
||||
|
||||
def onSetBPM(self) -> None:
|
||||
view = self._project_tabs.currentWidget()
|
||||
view.onSetBPM()
|
||||
|
||||
def onPlayingChanged(self, playing: bool) -> None:
|
||||
if playing:
|
||||
self._player_toggle_action.setIcon(
|
||||
|
|
|
@ -19,15 +19,15 @@
|
|||
# @end:license
|
||||
|
||||
add_python_package(
|
||||
track_item_tests.py
|
||||
base_track_item.py
|
||||
base_track_item_test.py
|
||||
beat_track_item.py
|
||||
beat_track_item_test.py
|
||||
control_track_item.py
|
||||
control_track_item_test.py
|
||||
sample_track_item.py
|
||||
sample_track_item_test.py
|
||||
score_track_item.py
|
||||
score_track_item_test.py
|
||||
base_node.py
|
||||
base_node_test.py
|
||||
canvas.py
|
||||
canvas_test.py
|
||||
node_widget.py
|
||||
plugin_ui.py
|
||||
score_track_node.py
|
||||
toolbox.py
|
||||
track_node.py
|
||||
view.py
|
||||
view_test.py
|
||||
)
|
|
@ -1,3 +1,5 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
# @begin:license
|
||||
#
|
||||
# Copyright (c) 2015-2018, Benjamin Niemann <pink@odahoda.de>
|
||||
|
@ -18,7 +20,4 @@
|
|||
#
|
||||
# @end:license
|
||||
|
||||
from .beat_track_item import BeatTrackEditorItem
|
||||
from .control_track_item import ControlTrackEditorItem
|
||||
from .score_track_item import ScoreTrackEditorItem
|
||||
from .sample_track_item import SampleTrackEditorItem
|
||||
from .view import PipelineGraphView
|
|
@ -0,0 +1,815 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
# @begin:license
|
||||
#
|
||||
# Copyright (c) 2015-2018, 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 functools
|
||||
import logging
|
||||
from typing import cast, Any, Optional, Dict, List, Set # pylint: disable=unused-import
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5 import QtCore
|
||||
from PyQt5 import QtGui
|
||||
from PyQt5 import QtSvg # type: ignore
|
||||
from PyQt5 import QtWidgets
|
||||
|
||||
from noisicaa import model
|
||||
from noisicaa import music
|
||||
from noisicaa import node_db
|
||||
from noisicaa.ui import ui_base
|
||||
|
||||
from . import node_widget
|
||||
from . import plugin_ui
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
port_colors = {
|
||||
node_db.PortDescription.AUDIO: QtGui.QColor(150, 150, 150),
|
||||
node_db.PortDescription.ARATE_CONTROL: QtGui.QColor(100, 255, 180),
|
||||
node_db.PortDescription.KRATE_CONTROL: QtGui.QColor(100, 180, 255),
|
||||
node_db.PortDescription.EVENTS: QtGui.QColor(255, 180, 100),
|
||||
}
|
||||
|
||||
|
||||
class SelectColorAction(QtWidgets.QWidgetAction):
|
||||
colorSelected = QtCore.pyqtSignal(model.Color)
|
||||
|
||||
def __init__(self, parent: QtCore.QObject) -> None:
|
||||
super().__init__(parent)
|
||||
|
||||
self.setDefaultWidget(SelectColorWidget(parent=parent, action=self))
|
||||
|
||||
|
||||
class ColorBox(QtWidgets.QWidget):
|
||||
clicked = QtCore.pyqtSignal()
|
||||
|
||||
def __init__(self, color: model.Color, parent: QtWidgets.QWidget) -> None:
|
||||
super().__init__(parent)
|
||||
|
||||
self.__color = color
|
||||
|
||||
self.setFixedSize(24, 24)
|
||||
|
||||
def paintEvent(self, event: QtGui.QPaintEvent) -> None:
|
||||
super().paintEvent(event)
|
||||
|
||||
painter = QtGui.QPainter(self)
|
||||
try:
|
||||
painter.fillRect(self.rect(), Qt.black)
|
||||
painter.fillRect(self.rect().adjusted(1, 1, -1, -1), Qt.white)
|
||||
painter.fillRect(self.rect().adjusted(2, 2, -2, -2), QtGui.QColor.fromRgbF(
|
||||
self.__color.r,
|
||||
self.__color.g,
|
||||
self.__color.b,
|
||||
self.__color.a))
|
||||
|
||||
finally:
|
||||
painter.end()
|
||||
|
||||
def mousePressEvent(self, event: QtGui.QMouseEvent) -> None:
|
||||
if event.button() == Qt.LeftButton:
|
||||
self.clicked.emit()
|
||||
|
||||
|
||||
class SelectColorWidget(QtWidgets.QWidget):
|
||||
colors = [
|
||||
model.Color(0.7, 0.7, 0.7),
|
||||
model.Color(0.8, 0.8, 0.8),
|
||||
model.Color(0.9, 0.9, 0.9),
|
||||
model.Color(1.0, 1.0, 1.0),
|
||||
|
||||
model.Color(1.0, 0.6, 0.6),
|
||||
model.Color(1.0, 0.7, 0.7),
|
||||
model.Color(1.0, 0.8, 0.8),
|
||||
model.Color(1.0, 0.9, 0.9),
|
||||
|
||||
model.Color(1.0, 0.6, 0.1),
|
||||
model.Color(1.0, 0.7, 0.3),
|
||||
model.Color(1.0, 0.8, 0.6),
|
||||
model.Color(1.0, 0.9, 0.8),
|
||||
|
||||
model.Color(0.6, 1.0, 0.6),
|
||||
model.Color(0.7, 1.0, 0.7),
|
||||
model.Color(0.8, 1.0, 0.8),
|
||||
model.Color(0.9, 1.0, 0.9),
|
||||
|
||||
model.Color(0.6, 0.6, 1.0),
|
||||
model.Color(0.7, 0.7, 1.0),
|
||||
model.Color(0.8, 0.8, 1.0),
|
||||
model.Color(0.9, 0.9, 1.0),
|
||||
|
||||
model.Color(1.0, 0.6, 1.0),
|
||||
model.Color(1.0, 0.7, 1.0),
|
||||
model.Color(1.0, 0.8, 1.0),
|
||||
model.Color(1.0, 0.9, 1.0),
|
||||
|
||||
model.Color(1.0, 1.0, 0.6),
|
||||
model.Color(1.0, 1.0, 0.7),
|
||||
model.Color(1.0, 1.0, 0.8),
|
||||
model.Color(1.0, 1.0, 0.9),
|
||||
|
||||
model.Color(0.6, 1.0, 1.0),
|
||||
model.Color(0.7, 1.0, 1.0),
|
||||
model.Color(0.8, 1.0, 1.0),
|
||||
model.Color(0.9, 1.0, 1.0),
|
||||
]
|
||||
|
||||
def __init__(self, *, action: SelectColorAction, **kwargs: Any) -> None:
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.__action = action
|
||||
|
||||
layout = QtWidgets.QGridLayout()
|
||||
layout.setContentsMargins(QtCore.QMargins(2, 2, 2, 2))
|
||||
layout.setSpacing(2)
|
||||
self.setLayout(layout)
|
||||
|
||||
for idx, color in enumerate(self.colors):
|
||||
w = ColorBox(color, self)
|
||||
w.clicked.connect(functools.partial(self.__action.colorSelected.emit, color))
|
||||
layout.addWidget(w, idx // 8, idx % 8)
|
||||
|
||||
|
||||
class NodeProps(QtCore.QObject):
|
||||
contentRectChanged = QtCore.pyqtSignal(QtCore.QRectF)
|
||||
canvasRectChanged = QtCore.pyqtSignal(QtCore.QRectF)
|
||||
|
||||
|
||||
class Title(QtWidgets.QGraphicsSimpleTextItem):
|
||||
def __init__(self, name: str, parent: 'Node') -> None:
|
||||
super().__init__(parent)
|
||||
|
||||
self.setText(name)
|
||||
self.setFlag(QtWidgets.QGraphicsItem.ItemClipsToShape, True)
|
||||
self.setAcceptedMouseButtons(Qt.LeftButton)
|
||||
|
||||
self.__width = None # type: float
|
||||
|
||||
def boundingRect(self) -> QtCore.QRectF:
|
||||
bounding_rect = super().boundingRect()
|
||||
if self.__width is not None:
|
||||
bounding_rect.setWidth(self.__width)
|
||||
return bounding_rect
|
||||
|
||||
def shape(self) -> QtGui.QPainterPath:
|
||||
shape = QtGui.QPainterPath()
|
||||
shape.addRect(self.boundingRect())
|
||||
return shape
|
||||
|
||||
def setWidth(self, width: float) -> None:
|
||||
self.__width = width
|
||||
|
||||
def mouseDoubleClickEvent(self, event: QtWidgets.QGraphicsSceneMouseEvent) -> None:
|
||||
logger.error("click")
|
||||
cast(Node, self.parentItem()).renameNode()
|
||||
|
||||
|
||||
class Box(QtWidgets.QGraphicsPathItem):
|
||||
def mousePressEvent(self, event: QtWidgets.QGraphicsSceneMouseEvent) -> None:
|
||||
# swallow mouse press events (which aren't handled by some other of the
|
||||
# node's items) to prevent the canvas from triggering a rubber band
|
||||
# selection.
|
||||
event.accept()
|
||||
|
||||
|
||||
class NodeIcon(QtWidgets.QGraphicsItem):
|
||||
def __init__(self, icon: QtSvg.QSvgRenderer, parent: QtWidgets.QGraphicsItem) -> None:
|
||||
super().__init__(parent)
|
||||
|
||||
self.__icon = icon
|
||||
self.__size = QtCore.QSizeF()
|
||||
self.__pixmap = None # type: QtGui.QPixmap
|
||||
|
||||
def setRect(self, rect: QtCore.QRectF) -> None:
|
||||
self.prepareGeometryChange()
|
||||
self.setPos(rect.topLeft())
|
||||
self.__size = rect.size()
|
||||
|
||||
def boundingRect(self) -> QtCore.QRectF:
|
||||
return QtCore.QRectF(QtCore.QPointF(), self.__size)
|
||||
|
||||
def paint(
|
||||
self, painter: QtGui.QPainter, option: QtWidgets.QStyleOptionGraphicsItem,
|
||||
widget: Optional[QtWidgets.QWidget] = None) -> None:
|
||||
size = min(self.__size.width(), self.__size.height())
|
||||
size = int(size - 0.4 * max(0, size - 50))
|
||||
|
||||
if size < 10:
|
||||
return
|
||||
|
||||
pixmap_size = QtCore.QSize(size, size)
|
||||
if self.__pixmap is None or self.__pixmap.size() != pixmap_size:
|
||||
self.__pixmap = QtGui.QPixmap(pixmap_size)
|
||||
self.__pixmap.fill(QtGui.QColor(0, 0, 0, 0))
|
||||
pixmap_painter = QtGui.QPainter(self.__pixmap)
|
||||
try:
|
||||
self.__icon.render(pixmap_painter, QtCore.QRectF(0, 0, size, size))
|
||||
finally:
|
||||
pixmap_painter.end()
|
||||
|
||||
painter.setOpacity(min(0.8, max(0.2, 0.8 - (size - 30) / 100)))
|
||||
painter.drawPixmap(
|
||||
int((self.__size.width() - size) / 2),
|
||||
int((self.__size.height() - size) / 2),
|
||||
self.__pixmap)
|
||||
|
||||
|
||||
class PortLabel(QtWidgets.QGraphicsRectItem):
|
||||
def __init__(self, port: 'Port') -> None:
|
||||
super().__init__()
|
||||
|
||||
self.setZValue(100000)
|
||||
|
||||
self.__text = QtWidgets.QGraphicsSimpleTextItem(self)
|
||||
tooltip = '%s: ' % port.name()
|
||||
tooltip += {
|
||||
(node_db.PortDescription.AUDIO, node_db.PortDescription.INPUT): "audio input",
|
||||
(node_db.PortDescription.AUDIO, node_db.PortDescription.OUTPUT): "audio output",
|
||||
(node_db.PortDescription.KRATE_CONTROL, node_db.PortDescription.INPUT): "control input",
|
||||
(node_db.PortDescription.KRATE_CONTROL, node_db.PortDescription.OUTPUT):
|
||||
"control output",
|
||||
(node_db.PortDescription.ARATE_CONTROL, node_db.PortDescription.INPUT): "control input",
|
||||
(node_db.PortDescription.ARATE_CONTROL, node_db.PortDescription.OUTPUT):
|
||||
"control output",
|
||||
(node_db.PortDescription.EVENTS, node_db.PortDescription.INPUT): "event input",
|
||||
(node_db.PortDescription.EVENTS, node_db.PortDescription.OUTPUT): "event output",
|
||||
}[(port.type(), port.direction())]
|
||||
self.__text.setText(tooltip)
|
||||
self.__text.setPos(4, 2)
|
||||
|
||||
text_box = self.__text.boundingRect()
|
||||
|
||||
pen = QtGui.QPen()
|
||||
pen.setColor(Qt.black)
|
||||
pen.setWidth(1)
|
||||
self.setPen(pen)
|
||||
self.setBrush(QtGui.QColor(255, 255, 200))
|
||||
self.setRect(0, 0, text_box.width() + 8, text_box.height() + 4)
|
||||
|
||||
|
||||
class Port(QtWidgets.QGraphicsPathItem):
|
||||
def __init__(self, port_desc: node_db.PortDescription, parent: 'Node') -> None:
|
||||
super().__init__(parent)
|
||||
|
||||
self.__desc = port_desc
|
||||
|
||||
self.__highlighted = False
|
||||
|
||||
self.__tooltip = None # type: PortLabel
|
||||
|
||||
def setup(self) -> None:
|
||||
self.__tooltip = PortLabel(self)
|
||||
self.scene().addItem(self.__tooltip)
|
||||
|
||||
self.__update()
|
||||
|
||||
def cleanup(self) -> None:
|
||||
if self.__tooltip is not None:
|
||||
self.scene().removeItem(self.__tooltip)
|
||||
self.__tooltip = None
|
||||
|
||||
def name(self) -> str:
|
||||
return self.__desc.name
|
||||
|
||||
def direction(self) -> node_db.PortDescription.Direction:
|
||||
return self.__desc.direction
|
||||
|
||||
def type(self) -> node_db.PortDescription.Type:
|
||||
return self.__desc.type
|
||||
|
||||
def node(self) -> 'Node':
|
||||
return cast(Node, self.parentItem())
|
||||
|
||||
def highlighted(self) -> bool:
|
||||
return self.__highlighted
|
||||
|
||||
def setHighlighted(self, highlighted: bool) -> None:
|
||||
self.__highlighted = highlighted
|
||||
self.__update()
|
||||
|
||||
def canConnectTo(self, port: 'Port') -> bool:
|
||||
if self.__desc.type != port.__desc.type:
|
||||
return False
|
||||
|
||||
if self.__desc.direction == port.__desc.direction:
|
||||
return False
|
||||
|
||||
if self.__desc.direction == node_db.PortDescription.INPUT:
|
||||
src = port
|
||||
dest = self
|
||||
else:
|
||||
src = self
|
||||
dest = port
|
||||
|
||||
upstream_nodes = {node.id for node in src.node().upstream_nodes()}
|
||||
upstream_nodes.add(src.node().id())
|
||||
if dest.node().id() in upstream_nodes:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def handleScenePos(self) -> QtCore.QPointF:
|
||||
if not self.isVisible():
|
||||
return self.scenePos()
|
||||
elif self.__desc.direction == node_db.PortDescription.INPUT:
|
||||
return self.scenePos() + QtCore.QPointF(-10, 0)
|
||||
else:
|
||||
return self.scenePos() + QtCore.QPointF(10, 0)
|
||||
|
||||
def __update(self) -> None:
|
||||
color = port_colors[self.__desc.type]
|
||||
|
||||
if self.__highlighted:
|
||||
self.setOpacity(1.0)
|
||||
|
||||
self.__tooltip.setVisible(self.__highlighted)
|
||||
ttpos = self.scenePos()
|
||||
ttpos += QtCore.QPointF(0, -self.__tooltip.boundingRect().height() / 2)
|
||||
if self.__desc.direction == node_db.PortDescription.OUTPUT:
|
||||
ttpos += QtCore.QPointF(20, 0)
|
||||
else:
|
||||
ttpos -= QtCore.QPointF(20 + self.__tooltip.boundingRect().width(), 0)
|
||||
self.__tooltip.setPos(ttpos)
|
||||
|
||||
else:
|
||||
self.setOpacity(0.7)
|
||||
self.__tooltip.setVisible(False)
|
||||
|
||||
if self.__highlighted:
|
||||
pen = QtGui.QPen()
|
||||
pen.setColor(Qt.red)
|
||||
pen.setWidth(2)
|
||||
self.setPen(pen)
|
||||
self.setBrush(color)
|
||||
rect = QtCore.QRectF(-15, -12, 30, 24)
|
||||
|
||||
else:
|
||||
pen = QtGui.QPen()
|
||||
pen.setColor(QtGui.QColor(80, 80, 200))
|
||||
pen.setWidth(1)
|
||||
self.setPen(pen)
|
||||
self.setBrush(color)
|
||||
rect = QtCore.QRectF(-10, -8, 20, 16)
|
||||
|
||||
path = QtGui.QPainterPath()
|
||||
if self.__desc.direction == node_db.PortDescription.INPUT:
|
||||
path.moveTo(0, rect.top())
|
||||
path.arcTo(rect, 90, 180)
|
||||
|
||||
else:
|
||||
path.moveTo(0, rect.top())
|
||||
path.arcTo(rect, 90, -180)
|
||||
|
||||
self.setPath(path)
|
||||
|
||||
|
||||
class Node(ui_base.ProjectMixin, QtWidgets.QGraphicsItem):
|
||||
__next_zvalue = 2.0
|
||||
|
||||
def __init__(
|
||||
self, *,
|
||||
node: music.BasePipelineGraphNode,
|
||||
icon: Optional[QtSvg.QSvgRenderer] = None,
|
||||
**kwargs: Any
|
||||
) -> None:
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.setZValue(1.0)
|
||||
self.setAcceptHoverEvents(True)
|
||||
self.setAcceptedMouseButtons(Qt.LeftButton)
|
||||
|
||||
self.props = NodeProps()
|
||||
|
||||
self.__node = node
|
||||
|
||||
self.__plugin_ui = None # type: Optional[plugin_ui.PluginUI]
|
||||
|
||||
self.__box = Box(self)
|
||||
|
||||
if icon is not None:
|
||||
self.__icon = NodeIcon(icon, self)
|
||||
else:
|
||||
self.__icon = None
|
||||
|
||||
self.__ports = {} # type: Dict[str, Port]
|
||||
self.__in_ports = [] # type: List[node_db.PortDescription]
|
||||
self.__out_ports = [] # type: List[node_db.PortDescription]
|
||||
|
||||
for port_desc in self.__node.description.ports:
|
||||
if (port_desc.direction == node_db.PortDescription.INPUT
|
||||
and port_desc.type == node_db.PortDescription.KRATE_CONTROL):
|
||||
continue
|
||||
|
||||
port = Port(port_desc, self)
|
||||
self.__ports[port_desc.name] = port
|
||||
|
||||
if port_desc.direction == node_db.PortDescription.INPUT:
|
||||
self.__in_ports.append(port_desc)
|
||||
else:
|
||||
self.__out_ports.append(port_desc)
|
||||
|
||||
self.__title = Title(self.__node.name, self)
|
||||
|
||||
self.__title_edit = QtWidgets.QLineEdit()
|
||||
self.__title_edit.editingFinished.connect(self.__renameNodeFinished)
|
||||
|
||||
self.__title_edit_proxy = QtWidgets.QGraphicsProxyWidget(self)
|
||||
self.__title_edit_proxy.setWidget(self.__title_edit)
|
||||
|
||||
self.__body = self.createBodyWidget()
|
||||
self.__body.setAutoFillBackground(False)
|
||||
self.__body.setAttribute(Qt.WA_NoSystemBackground, True)
|
||||
|
||||
self.__body_proxy = QtWidgets.QGraphicsProxyWidget(self)
|
||||
self.__body_proxy.setWidget(self.__body)
|
||||
|
||||
self.__transform = QtGui.QTransform()
|
||||
self.__canvas_rect = self.__transform.mapRect(self.contentRect())
|
||||
|
||||
self.__selected = False
|
||||
self.__hovered = False
|
||||
self.__rename_node = False
|
||||
|
||||
self.__drag_rect = QtCore.QRectF()
|
||||
|
||||
self.__name_listener = self.__node.name_changed.add(self.__nameChanged)
|
||||
self.__graph_pos_listener = self.__node.graph_pos_changed.add(self.__graphRectChanged)
|
||||
self.__graph_size_listener = self.__node.graph_size_changed.add(self.__graphRectChanged)
|
||||
self.__graph_color_listener = self.__node.graph_color_changed.add(
|
||||
lambda *_: self.__updateState())
|
||||
|
||||
self.__updateState()
|
||||
|
||||
def __str__(self) -> str:
|
||||
return '<node name=%r> ' % self.__node.name
|
||||
|
||||
def __nameChanged(self, *args: Any) -> None:
|
||||
self.__title.setText(self.__node.name)
|
||||
|
||||
def __graphRectChanged(self, *args: Any) -> None:
|
||||
self.__canvas_rect = self.__transform.mapRect(self.contentRect())
|
||||
self.__layout()
|
||||
self.props.contentRectChanged.emit(self.contentRect())
|
||||
self.props.canvasRectChanged.emit(self.canvasRect())
|
||||
|
||||
def createBodyWidget(self) -> QtWidgets.QWidget:
|
||||
return node_widget.NodeWidget(node=self.__node, context=self.context)
|
||||
|
||||
def setup(self) -> None:
|
||||
for port in self.__ports.values():
|
||||
port.setup()
|
||||
|
||||
def cleanup(self) -> None:
|
||||
self.__graph_pos_listener.remove()
|
||||
for port in self.__ports.values():
|
||||
port.cleanup()
|
||||
self.__ports.clear()
|
||||
if self.__plugin_ui is not None:
|
||||
self.__plugin_ui.cleanup()
|
||||
|
||||
def node(self) -> music.BasePipelineGraphNode:
|
||||
return self.__node
|
||||
|
||||
def id(self) -> int:
|
||||
return self.__node.id
|
||||
|
||||
def name(self) -> str:
|
||||
return self.__node.name
|
||||
|
||||
def graph_pos(self) -> model.Pos2F:
|
||||
return self.__node.graph_pos
|
||||
|
||||
def graph_size(self) -> model.SizeF:
|
||||
return self.__node.graph_size
|
||||
|
||||
def upstream_nodes(self) -> List[model.BasePipelineGraphNode]:
|
||||
return self.__node.upstream_nodes()
|
||||
|
||||
def selected(self) -> bool:
|
||||
return self.__selected
|
||||
|
||||
def setSelected(self, selected: bool) -> None:
|
||||
self.__selected = selected
|
||||
self.__updateState()
|
||||
|
||||
def port(self, port_name: str) -> Port:
|
||||
return self.__ports[port_name]
|
||||
|
||||
def portHandleScenePos(self, port_name: str) -> QtCore.QPointF:
|
||||
return self.__ports[port_name].handleScenePos()
|
||||
|
||||
def contentTopLeft(self) -> QtCore.QPointF:
|
||||
return QtCore.QPointF(self.__node.graph_pos.x, self.__node.graph_pos.y)
|
||||
|
||||
def contentSize(self) -> QtCore.QSizeF:
|
||||
return QtCore.QSizeF(self.__node.graph_size.width, self.__node.graph_size.height)
|
||||
|
||||
def contentRect(self) -> QtCore.QRectF:
|
||||
return QtCore.QRectF(self.contentTopLeft(), self.contentSize())
|
||||
|
||||
def canvasTopLeft(self) -> QtCore.QPointF:
|
||||
return self.__canvas_rect.topLeft()
|
||||
|
||||
def setCanvasTopLeft(self, pos: QtCore.QPointF) -> None:
|
||||
self.__canvas_rect.moveTopLeft(pos)
|
||||
self.__layout()
|
||||
self.props.canvasRectChanged.emit(self.__canvas_rect)
|
||||
|
||||
def setCanvasRect(self, rect: QtCore.QRectF) -> None:
|
||||
self.__canvas_rect = rect
|
||||
self.__layout()
|
||||
self.props.canvasRectChanged.emit(self.__canvas_rect)
|
||||
|
||||
def canvasRect(self) -> QtCore.QRectF:
|
||||
return self.__canvas_rect
|
||||
|
||||
def setCanvasTransform(self, transform: QtGui.QTransform) -> None:
|
||||
self.__transform = transform
|
||||
self.__canvas_rect = self.__transform.mapRect(self.contentRect())
|
||||
self.__layout()
|
||||
self.props.canvasRectChanged.emit(self.__canvas_rect)
|
||||
|
||||
def resizeSide(self, pos: QtCore.QPointF) -> Optional[str]:
|
||||
t = self.__canvas_rect.top()
|
||||
b = self.__canvas_rect.bottom()
|
||||
l = self.__canvas_rect.left()
|
||||
r = self.__canvas_rect.right()
|
||||
w = self.__canvas_rect.width()
|
||||
h = self.__canvas_rect.height()
|
||||
resize_rects = {
|
||||
'top': QtCore.QRectF(l + 4, t, w - 8, 4),
|
||||
'bottom': QtCore.QRectF(l + 10, b - 10, w - 20, 10),
|
||||
'left': QtCore.QRectF(l, t + 4, 4, h - 14),
|
||||
'right': QtCore.QRectF(r - 4, t + 4, 4, h - 14),
|
||||
'topleft': QtCore.QRectF(l, t, 4, 4),
|
||||
'topright': QtCore.QRectF(r - 4, t, 4, 4),
|
||||
'bottomleft': QtCore.QRectF(l, b - 10, 10, 10),
|
||||
'bottomright': QtCore.QRectF(r - 10, b - 10, 10, 10),
|
||||
}
|
||||
|
||||
for side, rect in resize_rects.items():
|
||||
if rect.contains(pos):
|
||||
return side
|
||||
|
||||
return None
|
||||
|
||||
def dragRect(self) -> QtCore.QRectF:
|
||||
return self.__drag_rect
|
||||
|
||||
def boundingRect(self) -> QtCore.QRectF:
|
||||
return self.__box.boundingRect()
|
||||
|
||||
def __updateState(self) -> None:
|
||||
if self.__selected or self.__hovered:
|
||||
opacity = 1.0
|
||||
else:
|
||||
opacity = 0.7
|
||||
|
||||
self.__box.setOpacity(opacity)
|
||||
for port in self.__ports.values():
|
||||
if not port.highlighted():
|
||||
port.setOpacity(opacity)
|
||||
|
||||
if self.__selected:
|
||||
pen = QtGui.QPen()
|
||||
pen.setColor(QtGui.QColor(80, 80, 200))
|
||||
pen.setWidth(2)
|
||||
self.__box.setPen(pen)
|
||||
self.__box.setBrush(QtGui.QColor(150, 150, 255))
|
||||
else:
|
||||
pen = QtGui.QPen()
|
||||
pen.setColor(Qt.black)
|
||||
pen.setWidth(2)
|
||||
self.__box.setPen(pen)
|
||||
self.__box.setBrush(QtGui.QColor.fromRgbF(
|
||||
self.__node.graph_color.r,
|
||||
self.__node.graph_color.g,
|
||||
self.__node.graph_color.b,
|
||||
self.__node.graph_color.a))
|
||||
|
||||
def __layout(self) -> None:
|
||||
self.setPos(self.__canvas_rect.topLeft())
|
||||
|
||||
w, h = self.__canvas_rect.width(), self.__canvas_rect.height()
|
||||
|
||||
path = QtGui.QPainterPath()
|
||||
path.addRoundedRect(0, 0, w, h, 5, 5)
|
||||
self.__box.setPath(path)
|
||||
|
||||
show_ports = (0.5 * h > 10 * max(len(self.__in_ports), len(self.__out_ports)))
|
||||
for idx, desc in enumerate(self.__in_ports):
|
||||
port = self.__ports[desc.name]
|
||||
if len(self.__in_ports) > 1:
|
||||
y = h * (0.5 * idx / (len(self.__in_ports) - 1) + 0.25)
|
||||
else:
|
||||
y = h * 0.5
|
||||
port.setPos(0, y)
|
||||
port.setVisible(show_ports)
|
||||
|
||||
for idx, desc in enumerate(self.__out_ports):
|
||||
port = self.__ports[desc.name]
|
||||
if len(self.__out_ports) > 1:
|
||||
y = h * (0.5 * idx / (len(self.__out_ports) - 1) + 0.25)
|
||||
else:
|
||||
y = h * 0.5
|
||||
port.setPos(w, y)
|
||||
port.setVisible(show_ports)
|
||||
|
||||
if self.__rename_node:
|
||||
title_h = self.__title_edit_proxy.minimumHeight() + 4
|
||||
|
||||
self.__title_edit_proxy.setVisible(True)
|
||||
self.__title_edit_proxy.setPos(4, 4)
|
||||
self.__title_edit_proxy.resize(w - 8, self.__title_edit_proxy.minimumHeight())
|
||||
|
||||
else:
|
||||
title_h = 24
|
||||
|
||||
self.__title_edit_proxy.setVisible(False)
|
||||
|
||||
if h > 20 and not self.__rename_node:
|
||||
self.__title.setVisible(True)
|
||||
self.__title.setPos(8, 0)
|
||||
self.__title.setWidth(w - 16)
|
||||
else:
|
||||
self.__title.setVisible(False)
|
||||
|
||||
if self.__icon is not None:
|
||||
if self.__title.isVisible():
|
||||
icon_y = 24
|
||||
else:
|
||||
icon_y = 0
|
||||
self.__icon.setRect(QtCore.QRectF(3, icon_y, w - 6, h - icon_y - 6))
|
||||
|
||||
bsize = self.__body_proxy.minimumSize()
|
||||
if h > bsize.height() + (title_h + 4) and w > bsize.width() + 8:
|
||||
self.__body_proxy.setVisible(True)
|
||||
self.__body_proxy.setPos(4, title_h)
|
||||
self.__body_proxy.resize(w - 8, h - (title_h + 4))
|
||||
|
||||
else:
|
||||
self.__body_proxy.setVisible(False)
|
||||
|
||||
if self.__title_edit_proxy.isVisible():
|
||||
self.__drag_rect = QtCore.QRectF(0, 0, 0, 0)
|
||||
elif self.__body_proxy.isVisible():
|
||||
self.__drag_rect = QtCore.QRectF(0, 0, w, title_h)
|
||||
else:
|
||||
self.__drag_rect = QtCore.QRectF(0, 0, w, h)
|
||||
|
||||
def paint(
|
||||
self, painter: QtGui.QPainter, option: QtWidgets.QStyleOptionGraphicsItem,
|
||||
widget: Optional[QtWidgets.QWidget] = None) -> None:
|
||||
pass
|
||||
|
||||
def mousePressEvent(self, event: QtWidgets.QGraphicsSceneMouseEvent) -> None:
|
||||
self.setZValue(Node.__next_zvalue)
|
||||
Node.__next_zvalue += 1
|
||||
event.ignore()
|
||||
super().mousePressEvent(event)
|
||||
|
||||
def hoverEnterEvent(self, event: QtWidgets.QGraphicsSceneHoverEvent) -> None:
|
||||
self.__hovered = True
|
||||
self.__updateState()
|
||||
return super().hoverEnterEvent(event)
|
||||
|
||||
def hoverLeaveEvent(self, event: QtWidgets.QGraphicsSceneHoverEvent) -> None:
|
||||
self.__hovered = False
|
||||
self.__updateState()
|
||||
return super().hoverLeaveEvent(event)
|
||||
|
||||
def buildContextMenu(self, menu: QtWidgets.QMenu) -> None:
|
||||
if self.__node.removable:
|
||||
remove = menu.addAction("Remove")
|
||||
remove.triggered.connect(self.onRemove)
|
||||
|
||||
if self.__node.description.has_ui:
|
||||
show_ui = menu.addAction("Show UI")
|
||||
show_ui.triggered.connect(self.onShowUI)
|
||||
|
||||
color_menu = menu.addMenu("Set color")
|
||||
color_action = SelectColorAction(color_menu)
|
||||
color_action.colorSelected.connect(self.onSetColor)
|
||||
color_menu.addAction(color_action)
|
||||
|
||||
def onRemove(self) -> None:
|
||||
self.send_command_async(music.Command(
|
||||
target=self.__node.parent.id,
|
||||
remove_pipeline_graph_node=music.RemovePipelineGraphNode(
|
||||
node_id=self.__node.id)))
|
||||
|
||||
def onShowUI(self) -> None:
|
||||
if self.__plugin_ui is not None:
|
||||
self.__plugin_ui.show()
|
||||
self.__plugin_ui.raise_()
|
||||
self.__plugin_ui.activateWindow()
|
||||
else:
|
||||
self.__plugin_ui = plugin_ui.PluginUI(node=self.__node, context=self.context)
|
||||
|
||||
def onSetColor(self, color: model.Color) -> None:
|
||||
if color != self.__node.graph_color:
|
||||
self.send_command_async(music.Command(
|
||||
target=self.__node.id,
|
||||
change_pipeline_graph_node=music.ChangePipelineGraphNode(
|
||||
graph_color=color.to_proto())))
|
||||
|
||||
def renameNode(self) -> None:
|
||||
self.__rename_node = True
|
||||
self.__title_edit.setText(self.__node.name)
|
||||
self.__layout()
|
||||
|
||||
def __renameNodeFinished(self) -> None:
|
||||
new_name = self.__title_edit.text()
|
||||
if new_name != self.__node.name:
|
||||
self.__title.setText(self.__node.name)
|
||||
self.send_command_async(music.Command(
|
||||
target=self.__node.id,
|
||||
change_pipeline_graph_node=music.ChangePipelineGraphNode(
|
||||
name=new_name)))
|
||||
|
||||
self.__rename_node = False
|
||||
self.__layout()
|
||||
|
||||
|
||||
class Connection(ui_base.ProjectMixin, QtWidgets.QGraphicsPathItem):
|
||||
def __init__(
|
||||
self, *,
|
||||
connection: music.PipelineGraphConnection,
|
||||
src_node: Node,
|
||||
dest_node: Node,
|
||||
**kwargs: Any) -> None:
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.__connection = connection
|
||||
self.__src_node = src_node
|
||||
self.__dest_node = dest_node
|
||||
|
||||
self.__highlighted = False
|
||||
|
||||
self.__src_node.props.canvasRectChanged.connect(lambda _: self.__update())
|
||||
self.__dest_node.props.canvasRectChanged.connect(lambda _: self.__update())
|
||||
|
||||
self.__update()
|
||||
|
||||
def cleanup(self) -> None:
|
||||
pass
|
||||
|
||||
def id(self) -> int:
|
||||
return self.__connection.id
|
||||
|
||||
def src_node(self) -> Node:
|
||||
return self.__src_node
|
||||
|
||||
def src_port(self) -> Port:
|
||||
return self.__src_node.port(self.__connection.source_port)
|
||||
|
||||
def dest_node(self) -> Node:
|
||||
return self.__dest_node
|
||||
|
||||
def dest_port(self) -> Port:
|
||||
return self.__dest_node.port(self.__connection.dest_port)
|
||||
|
||||
def setHighlighted(self, highlighted: bool) -> None:
|
||||
self.__highlighted = highlighted
|
||||
self.__update()
|
||||
|
||||
def __update(self) -> None:
|
||||
color = port_colors[self.src_port().type()]
|
||||
|
||||
if self.__highlighted:
|
||||
pen = QtGui.QPen()
|
||||
pen.setColor(color)
|
||||
pen.setWidth(4)
|
||||
self.setPen(pen)
|
||||
else:
|
||||
pen = QtGui.QPen()
|
||||
pen.setColor(color)
|
||||
pen.setWidth(2)
|
||||
self.setPen(pen)
|
||||
|
||||
pos1 = self.__src_node.portHandleScenePos(self.__connection.source_port)
|
||||
pos2 = self.__dest_node.portHandleScenePos(self.__connection.dest_port)
|
||||
cpos = QtCore.QPointF(min(100, abs(pos2.x() - pos1.x()) / 2), 0)
|
||||
|
||||
path = QtGui.QPainterPath()
|
||||
path.moveTo(pos1)
|
||||
path.cubicTo(pos1 + cpos, pos2 - cpos, pos2)
|
||||
self.setPath(path)
|
|
@ -0,0 +1,62 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
# @begin:license
|
||||
#
|
||||
# Copyright (c) 2015-2018, 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 QtCore
|
||||
|
||||
from noisidev import uitest
|
||||
from noisicaa import model
|
||||
from noisicaa import music
|
||||
from . import base_node
|
||||
|
||||
|
||||
class NoteTest(uitest.ProjectMixin, uitest.UITestCase):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.node = None
|
||||
self.nitem = None
|
||||
|
||||
async def setup_testcase(self):
|
||||
node_id = await self.project_client.send_command(music.Command(
|
||||
target=self.project.id,
|
||||
add_pipeline_graph_node=music.AddPipelineGraphNode(
|
||||
uri='ladspa://passthru.so/passthru',
|
||||
graph_pos=model.Pos2F(200, 100).to_proto(),
|
||||
graph_size=model.SizeF(140, 65).to_proto())))
|
||||
self.node = self.project_client.get_object(node_id)
|
||||
|
||||
self.nitem = base_node.Node(node=self.node, context=self.context)
|
||||
|
||||
async def cleanup_testcase(self):
|
||||
if self.nitem is not None:
|
||||
self.nitem.cleanup()
|
||||
|
||||
def _scaledSize(self, zoom):
|
||||
return QtCore.QSize(
|
||||
int(zoom * self.nitem.sceneSize().width()),
|
||||
int(zoom * self.nitem.sceneSize().height()))
|
||||
|
||||
async def test_attrs(self):
|
||||
self.assertEqual(self.nitem.id(), self.node.id)
|
||||
self.assertEqual(self.nitem.contentTopLeft(), QtCore.QPointF(200, 100))
|
||||
self.assertEqual(self.nitem.contentSize(), QtCore.QSizeF(140, 65))
|
||||
self.assertEqual(self.nitem.contentRect(), QtCore.QRectF(200, 100, 140, 65))
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,97 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
# @begin:license
|
||||
#
|
||||
# Copyright (c) 2015-2018, 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 unittest import mock
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5 import QtCore
|
||||
from PyQt5 import QtGui
|
||||
|
||||
from noisidev import uitest
|
||||
from . import canvas
|
||||
|
||||
|
||||
class SceneTest(uitest.ProjectMixin, uitest.UITestCase):
|
||||
def setup_testcase(self):
|
||||
self.scene = canvas.Scene(context=self.context)
|
||||
|
||||
def test_setZoom(self):
|
||||
self.scene.setZoom(0.5)
|
||||
self.assertAlmostEqual(self.scene.zoom(), 0.5)
|
||||
|
||||
|
||||
class CanvasTest(uitest.ProjectMixin, uitest.UITestCase):
|
||||
def setup_testcase(self):
|
||||
self.canvas = canvas.Canvas(context=self.context)
|
||||
self.scene = self.canvas.scene()
|
||||
|
||||
def test_wheelEvent_zoom_in(self):
|
||||
sig = mock.Mock()
|
||||
self.canvas.zoomStarted.connect(sig)
|
||||
|
||||
evt = QtGui.QWheelEvent(
|
||||
QtCore.QPointF(200, 100),
|
||||
QtCore.QPointF(500, 300),
|
||||
QtCore.QPoint(0, 10),
|
||||
QtCore.QPoint(0, 120),
|
||||
10, Qt.Vertical,
|
||||
Qt.NoButton,
|
||||
Qt.NoModifier)
|
||||
self.canvas.wheelEvent(evt)
|
||||
self.assertTrue(evt.isAccepted())
|
||||
|
||||
sig.assert_called()
|
||||
zoom, = sig.call_args[0]
|
||||
self.assertEqual(zoom.direction, 1)
|
||||
self.assertEqual(zoom.center, QtCore.QPointF(200, 100))
|
||||
|
||||
def test_wheelEvent_zoom_out(self):
|
||||
sig = mock.Mock()
|
||||
self.canvas.zoomStarted.connect(sig)
|
||||
|
||||
evt = QtGui.QWheelEvent(
|
||||
QtCore.QPointF(200, 100),
|
||||
QtCore.QPointF(500, 300),
|
||||
QtCore.QPoint(0, -10),
|
||||
QtCore.QPoint(0, -120),
|
||||
10, Qt.Vertical,
|
||||
Qt.NoButton,
|
||||
Qt.NoModifier)
|
||||
self.canvas.wheelEvent(evt)
|
||||
self.assertTrue(evt.isAccepted())
|
||||
|
||||
sig.assert_called()
|
||||
zoom, = sig.call_args[0]
|
||||
self.assertEqual(zoom.direction, -1)
|
||||
self.assertEqual(zoom.center, QtCore.QPointF(200, 100))
|
||||
|
||||
async def test_mousePressEvent(self):
|
||||
self.canvas.resize(400, 300)
|
||||
|
||||
evt = QtGui.QMouseEvent(
|
||||
QtCore.QEvent.MouseButtonPress,
|
||||
QtCore.QPointF(20, 10),
|
||||
Qt.LeftButton,
|
||||
Qt.LeftButton,
|
||||
Qt.NoModifier)
|
||||
self.canvas.mousePressEvent(evt)
|
||||
self.assertTrue(evt.isAccepted())
|
|
@ -0,0 +1,329 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
# @begin:license
|
||||
#
|
||||
# Copyright (c) 2015-2018, 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 functools
|
||||
import logging
|
||||
from typing import cast, Any, Optional, Dict, List, Set # pylint: disable=unused-import
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5 import QtCore
|
||||
from PyQt5 import QtGui
|
||||
from PyQt5 import QtWidgets
|
||||
|
||||
from noisicaa import core
|
||||
from noisicaa import model
|
||||
from noisicaa import music
|
||||
from noisicaa import node_db
|
||||
from noisicaa.ui import ui_base
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ControlValuesConnector(object):
|
||||
def __init__(self, node: music.BasePipelineGraphNode) -> None:
|
||||
self.__node = node
|
||||
|
||||
self.__control_values = {} # type: Dict[str, model.ControlValue]
|
||||
for port in self.__node.description.ports:
|
||||
if (port.direction == node_db.PortDescription.INPUT
|
||||
and port.type == node_db.PortDescription.KRATE_CONTROL):
|
||||
self.__control_values[port.name] = model.ControlValue(
|
||||
value=port.float_value.default, generation=1)
|
||||
|
||||
self.__control_value_listeners = [] # type: List[core.Listener]
|
||||
for control_value in self.__node.control_values:
|
||||
self.__control_values[control_value.name] = control_value.value
|
||||
|
||||
self.__control_value_listeners.append(
|
||||
control_value.value_changed.add(
|
||||
functools.partial(self.onControlValueChanged, control_value.name)))
|
||||
|
||||
self.__control_values_listener = self.__node.control_values_changed.add(
|
||||
self.onControlValuesChanged)
|
||||
|
||||
self.control_value_changed = core.CallbackMap[str, model.PropertyValueChange]()
|
||||
|
||||
def value(self, name: str) -> float:
|
||||
return self.__control_values[name].value
|
||||
|
||||
def generation(self, name: str) -> int:
|
||||
return self.__control_values[name].generation
|
||||
|
||||
def cleanup(self) -> None:
|
||||
for listener in self.__control_value_listeners:
|
||||
listener.remove()
|
||||
self.__control_value_listeners.clear()
|
||||
|
||||
if self.__control_values_listener is not None:
|
||||
self.__control_values_listener.remove()
|
||||
self.__control_values_listener = None
|
||||
|
||||
def onControlValuesChanged(
|
||||
self, change: model.PropertyListChange[music.PipelineGraphControlValue]) -> None:
|
||||
if isinstance(change, model.PropertyListInsert):
|
||||
control_value = change.new_value
|
||||
|
||||
self.control_value_changed.call(
|
||||
control_value.name,
|
||||
model.PropertyValueChange(
|
||||
self.__node, control_value.name,
|
||||
self.__control_values[control_value.name], control_value.value))
|
||||
self.__control_values[control_value.name] = control_value.value
|
||||
|
||||
self.__control_value_listeners.insert(
|
||||
change.index,
|
||||
control_value.value_changed.add(
|
||||
functools.partial(self.onControlValueChanged, control_value.name)))
|
||||
|
||||
elif isinstance(change, model.PropertyListDelete):
|
||||
control_value = change.old_value
|
||||
|
||||
for port in self.__node.description.ports:
|
||||
if port.name == control_value.name:
|
||||
default_value = model.ControlValue(
|
||||
value=port.float_value.default, generation=1)
|
||||
self.control_value_changed.call(
|
||||
control_value.name,
|
||||
model.PropertyValueChange(
|
||||
self.__node, control_value.name,
|
||||
self.__control_values[control_value.name], default_value))
|
||||
self.__control_values[control_value.name] = default_value
|
||||
break
|
||||
|
||||
listener = self.__control_value_listeners.pop(change.index)
|
||||
listener.remove()
|
||||
|
||||
else:
|
||||
raise TypeError(type(change))
|
||||
|
||||
def onControlValueChanged(
|
||||
self, control_value_name: str, change: model.PropertyValueChange[model.ControlValue]
|
||||
) -> None:
|
||||
self.control_value_changed.call(
|
||||
control_value_name,
|
||||
model.PropertyValueChange(
|
||||
self.__node, control_value_name,
|
||||
self.__control_values[control_value_name], change.new_value))
|
||||
self.__control_values[control_value_name] = change.new_value
|
||||
|
||||
|
||||
class ControlValueWidget(ui_base.ProjectMixin, QtCore.QObject):
|
||||
def __init__(
|
||||
self, *,
|
||||
node: music.BasePipelineGraphNode, port: node_db.PortDescription,
|
||||
connector: ControlValuesConnector, parent: Optional[QtWidgets.QWidget],
|
||||
**kwargs: Any) -> None:
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.__node = node
|
||||
self.__port = port
|
||||
self.__connector = connector
|
||||
self.__parent = parent
|
||||
|
||||
self.__listeners = [] # type: List[core.Listener]
|
||||
self.__generation = self.__connector.generation(self.__port.name)
|
||||
|
||||
self.__widget = QtWidgets.QLineEdit(self.__parent)
|
||||
self.__widget.setText(str(self.__connector.value(self.__port.name)))
|
||||
self.__widget.setValidator(QtGui.QDoubleValidator())
|
||||
self.__widget.editingFinished.connect(self.__onValueEdited)
|
||||
|
||||
self.__listeners.append(self.__connector.control_value_changed.add(
|
||||
self.__port.name, self.__onValueChanged))
|
||||
|
||||
def cleanup(self) -> None:
|
||||
for listener in self.__listeners:
|
||||
listener.remove()
|
||||
self.__listeners.clear()
|
||||
|
||||
def label(self) -> str:
|
||||
return self.__port.name
|
||||
|
||||
def widget(self) -> QtWidgets.QWidget:
|
||||
return self.__widget
|
||||
|
||||
def __onValueEdited(self) -> None:
|
||||
value, ok = self.__widget.locale().toDouble(self.__widget.text())
|
||||
if ok and value != self.__connector.value(self.__port.name):
|
||||
self.__generation += 1
|
||||
self.send_command_async(music.Command(
|
||||
target=self.__node.id,
|
||||
set_pipeline_graph_control_value=music.SetPipelineGraphControlValue(
|
||||
port_name=self.__port.name,
|
||||
float_value=value,
|
||||
generation=self.__generation)))
|
||||
|
||||
def __onValueChanged(
|
||||
self, change: model.PropertyValueChange[model.ControlValue]) -> None:
|
||||
if change.new_value.generation < self.__generation:
|
||||
return
|
||||
|
||||
self.__generation = change.new_value.generation
|
||||
self.__widget.setText(str(change.new_value.value))
|
||||
|
||||
|
||||
class NodeWidget(ui_base.ProjectMixin, QtWidgets.QWidget):
|
||||
def __init__(self, node: music.BasePipelineGraphNode, **kwargs: Any) -> None:
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.__node = node
|
||||
|
||||
self.__listeners = [] # type: List[core.Listener]
|
||||
self.__control_values = ControlValuesConnector(self.__node)
|
||||
self.__control_value_widgets = [] # type: List[ControlValueWidget]
|
||||
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
layout.setSpacing(0)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.addWidget(self.__createPropertiesForm(self))
|
||||
self.setLayout(layout)
|
||||
|
||||
def cleanup(self) -> None:
|
||||
for listener in self.__listeners:
|
||||
listener.remove()
|
||||
self.__listeners.clear()
|
||||
|
||||
for widget in self.__control_value_widgets:
|
||||
widget.cleanup()
|
||||
self.__control_value_widgets.clear()
|
||||
|
||||
def __createPropertiesForm(self, parent: QtWidgets.QWidget) -> QtWidgets.QWidget:
|
||||
form = QtWidgets.QWidget()
|
||||
form.setAutoFillBackground(False)
|
||||
form.setAttribute(Qt.WA_NoSystemBackground, True)
|
||||
|
||||
layout = QtWidgets.QFormLayout()
|
||||
layout.setVerticalSpacing(1)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
form.setLayout(layout)
|
||||
|
||||
for port in self.__node.description.ports:
|
||||
if (port.direction == node_db.PortDescription.INPUT
|
||||
and port.type == node_db.PortDescription.KRATE_CONTROL):
|
||||
widget = ControlValueWidget(
|
||||
node=self.__node,
|
||||
port=port,
|
||||
connector=self.__control_values,
|
||||
parent=form,
|
||||
context=self.context)
|
||||
self.__control_value_widgets.append(widget)
|
||||
layout.addRow(widget.label(), widget.widget())
|
||||
|
||||
scroll = QtWidgets.QScrollArea(parent)
|
||||
scroll.setWidgetResizable(True)
|
||||
scroll.setFrameShape(QtWidgets.QFrame.NoFrame)
|
||||
scroll.setWidget(form)
|
||||
return scroll
|
||||
|
||||
# def __createUITab(self, parent: Optional[QtWidgets.QWidget]) -> QtWidgets.QWidget:
|
||||
# node = self.__node
|
||||
# node_description = node.description
|
||||
|
||||
# if node_description.has_ui:
|
||||
# return PluginUI(parent=parent, node=node, context=self.context)
|
||||
|
||||
# else:
|
||||
# tab = QtWidgets.QWidget(parent)
|
||||
|
||||
# label = QtWidgets.QLabel(tab)
|
||||
# label.setText("This node has no native UI.")
|
||||
# label.setAlignment(Qt.AlignHCenter)
|
||||
|
||||
# layout = QtWidgets.QVBoxLayout()
|
||||
# layout.setContentsMargins(QtCore.QMargins(0, 0, 0, 0))
|
||||
# layout.addStretch(1)
|
||||
# layout.addWidget(label)
|
||||
# layout.addStretch(1)
|
||||
# tab.setLayout(layout)
|
||||
|
||||
# return tab
|
||||
|
||||
# def onPresetEditMetadata(self) -> None:
|
||||
# pass
|
||||
|
||||
# def onPresetLoad(self) -> None:
|
||||
# pass
|
||||
|
||||
# def onPresetRevert(self) -> None:
|
||||
# pass
|
||||
|
||||
# def onPresetSave(self) -> None:
|
||||
# self.send_command_async(
|
||||
# music.Command(
|
||||
# target=self.__node.id,
|
||||
# pipeline_graph_node_to_preset=music.PipelineGraphNodeToPreset()),
|
||||
# callback=self.onPresetSaveDone)
|
||||
|
||||
# def onPresetSaveDone(self, preset: bytes) -> None:
|
||||
# print(preset)
|
||||
|
||||
# def onPresetSaveAs(self) -> None:
|
||||
# pass
|
||||
|
||||
# def onPresetImport(self) -> None:
|
||||
# path, _ = QtWidgets.QFileDialog.getOpenFileName(
|
||||
# parent=self,
|
||||
# caption="Import preset",
|
||||
# #directory=self.ui_state.get(
|
||||
# #'instruments_add_dialog_path', ''),
|
||||
# filter="All Files (*);;noisicaä Presets (*.preset)",
|
||||
# initialFilter='noisicaä Presets (*.preset)',
|
||||
# )
|
||||
# if not path:
|
||||
# return
|
||||
|
||||
# self.call_async(self.onPresetImportAsync(path))
|
||||
|
||||
# async def onPresetImportAsync(self, path: str) -> None:
|
||||
# logger.info("Importing preset from %s...", path)
|
||||
|
||||
# with open(path, 'rb') as fp:
|
||||
# preset = fp.read()
|
||||
|
||||
# await self.project_client.send_command(music.Command(
|
||||
# target=self.__node.id,
|
||||
# pipeline_graph_node_from_preset=music.PipelineGraphNodeFromPreset(
|
||||
# preset=preset)))
|
||||
|
||||
# def onPresetExport(self) -> None:
|
||||
# path, _ = QtWidgets.QFileDialog.getSaveFileName(
|
||||
# parent=self,
|
||||
# caption="Export preset",
|
||||
# #directory=self.ui_state.get(
|
||||
# #'instruments_add_dialog_path', ''),
|
||||
# filter="All Files (*);;noisicaä Presets (*.preset)",
|
||||
# initialFilter='noisicaä Presets (*.preset)',
|
||||
# )
|
||||
# if not path:
|
||||
# return
|
||||
|
||||
# self.call_async(self.onPresetExportAsync(path))
|
||||
|
||||
# async def onPresetExportAsync(self, path: str) -> None:
|
||||
# logger.info("Exporting preset to %s...", path)
|
||||
|
||||
# preset = await self.project_client.send_command(music.Command(
|
||||
# target=self.__node.id,
|
||||
# pipeline_graph_node_to_preset=music.PipelineGraphNodeToPreset()))
|
||||
|
||||
# with open(path, 'wb') as fp:
|
||||
# fp.write(preset)
|
|
@ -0,0 +1,185 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
# @begin:license
|
||||
#
|
||||
# Copyright (c) 2015-2018, 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 asyncio
|
||||
import logging
|
||||
from typing import cast, Any, Optional, Dict, List, Set # pylint: disable=unused-import
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5 import QtGui
|
||||
from PyQt5 import QtWidgets
|
||||
|
||||
from noisicaa import music
|
||||
from noisicaa.ui import ui_base
|
||||
from noisicaa.ui import qprogressindicator
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PluginUI(ui_base.ProjectMixin, QtWidgets.QWidget):
|
||||
def __init__(self, *, node: music.BasePipelineGraphNode, **kwargs: Any) -> None:
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.setParent(self.editor_window)
|
||||
self.setWindowFlags(Qt.Tool)
|
||||
self.setAttribute(Qt.WA_DeleteOnClose, False)
|
||||
self.setWindowTitle(node.name)
|
||||
|
||||
self.__node = node
|
||||
|
||||
self.__lock = asyncio.Lock(loop=self.event_loop)
|
||||
self.__loading = False
|
||||
self.__loaded = False
|
||||
self.__show_task = None # type: asyncio.Task
|
||||
self.__closing = False
|
||||
self.__initial_size_set = False
|
||||
|
||||
self.__wid = None # type: int
|
||||
|
||||
self.__main_area = QtWidgets.QScrollArea()
|
||||
self.__main_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
|
||||
self.__main_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
|
||||
self.__main_area.setFrameShape(QtWidgets.QFrame.NoFrame)
|
||||
self.__main_area.setWidget(self.__createLoadingWidget())
|
||||
self.__main_area.setWidgetResizable(True)
|
||||
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
layout.setSpacing(0)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.addWidget(self.__main_area)
|
||||
self.setLayout(layout)
|
||||
|
||||
self.__show_task = self.event_loop.create_task(self.__deferShow())
|
||||
self.__loading = True
|
||||
self.call_async(self.__loadUI())
|
||||
|
||||
def cleanup(self) -> None:
|
||||
if self.__show_task is not None:
|
||||
self.__show_task.cancel()
|
||||
self.__show_task = None
|
||||
|
||||
if not self.__closing:
|
||||
self.__closing = True
|
||||
self.call_async(self.__cleanupAsync())
|
||||
|
||||
async def __cleanupAsync(self) -> None:
|
||||
async with self.__lock:
|
||||
if self.__wid is not None:
|
||||
await self.project_view.deletePluginUI('%016x' % self.__node.id)
|
||||
self.__wid = None
|
||||
|
||||
async def __deferShow(self) -> None:
|
||||
await asyncio.sleep(0.5, loop=self.event_loop)
|
||||
self.show()
|
||||
self.raise_()
|
||||
self.activateWindow()
|
||||
self.__show_task = None
|
||||
|
||||
async def __deferHide(self) -> None:
|
||||
async with self.__lock:
|
||||
# TODO: this should use self.__node.pipeline_node_id
|
||||
await self.project_view.deletePluginUI('%016x' % self.__node.id)
|
||||
|
||||
self.__main_area.setWidget(self.__createLoadingWidget())
|
||||
self.__main_area.setWidgetResizable(True)
|
||||
|
||||
self.__loaded = False
|
||||
|
||||
self.hide()
|
||||
|
||||
def closeEvent(self, event: QtGui.QCloseEvent) -> None:
|
||||
event.ignore()
|
||||
self.event_loop.create_task(self.__deferHide())
|
||||
|
||||
def showEvent(self, evt: QtGui.QShowEvent) -> None:
|
||||
if not self.__loading and not self.__loaded:
|
||||
self.__loading = True
|
||||
self.call_async(self.__loadUI())
|
||||
|
||||
super().showEvent(evt)
|
||||
|
||||
def hideEvent(self, evt: QtGui.QHideEvent) -> None:
|
||||
if self.__show_task is not None:
|
||||
self.__show_task.cancel()
|
||||
self.__show_task = None
|
||||
|
||||
super().hideEvent(evt)
|
||||
|
||||
def __createLoadingWidget(self) -> QtWidgets.QWidget:
|
||||
loading_spinner = qprogressindicator.QProgressIndicator(self)
|
||||
loading_spinner.setAnimationDelay(100)
|
||||
loading_spinner.startAnimation()
|
||||
|
||||
loading_text = QtWidgets.QLabel(self)
|
||||
loading_text.setText("Loading native UI...")
|
||||
|
||||
hlayout = QtWidgets.QHBoxLayout()
|
||||
hlayout.setContentsMargins(0, 0, 0, 0)
|
||||
hlayout.addStretch(1)
|
||||
hlayout.addWidget(loading_spinner)
|
||||
hlayout.addWidget(loading_text)
|
||||
hlayout.addStretch(1)
|
||||
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.addStretch(1)
|
||||
layout.addLayout(hlayout)
|
||||
layout.addStretch(1)
|
||||
|
||||
loading = QtWidgets.QWidget(self)
|
||||
loading.setLayout(layout)
|
||||
|
||||
return loading
|
||||
|
||||
async def __loadUI(self) -> None:
|
||||
async with self.__lock:
|
||||
# TODO: this should use self.__node.pipeline_node_id
|
||||
self.__wid, size = await self.project_view.createPluginUI('%016x' % self.__node.id)
|
||||
|
||||
proxy_win = QtGui.QWindow.fromWinId(self.__wid) # type: ignore
|
||||
proxy_widget = QtWidgets.QWidget.createWindowContainer(proxy_win, self)
|
||||
proxy_widget.setMinimumSize(*size)
|
||||
#proxy_widget.setMaximumSize(*size)
|
||||
|
||||
self.__main_area.setWidget(proxy_widget)
|
||||
self.__main_area.setWidgetResizable(False)
|
||||
|
||||
if not self.__initial_size_set:
|
||||
view_size = self.size()
|
||||
view_size.setWidth(max(view_size.width(), size[0]))
|
||||
view_size.setHeight(max(view_size.height(), size[1]))
|
||||
logger.info("Resizing to %s", view_size)
|
||||
self.__main_area.setMinimumSize(view_size)
|
||||
|
||||
self.adjustSize()
|
||||
|
||||
self.__initial_size_set = True
|
||||
|
||||
self.__loaded = True
|
||||
self.__loading = False
|
||||
|
||||
if self.__show_task is not None:
|
||||
self.__show_task.cancel()
|
||||
self.__show_task = None
|
||||
self.show()
|
||||
self.raise_()
|
||||
self.activateWindow()
|
|
@ -0,0 +1,103 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
# @begin:license
|
||||
#
|
||||
# Copyright (c) 2015-2018, 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, Optional, Dict, List, Set # pylint: disable=unused-import
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5 import QtWidgets
|
||||
|
||||
from noisicaa import core # pylint: disable=unused-import
|
||||
from noisicaa import model
|
||||
from noisicaa import music
|
||||
from noisicaa.ui import ui_base
|
||||
|
||||
from . import track_node
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ScoreTrackWidget(ui_base.ProjectMixin, QtWidgets.QScrollArea):
|
||||
def __init__(self, track: music.ScoreTrack, **kwargs: Any) -> None:
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.__track = track
|
||||
|
||||
self.__listeners = {} # type: Dict[str, core.Listener]
|
||||
|
||||
body = QtWidgets.QWidget(self)
|
||||
body.setAutoFillBackground(False)
|
||||
body.setAttribute(Qt.WA_NoSystemBackground, True)
|
||||
|
||||
self.__transpose_octaves = QtWidgets.QSpinBox(body)
|
||||
self.__transpose_octaves.setSuffix(' octaves')
|
||||
self.__transpose_octaves.setRange(-4, 4)
|
||||
self.__transpose_octaves.setSingleStep(1)
|
||||
self.__transpose_octaves.valueChanged.connect(self.onTransposeOctavesEdited)
|
||||
self.__transpose_octaves.setVisible(True)
|
||||
self.__transpose_octaves.setValue(self.__track.transpose_octaves)
|
||||
self.__listeners['track:transpose_octaves'] = (
|
||||
self.__track.transpose_octaves_changed.add(self.onTransposeOctavesChanged))
|
||||
|
||||
layout = QtWidgets.QFormLayout()
|
||||
layout.setVerticalSpacing(1)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.addRow("Transpose:", self.__transpose_octaves)
|
||||
body.setLayout(layout)
|
||||
|
||||
self.setWidgetResizable(True)
|
||||
self.setFrameShape(QtWidgets.QFrame.NoFrame)
|
||||
self.setWidget(body)
|
||||
|
||||
def cleanup(self) -> None:
|
||||
for listener in self.__listeners.values():
|
||||
listener.remove()
|
||||
self.__listeners.clear()
|
||||
|
||||
def onTransposeOctavesChanged(self, change: model.PropertyValueChange[int]) -> None:
|
||||
self.__transpose_octaves.setValue(change.new_value)
|
||||
|
||||
def onTransposeOctavesEdited(self, transpose_octaves: int) -> None:
|
||||
if transpose_octaves != self.__track.transpose_octaves:
|
||||
self.send_command_async(music.Command(
|
||||
target=self.__track.id,
|
||||
update_track_properties=music.UpdateTrackProperties(
|
||||
transpose_octaves=transpose_octaves)))
|
||||
|
||||
|
||||
class ScoreTrackNode(track_node.TrackNode):
|
||||
def __init__(self, *, node: music.BasePipelineGraphNode, **kwargs: Any) -> None:
|
||||
assert isinstance(node, music.ScoreTrack)
|
||||
self.__widget = None # type: ScoreTrackWidget
|
||||
self.__track = node # type: music.ScoreTrack
|
||||
|
||||
super().__init__(node=node, **kwargs)
|
||||
|
||||
def cleanup(self) -> None:
|
||||
if self.__widget is not None:
|
||||
self.__widget.cleanup()
|
||||
super().cleanup()
|
||||
|
||||
def createBodyWidget(self) -> QtWidgets.QWidget:
|
||||
assert self.__widget is None
|
||||
self.__widget = ScoreTrackWidget(track=self.__track, context=self.context)
|
||||
return self.__widget
|
|
@ -0,0 +1,107 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
# @begin:license
|
||||
#
|
||||
# Copyright (c) 2015-2018, 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 functools
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from PyQt5 import QtCore
|
||||
from PyQt5 import QtGui
|
||||
from PyQt5 import QtWidgets
|
||||
|
||||
from noisicaa.ui import ui_base
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Tool(enum.Enum):
|
||||
SELECT = 'select'
|
||||
INSERT = 'insert'
|
||||
|
||||
|
||||
class Toolbox(ui_base.ProjectMixin, QtWidgets.QWidget):
|
||||
toolChanged = QtCore.pyqtSignal(Tool)
|
||||
resetViewTriggered = QtCore.pyqtSignal()
|
||||
|
||||
def __init__(self, **kwargs: Any) -> None:
|
||||
super().__init__(**kwargs)
|
||||
|
||||
icon_size = QtCore.QSize(32, 32)
|
||||
|
||||
self.__select_tool_action = QtWidgets.QAction("Selection tool", self)
|
||||
self.__select_tool_action.setIcon(QtGui.QIcon.fromTheme('edit-select'))
|
||||
self.__select_tool_action.setCheckable(True)
|
||||
self.__select_tool_action.triggered.connect(functools.partial(self.setTool, Tool.SELECT))
|
||||
|
||||
self.__select_tool_button = QtWidgets.QToolButton(self)
|
||||
self.__select_tool_button.setAutoRaise(True)
|
||||
self.__select_tool_button.setIconSize(icon_size)
|
||||
self.__select_tool_button.setDefaultAction(self.__select_tool_action)
|
||||
|
||||
self.__insert_tool_action = QtWidgets.QAction("Insert tool", self)
|
||||
self.__insert_tool_action.setIcon(QtGui.QIcon.fromTheme('list-add'))
|
||||
self.__insert_tool_action.setCheckable(True)
|
||||
self.__insert_tool_action.triggered.connect(functools.partial(self.setTool, Tool.INSERT))
|
||||
|
||||
self.__insert_tool_button = QtWidgets.QToolButton(self)
|
||||
self.__insert_tool_button.setAutoRaise(True)
|
||||
self.__insert_tool_button.setIconSize(icon_size)
|
||||
self.__insert_tool_button.setDefaultAction(self.__insert_tool_action)
|
||||
|
||||
self.__reset_view_action = QtWidgets.QAction("Reset view", self)
|
||||
self.__reset_view_action.setIcon(QtGui.QIcon.fromTheme('zoom-original'))
|
||||
self.__reset_view_action.triggered.connect(self.resetViewTriggered.emit)
|
||||
|
||||
self.__reset_view_button = QtWidgets.QToolButton(self)
|
||||
self.__reset_view_button.setAutoRaise(True)
|
||||
self.__reset_view_button.setIconSize(icon_size)
|
||||
self.__reset_view_button.setDefaultAction(self.__reset_view_action)
|
||||
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
layout.setContentsMargins(2, 2, 2, 2)
|
||||
layout.setSpacing(2)
|
||||
layout.addWidget(self.__select_tool_button)
|
||||
layout.addWidget(self.__insert_tool_button)
|
||||
layout.addSpacing(8)
|
||||
layout.addWidget(self.__reset_view_button)
|
||||
layout.addStretch(1)
|
||||
self.setLayout(layout)
|
||||
|
||||
self.__tool_actions = {
|
||||
Tool.SELECT: self.__select_tool_action,
|
||||
Tool.INSERT: self.__insert_tool_action,
|
||||
}
|
||||
|
||||
self.__current_tool = Tool.SELECT
|
||||
|
||||
for tool, action in self.__tool_actions.items():
|
||||
action.setChecked(tool == self.__current_tool)
|
||||
|
||||
def setTool(self, tool: Tool) -> None:
|
||||
if tool == self.__current_tool:
|
||||
return
|
||||
|
||||
self.__current_tool = tool
|
||||
for tool, action in self.__tool_actions.items():
|
||||
action.setChecked(tool == self.__current_tool)
|
||||
self.toolChanged.emit(self.__current_tool)
|
|
@ -0,0 +1,71 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
# @begin:license
|
||||
#
|
||||
# Copyright (c) 2015-2018, 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
|
||||
import os.path
|
||||
from typing import cast, Any, Optional, Dict, List, Set # pylint: disable=unused-import
|
||||
|
||||
from PyQt5 import QtSvg # type: ignore
|
||||
from PyQt5 import QtWidgets
|
||||
|
||||
from noisicaa.constants import DATA_DIR
|
||||
from noisicaa import music
|
||||
|
||||
from . import base_node
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TrackNode(base_node.Node):
|
||||
def __init__(self, *, node: music.BasePipelineGraphNode, **kwargs: Any) -> None:
|
||||
if isinstance(node, music.ScoreTrack):
|
||||
icon = QtSvg.QSvgRenderer(os.path.join(DATA_DIR, 'icons', 'track-type-score.svg'))
|
||||
elif isinstance(node, music.BeatTrack):
|
||||
icon = QtSvg.QSvgRenderer(os.path.join(DATA_DIR, 'icons', 'track-type-beat.svg'))
|
||||
elif isinstance(node, music.ControlTrack):
|
||||
icon = QtSvg.QSvgRenderer(os.path.join(DATA_DIR, 'icons', 'track-type-control.svg'))
|
||||
elif isinstance(node, music.SampleTrack):
|
||||
icon = QtSvg.QSvgRenderer(os.path.join(DATA_DIR, 'icons', 'track-type-sample.svg'))
|
||||
else:
|
||||
raise ValueError(type(node))
|
||||
|
||||
super().__init__(node=node, icon=icon, **kwargs)
|
||||
|
||||
self.__track = cast(music.Track, node)
|
||||
|
||||
self.__show_track_action = QtWidgets.QAction("Show track")
|
||||
self.__show_track_action.setCheckable(True)
|
||||
self.__show_track_action.setChecked(self.__track.visible)
|
||||
self.__show_track_action.toggled.connect(self.setTrackVisiblity)
|
||||
|
||||
def buildContextMenu(self, menu: QtWidgets.QMenu) -> None:
|
||||
menu.addAction(self.__show_track_action)
|
||||
super().buildContextMenu(menu)
|
||||
|
||||
def setTrackVisiblity(self, visible: bool) -> None:
|
||||
if visible == self.__track.visible:
|
||||
return
|
||||
|
||||
self.send_command_async(music.Command(
|
||||
target=self.__track.id,
|
||||
update_track=music.UpdateTrack(
|
||||
visible=visible)))
|
|
@ -0,0 +1,74 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
# @begin:license
|
||||
#
|
||||
# Copyright (c) 2015-2018, 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 Any, Optional
|
||||
|
||||
from PyQt5 import QtWidgets
|
||||
|
||||
from noisicaa import music
|
||||
from noisicaa.ui import ui_base
|
||||
from noisicaa.ui import slots
|
||||
from . import canvas
|
||||
from . import toolbox
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Frame(QtWidgets.QFrame):
|
||||
def __init__(self, parent: Optional[QtWidgets.QWidget]) -> None:
|
||||
super().__init__(parent)
|
||||
|
||||
self.setFrameStyle(QtWidgets.QFrame.Sunken | QtWidgets.QFrame.Panel)
|
||||
self.__layout = QtWidgets.QVBoxLayout()
|
||||
self.__layout.setSizeConstraint(QtWidgets.QLayout.SetMinAndMaxSize)
|
||||
self.__layout.setContentsMargins(1, 1, 1, 1)
|
||||
self.setLayout(self.__layout)
|
||||
|
||||
def setWidget(self, widget: QtWidgets.QWidget) -> None:
|
||||
self.__layout.addWidget(widget, 1)
|
||||
|
||||
|
||||
class PipelineGraphView(ui_base.ProjectMixin, slots.SlotContainer, QtWidgets.QWidget):
|
||||
currentTrack, setCurrentTrack, currentTrackChanged = slots.slot(
|
||||
music.Track, 'currentTrack')
|
||||
|
||||
def __init__(self, **kwargs: Any) -> None:
|
||||
super().__init__(**kwargs)
|
||||
|
||||
canvas_frame = Frame(parent=self)
|
||||
self.__canvas = canvas.Canvas(parent=canvas_frame, context=self.context)
|
||||
canvas_frame.setWidget(self.__canvas)
|
||||
|
||||
self.__toolbox = toolbox.Toolbox(parent=self, context=self.context)
|
||||
self.__toolbox.toolChanged.connect(self.__canvas.toolChanged)
|
||||
self.__toolbox.resetViewTriggered.connect(self.__canvas.resetView)
|
||||
|
||||
layout = QtWidgets.QHBoxLayout()
|
||||
layout.setSpacing(0)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.addWidget(self.__toolbox)
|
||||
layout.addWidget(canvas_frame)
|
||||
self.setLayout(layout)
|
||||
|
||||
self.currentTrackChanged.connect(self.__canvas.setCurrentTrack)
|
||||
self.__canvas.currentTrackChanged.connect(self.setCurrentTrack)
|
|
@ -21,16 +21,9 @@
|
|||
# @end:license
|
||||
|
||||
from noisidev import uitest
|
||||
from . import pipeline_graph_view
|
||||
from . import view
|
||||
|
||||
|
||||
class PluginUITest(uitest.UITestCase):
|
||||
async def test_init(self):
|
||||
node = None
|
||||
|
||||
plugin_ui = pipeline_graph_view.PluginUI(node=node, context=self.context)
|
||||
try:
|
||||
pass
|
||||
|
||||
finally:
|
||||
plugin_ui.cleanup()
|
||||
class PipelineGraphTest(uitest.ProjectMixin, uitest.UITestCase):
|
||||
def test_init(self):
|
||||
view.PipelineGraphView(context=self.context)
|
File diff suppressed because it is too large
Load Diff
|
@ -78,8 +78,9 @@ class PipelinePerfMonitor(ui_base.AbstractPipelinePerfMonitor):
|
|||
self.gantt_view.setDragMode(QtWidgets.QGraphicsView.ScrollHandDrag)
|
||||
self.setCentralWidget(self.gantt_view)
|
||||
|
||||
self.setVisible(
|
||||
bool(self.app.settings.value('dialog/pipeline_perf_monitor/visible', False)))
|
||||
# TODO: somehow always ends up being shown...
|
||||
#self.setVisible(
|
||||
# bool(self.app.settings.value('dialog/pipeline_perf_monitor/visible', False)))
|
||||
self.restoreGeometry(
|
||||
self.app.settings.value('dialog/pipeline_perf_monitor/geometry', b''))
|
||||
|
||||
|
|
|
@ -0,0 +1,156 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
# @begin:license
|
||||
#
|
||||
# Copyright (c) 2015-2018, 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
|
||||
import time as time_lib
|
||||
from typing import cast, Any, Optional, Union, Sequence, Dict, List, Tuple, Type # pylint: disable=unused-import
|
||||
|
||||
from PyQt5 import QtCore
|
||||
|
||||
from noisicaa import audioproc
|
||||
from noisicaa.audioproc.public import musical_time_pb2
|
||||
from . import ui_base
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PlayerState(ui_base.ProjectMixin, QtCore.QObject):
|
||||
playingChanged = QtCore.pyqtSignal(bool)
|
||||
currentTimeChanged = QtCore.pyqtSignal(object)
|
||||
loopStartTimeChanged = QtCore.pyqtSignal(object)
|
||||
loopEndTimeChanged = QtCore.pyqtSignal(object)
|
||||
loopEnabledChanged = QtCore.pyqtSignal(bool)
|
||||
|
||||
def __init__(self, **kwargs: Any) -> None:
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.__session_prefix = 'player_state:%s:' % self.project.id
|
||||
self.__last_current_time_update = None # type: float
|
||||
|
||||
self.__playing = False
|
||||
self.__current_time = self.__get_session_value('current_time', audioproc.MusicalTime())
|
||||
self.__loop_start_time = self.__get_session_value('loop_start_time', None)
|
||||
self.__loop_end_time = self.__get_session_value('loop_end_time', None)
|
||||
self.__loop_enabled = self.__get_session_value('loop_enabled', False)
|
||||
|
||||
self.__player_id = None # type: str
|
||||
|
||||
def __get_session_value(self, key: str, default: Any) -> Any:
|
||||
return self.get_session_value(self.__session_prefix + key, default)
|
||||
|
||||
def __set_session_value(self, key: str, value: Any) -> None:
|
||||
self.set_session_value(self.__session_prefix + key, value)
|
||||
|
||||
def playerID(self) -> str:
|
||||
return self.__player_id
|
||||
|
||||
def setPlayerID(self, player_id: str) -> None:
|
||||
self.__player_id = player_id
|
||||
|
||||
def updateFromProto(self, player_state: audioproc.PlayerState) -> None:
|
||||
if player_state.HasField('current_time'):
|
||||
self.setCurrentTime(audioproc.MusicalTime.from_proto(player_state.current_time))
|
||||
|
||||
if player_state.HasField('playing'):
|
||||
self.setPlaying(player_state.playing)
|
||||
|
||||
if player_state.HasField('loop_enabled'):
|
||||
self.setLoopEnabled(player_state.loop_enabled)
|
||||
|
||||
if player_state.HasField('loop_start_time'):
|
||||
self.setLoopStartTime(audioproc.MusicalTime.from_proto(player_state.loop_start_time))
|
||||
|
||||
if player_state.HasField('loop_end_time'):
|
||||
self.setLoopEndTime(audioproc.MusicalTime.from_proto(player_state.loop_end_time))
|
||||
|
||||
def setPlaying(self, playing: bool) -> None:
|
||||
if playing == self.__playing:
|
||||
return
|
||||
|
||||
self.__playing = playing
|
||||
self.playingChanged.emit(playing)
|
||||
|
||||
def playing(self) -> bool:
|
||||
return self.__playing
|
||||
|
||||
def setCurrentTime(self, current_time: audioproc.MusicalTime) -> None:
|
||||
if current_time == self.__current_time:
|
||||
return
|
||||
|
||||
self.__current_time = current_time
|
||||
if (self.__last_current_time_update is None
|
||||
or time_lib.time() - self.__last_current_time_update > 5):
|
||||
self.__set_session_value('current_time', current_time)
|
||||
self.__last_current_time_update = time_lib.time()
|
||||
self.currentTimeChanged.emit(current_time)
|
||||
|
||||
def currentTime(self) -> audioproc.MusicalTime:
|
||||
return self.__current_time
|
||||
|
||||
def currentTimeProto(self) -> musical_time_pb2.MusicalTime:
|
||||
return self.__current_time.to_proto()
|
||||
|
||||
def setLoopStartTime(self, loop_start_time: audioproc.MusicalTime) -> None:
|
||||
if loop_start_time == self.__loop_start_time:
|
||||
return
|
||||
|
||||
self.__loop_start_time = loop_start_time
|
||||
self.__set_session_value('loop_start_time', loop_start_time)
|
||||
self.loopStartTimeChanged.emit(loop_start_time)
|
||||
|
||||
def loopStartTime(self) -> audioproc.MusicalTime:
|
||||
return self.__loop_start_time
|
||||
|
||||
def loopStartTimeProto(self) -> musical_time_pb2.MusicalTime:
|
||||
if self.__loop_start_time is not None:
|
||||
return self.__loop_start_time.to_proto()
|
||||
else:
|
||||
return None
|
||||
|
||||
def setLoopEndTime(self, loop_end_time: audioproc.MusicalTime) -> None:
|
||||
if loop_end_time == self.__loop_end_time:
|
||||
return
|
||||
|
||||
self.__loop_end_time = loop_end_time
|
||||
self.__set_session_value('loop_end_time', loop_end_time)
|
||||
self.loopEndTimeChanged.emit(loop_end_time)
|
||||
|
||||
def loopEndTime(self) -> audioproc.MusicalTime:
|
||||
return self.__loop_end_time
|
||||
|
||||
def loopEndTimeProto(self) -> musical_time_pb2.MusicalTime:
|
||||
if self.__loop_end_time is not None:
|
||||
return self.__loop_end_time.to_proto()
|
||||
else:
|
||||
return None
|
||||
|
||||
def setLoopEnabled(self, loop_enabled: bool) -> None:
|
||||
loop_enabled = bool(loop_enabled)
|
||||
if loop_enabled == self.__loop_enabled:
|
||||
return
|
||||
|
||||
self.__loop_enabled = loop_enabled
|
||||
self.__set_session_value('loop_enabled', loop_enabled)
|
||||
self.loopEnabledChanged.emit(loop_enabled)
|
||||
|
||||
def loopEnabled(self) -> bool:
|
||||
return self.__loop_enabled
|
|
@ -1,84 +0,0 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
# @begin:license
|
||||
#
|
||||
# Copyright (c) 2015-2018, 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 Any, List # pylint: disable=unused-import
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5 import QtCore
|
||||
from PyQt5 import QtWidgets
|
||||
|
||||
from noisicaa import core # pylint: disable=unused-import
|
||||
from noisicaa import music
|
||||
from noisicaa import model
|
||||
from . import dock_widget
|
||||
from . import ui_base
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ProjectProperties(ui_base.ProjectMixin, QtWidgets.QWidget):
|
||||
def __init__(self, **kwargs: Any) -> None:
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.__listeners = [] # type: List[core.Listener]
|
||||
|
||||
self.__bpm = QtWidgets.QSpinBox(self)
|
||||
self.__bpm.setRange(1, 1000)
|
||||
self.__bpm.valueChanged.connect(self.onBPMEdited)
|
||||
self.__bpm.setValue(self.project.bpm)
|
||||
self.__listeners.append(self.project.bpm_changed.add(self.onBPMChanged))
|
||||
|
||||
self.__form_layout = QtWidgets.QFormLayout()
|
||||
self.__form_layout.setSpacing(1)
|
||||
self.__form_layout.setContentsMargins(QtCore.QMargins(0, 0, 0, 0))
|
||||
self.__form_layout.addRow("BPM", self.__bpm)
|
||||
|
||||
self.setLayout(self.__form_layout)
|
||||
|
||||
# TODO: this gets never called...
|
||||
def cleanup(self) -> None:
|
||||
for listener in self.__listeners:
|
||||
listener.remove()
|
||||
self.__listeners.clear()
|
||||
|
||||
def onBPMChanged(self, change: model.PropertyValueChange[int]) -> None:
|
||||
self.__bpm.setValue(change.new_value)
|
||||
|
||||
def onBPMEdited(self, bpm: int) -> None:
|
||||
if bpm != self.project.bpm:
|
||||
self.send_command_async(music.Command(
|
||||
target=self.project.id,
|
||||
update_project_properties=music.UpdateProjectProperties(bpm=bpm)))
|
||||
|
||||
|
||||
class ProjectPropertiesDockWidget(ui_base.ProjectMixin, dock_widget.DockWidget):
|
||||
def __init__(self, **kwargs: Any) -> None:
|
||||
super().__init__(
|
||||
identifier='project-properties',
|
||||
title="Project Properties",
|
||||
allowed_areas=Qt.AllDockWidgetAreas,
|
||||
initial_area=Qt.RightDockWidgetArea,
|
||||
initial_visible=True,
|
||||
**kwargs)
|
||||
|
||||
self.setWidget(ProjectProperties(context=self.context))
|
File diff suppressed because it is too large
Load Diff
|
@ -56,7 +56,8 @@ class SettingsDialog(ui_base.CommonMixin, QtWidgets.QDialog):
|
|||
|
||||
self.setLayout(layout)
|
||||
|
||||
self.setVisible(bool(self.app.settings.value('dialog/settings/visible', False)))
|
||||
# TODO: somehow always ends up being shown...
|
||||
#self.setVisible(bool(self.app.settings.value('dialog/settings/visible', False)))
|
||||
self.restoreGeometry(self.app.settings.value('dialog/settings/geometry', b''))
|
||||
self.tabs.setCurrentIndex(int(self.app.settings.value('dialog/settings/page', 0)))
|
||||
|
||||
|
|
|
@ -0,0 +1,74 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
# @begin:license
|
||||
#
|
||||
# Copyright (c) 2015-2018, 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
|
||||
import operator
|
||||
from typing import cast, Any, Dict, Tuple, Type, Callable, Generic, TypeVar # pylint: disable=unused-import
|
||||
|
||||
from PyQt5 import QtCore
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# This should really be a subclass of QtCore.QObject, but PyQt5 doesn't support
|
||||
# multiple inheritance with QObjects.
|
||||
class SlotContainer(object):
|
||||
def __init__(self, **kwargs: Any) -> None:
|
||||
super().__init__(**kwargs) # type: ignore
|
||||
|
||||
self._slots = {} # type: Dict[str, Any]
|
||||
|
||||
|
||||
_type = type
|
||||
|
||||
T = TypeVar('T')
|
||||
def slot(
|
||||
type: Type[T], # pylint: disable=redefined-builtin
|
||||
name: str,
|
||||
*,
|
||||
default: T = None,
|
||||
equality: Callable = None
|
||||
) -> Tuple[Callable[[SlotContainer], T], Callable[[SlotContainer, T], None], QtCore.pyqtSignal]:
|
||||
assert isinstance(type, _type), type
|
||||
if equality is None:
|
||||
if type in (int, float, bool, str):
|
||||
equality = operator.eq
|
||||
else:
|
||||
equality = operator.is_
|
||||
|
||||
signal = QtCore.pyqtSignal(type)
|
||||
|
||||
def getter(self: SlotContainer) -> T:
|
||||
return self._slots.get(name, default)
|
||||
|
||||
def setter(self: SlotContainer, value: T) -> None:
|
||||
if not isinstance(value, type):
|
||||
raise TypeError("Expected %s, got %s" % (type.__name__, _type(value).__name__))
|
||||
|
||||
current_value = self._slots.get(name, default)
|
||||
if not equality(value, current_value):
|
||||
logger.debug("Slot %s on %s set to %s", name, self, value)
|
||||
self._slots[name] = value
|
||||
sig_inst = signal.__get__(cast(QtCore.QObject, self))
|
||||
sig_inst.emit(value)
|
||||
|
||||
return getter, setter, signal
|
|
@ -1,105 +0,0 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
# @begin:license
|
||||
#
|
||||
# Copyright (c) 2015-2018, 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 noisidev import uitest
|
||||
from noisicaa import music
|
||||
from noisicaa.ui import tools
|
||||
from . import base_track_item
|
||||
from . import track_item_tests
|
||||
|
||||
|
||||
class TestTool(tools.ToolBase):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(
|
||||
type=tools.ToolType.EDIT_SAMPLES,
|
||||
group=tools.ToolGroup.EDIT,
|
||||
**kwargs)
|
||||
|
||||
def iconName(self):
|
||||
return 'test-icon'
|
||||
|
||||
|
||||
class TestToolBox(tools.ToolBox):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.addTool(TestTool(context=self.context))
|
||||
|
||||
|
||||
class TestTrackEditorItem(base_track_item.BaseTrackEditorItem):
|
||||
toolBoxClass = TestToolBox
|
||||
|
||||
|
||||
class BaseTrackEditorItemTest(track_item_tests.TrackEditorItemTestMixin, uitest.UITestCase):
|
||||
async def setup_testcase(self):
|
||||
await self.project_client.send_command(music.Command(
|
||||
target=self.project.id,
|
||||
add_track=music.AddTrack(
|
||||
track_type='score',
|
||||
parent_group_id=self.project.master_group.id)))
|
||||
|
||||
self.tool_box = TestToolBox(context=self.context)
|
||||
|
||||
def _createTrackItem(self, **kwargs):
|
||||
return TestTrackEditorItem(
|
||||
track=self.project.master_group.tracks[0],
|
||||
player_state=self.player_state,
|
||||
editor=self.editor,
|
||||
context=self.context,
|
||||
**kwargs)
|
||||
|
||||
|
||||
class TestMeasureEditorItem(base_track_item.MeasureEditorItem):
|
||||
layers = ['fg']
|
||||
|
||||
def addMeasureListeners(self):
|
||||
pass
|
||||
|
||||
def paintPlaybackPos(self, painter):
|
||||
pass
|
||||
|
||||
def paintLayer(self, layer, painter):
|
||||
assert layer == 'fg'
|
||||
|
||||
|
||||
class TestMeasuredTrackEditorItem(base_track_item.MeasuredTrackEditorItem):
|
||||
toolBoxClass = TestToolBox
|
||||
measure_item_cls = TestMeasureEditorItem
|
||||
|
||||
|
||||
class MeasuredTrackEditorItemTest(track_item_tests.TrackEditorItemTestMixin, uitest.UITestCase):
|
||||
async def setup_testcase(self):
|
||||
await self.project_client.send_command(music.Command(
|
||||
target=self.project.id,
|
||||
add_track=music.AddTrack(
|
||||
track_type='score',
|
||||
parent_group_id=self.project.master_group.id)))
|
||||
|
||||
self.tool_box = TestToolBox(context=self.context)
|
||||
|
||||
def _createTrackItem(self, **kwargs):
|
||||
return TestMeasuredTrackEditorItem(
|
||||
track=self.project.master_group.tracks[0],
|
||||
player_state=self.player_state,
|
||||
editor=self.editor,
|
||||
context=self.context,
|
||||
**kwargs)
|
|
@ -1,45 +0,0 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
# @begin:license
|
||||
#
|
||||
# Copyright (c) 2015-2018, 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 noisidev import uitest
|
||||
from noisicaa import music
|
||||
from . import beat_track_item
|
||||
from . import track_item_tests
|
||||
|
||||
|
||||
class BeatTrackEditorItemTest(track_item_tests.TrackEditorItemTestMixin, uitest.UITestCase):
|
||||
async def setup_testcase(self):
|
||||
await self.project_client.send_command(music.Command(
|
||||
target=self.project.id,
|
||||
add_track=music.AddTrack(
|
||||
track_type='beat',
|
||||
parent_group_id=self.project.master_group.id)))
|
||||
|
||||
self.tool_box = beat_track_item.BeatToolBox(context=self.context)
|
||||
|
||||
def _createTrackItem(self, **kwargs):
|
||||
return beat_track_item.BeatTrackEditorItem(
|
||||
track=self.project.master_group.tracks[0],
|
||||
player_state=self.player_state,
|
||||
editor=self.editor,
|
||||
context=self.context,
|
||||
**kwargs)
|
|
@ -1,45 +0,0 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
# @begin:license
|
||||
#
|
||||
# Copyright (c) 2015-2018, 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 noisidev import uitest
|
||||
from noisicaa import music
|
||||
from . import score_track_item
|
||||
from . import track_item_tests
|
||||
|
||||
|
||||
class ScoreTrackEditorItemTest(track_item_tests.TrackEditorItemTestMixin, uitest.UITestCase):
|
||||
async def setup_testcase(self):
|
||||
await self.project_client.send_command(music.Command(
|
||||
target=self.project.id,
|
||||
add_track=music.AddTrack(
|
||||
track_type='score',
|
||||
parent_group_id=self.project.master_group.id)))
|
||||
|
||||
self.tool_box = score_track_item.ScoreToolBox(context=self.context)
|
||||
|
||||
def _createTrackItem(self, **kwargs):
|
||||
return score_track_item.ScoreTrackEditorItem(
|
||||
track=self.project.master_group.tracks[0],
|
||||
player_state=self.player_state,
|
||||
editor=self.editor,
|
||||
context=self.context,
|
||||
**kwargs)
|
|
@ -0,0 +1,40 @@
|
|||
# @begin:license
|
||||
#
|
||||
# Copyright (c) 2015-2018, 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(
|
||||
base_track_editor.py
|
||||
base_track_editor_test.py
|
||||
beat_track_editor.py
|
||||
beat_track_editor_test.py
|
||||
control_track_editor.py
|
||||
control_track_editor_test.py
|
||||
editor.py
|
||||
measured_track_editor.py
|
||||
sample_track_editor.py
|
||||
sample_track_editor_test.py
|
||||
score_track_editor.py
|
||||
score_track_editor_test.py
|
||||
time_line.py
|
||||
time_view_mixin.py
|
||||
toolbox.py
|
||||
tools.py
|
||||
track_editor_tests.py
|
||||
view.py
|
||||
)
|
|
@ -20,23 +20,4 @@
|
|||
#
|
||||
# @end:license
|
||||
|
||||
import logging
|
||||
|
||||
from noisicaa import model
|
||||
from . import commands_pb2
|
||||
from . import commands_test
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PropertiesTrackTest(commands_test.CommandsTestBase):
|
||||
async def test_set_time_signature(self):
|
||||
measure = self.project.property_track.measure_list[0].measure
|
||||
|
||||
await self.client.send_command(commands_pb2.Command(
|
||||
target=self.project.property_track.id,
|
||||
set_time_signature=commands_pb2.SetTimeSignature(
|
||||
measure_ids=[measure.id],
|
||||
upper=3,
|
||||
lower=4)))
|
||||
self.assertEqual(measure.time_signature, model.TimeSignature(3, 4))
|
||||
from .view import TrackListView
|
|
@ -0,0 +1,221 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
# @begin:license
|
||||
#
|
||||
# Copyright (c) 2015-2018, 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
|
||||
import logging
|
||||
from typing import Any, Optional, Union, Dict, List, Type # pylint: disable=unused-import
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5 import QtCore
|
||||
from PyQt5 import QtGui
|
||||
from PyQt5 import QtWidgets
|
||||
|
||||
from noisicaa import audioproc
|
||||
from noisicaa import music
|
||||
from noisicaa.ui import ui_base
|
||||
from noisicaa.ui import player_state as player_state_lib
|
||||
from . import time_view_mixin
|
||||
from . import tools
|
||||
|
||||
# TODO: These would create cyclic import dependencies.
|
||||
Editor = Any
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# TODO: fold into BaseTrackEditor
|
||||
class _Base(time_view_mixin.ScaledTimeMixin, ui_base.ProjectMixin, QtCore.QObject):
|
||||
rectChanged = QtCore.pyqtSignal(QtCore.QRect)
|
||||
sizeChanged = QtCore.pyqtSignal(QtCore.QSize)
|
||||
|
||||
def __init__(self, *, track: music.Track, **kwargs: Any) -> None:
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.__track = track
|
||||
self.__view_top_left = QtCore.QPoint()
|
||||
self.__is_current = False
|
||||
|
||||
self.__size = QtCore.QSize()
|
||||
|
||||
self.scaleXChanged.connect(self.__scaleXChanged)
|
||||
self.contentWidthChanged.connect(self.setWidth)
|
||||
|
||||
def close(self) -> None:
|
||||
pass
|
||||
|
||||
@property
|
||||
def track(self) -> music.Track:
|
||||
return self.__track
|
||||
|
||||
def __scaleXChanged(self, scale_x: fractions.Fraction) -> None:
|
||||
self.updateSize()
|
||||
self.purgePaintCaches()
|
||||
self.rectChanged.emit(self.viewRect())
|
||||
|
||||
def width(self) -> int:
|
||||
return self.__size.width()
|
||||
|
||||
def setWidth(self, width: int) -> None:
|
||||
self.setSize(QtCore.QSize(width, self.height()))
|
||||
|
||||
def height(self) -> int:
|
||||
return self.__size.height()
|
||||
|
||||
def setHeight(self, height: int) -> None:
|
||||
self.setSize(QtCore.QSize(self.width(), height))
|
||||
|
||||
def size(self) -> QtCore.QSize:
|
||||
return QtCore.QSize(self.__size)
|
||||
|
||||
def setSize(self, size: QtCore.QSize) -> None:
|
||||
if size != self.__size:
|
||||
self.__size = QtCore.QSize(size)
|
||||
self.sizeChanged.emit(self.__size)
|
||||
|
||||
def updateSize(self) -> None:
|
||||
pass
|
||||
|
||||
def viewTopLeft(self) -> QtCore.QPoint:
|
||||
return self.__view_top_left
|
||||
|
||||
def viewLeft(self) -> int:
|
||||
return self.__view_top_left.x()
|
||||
|
||||
def viewTop(self) -> int:
|
||||
return self.__view_top_left.y()
|
||||
|
||||
def setViewTopLeft(self, top_left: QtCore.QPoint) -> None:
|
||||
self.__view_top_left = QtCore.QPoint(top_left)
|
||||
|
||||
def viewRect(self) -> QtCore.QRect:
|
||||
return QtCore.QRect(self.__view_top_left, self.size())
|
||||
|
||||
def isCurrent(self) -> bool:
|
||||
return self.__is_current
|
||||
|
||||
def setIsCurrent(self, is_current: bool) -> None:
|
||||
if is_current != self.__is_current:
|
||||
self.__is_current = is_current
|
||||
#self.rectChanged.emit(self.viewRect())
|
||||
|
||||
def buildContextMenu(self, menu: QtWidgets.QMenu, pos: QtCore.QPoint) -> None:
|
||||
pass
|
||||
|
||||
def purgePaintCaches(self) -> None:
|
||||
pass
|
||||
|
||||
def paint(self, painter: QtGui.QPainter, paint_rect: QtCore.QRect) -> None:
|
||||
if self.isCurrent():
|
||||
painter.fillRect(paint_rect, QtGui.QColor(240, 240, 255))
|
||||
else:
|
||||
painter.fillRect(paint_rect, Qt.white)
|
||||
|
||||
def enterEvent(self, evt: QtCore.QEvent) -> None:
|
||||
pass # pragma: no coverage
|
||||
|
||||
def leaveEvent(self, evt: QtCore.QEvent) -> None:
|
||||
pass # pragma: no coverage
|
||||
|
||||
def mousePressEvent(self, evt: QtGui.QMouseEvent) -> None:
|
||||
pass # pragma: no coverage
|
||||
|
||||
def mouseReleaseEvent(self, evt: QtGui.QMouseEvent) -> None:
|
||||
pass # pragma: no coverage
|
||||
|
||||
def mouseDoubleClickEvent(self, evt: QtGui.QMouseEvent) -> None:
|
||||
pass # pragma: no coverage
|
||||
|
||||
def mouseMoveEvent(self, evt: QtGui.QMouseEvent) -> None:
|
||||
pass # pragma: no coverage
|
||||
|
||||
def wheelEvent(self, evt: QtGui.QWheelEvent) -> None:
|
||||
pass # pragma: no coverage
|
||||
|
||||
def keyPressEvent(self, evt: QtGui.QKeyEvent) -> None:
|
||||
pass # pragma: no coverage
|
||||
|
||||
def keyReleaseEvent(self, evt: QtGui.QKeyEvent) -> None:
|
||||
pass # pragma: no coverage
|
||||
|
||||
|
||||
class BaseTrackEditor(_Base):
|
||||
currentToolChanged = QtCore.pyqtSignal(tools.ToolType)
|
||||
|
||||
toolBoxClass = None # type: Type[tools.ToolBox]
|
||||
|
||||
def __init__(
|
||||
self, *,
|
||||
player_state: player_state_lib.PlayerState, editor: Editor, **kwargs: Any
|
||||
) -> None:
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.__player_state = player_state
|
||||
self.__editor = editor
|
||||
|
||||
def toolBox(self) -> tools.ToolBox:
|
||||
tool_box = self.__editor.currentToolBox()
|
||||
assert isinstance(tool_box, self.toolBoxClass)
|
||||
return tool_box
|
||||
|
||||
def currentTool(self) -> tools.ToolBase:
|
||||
return self.toolBox().currentTool()
|
||||
|
||||
def currentToolType(self) -> tools.ToolType:
|
||||
return self.toolBox().currentToolType()
|
||||
|
||||
def toolBoxMatches(self) -> bool:
|
||||
return isinstance(self.__editor.currentToolBox(), self.toolBoxClass)
|
||||
|
||||
def playerState(self) -> player_state_lib.PlayerState:
|
||||
return self.__player_state
|
||||
|
||||
def setPlaybackPos(self, time: audioproc.MusicalTime) -> None:
|
||||
pass
|
||||
|
||||
def mouseMoveEvent(self, evt: QtGui.QMouseEvent) -> None:
|
||||
if self.toolBoxMatches():
|
||||
self.toolBox().mouseMoveEvent(self, evt)
|
||||
|
||||
def mousePressEvent(self, evt: QtGui.QMouseEvent) -> None:
|
||||
if self.toolBoxMatches():
|
||||
self.toolBox().mousePressEvent(self, evt)
|
||||
|
||||
def mouseReleaseEvent(self, evt: QtGui.QMouseEvent) -> None:
|
||||
if self.toolBoxMatches():
|
||||
self.toolBox().mouseReleaseEvent(self, evt)
|
||||
|
||||
def mouseDoubleClickEvent(self, evt: QtGui.QMouseEvent) -> None:
|
||||
if self.toolBoxMatches():
|
||||
self.toolBox().mouseDoubleClickEvent(self, evt)
|
||||
|
||||
def wheelEvent(self, evt: QtGui.QWheelEvent) -> None:
|
||||
if self.toolBoxMatches():
|
||||
self.toolBox().wheelEvent(self, evt)
|
||||
|
||||
def keyPressEvent(self, evt: QtGui.QKeyEvent) -> None:
|
||||
if self.toolBoxMatches():
|
||||
self.toolBox().keyPressEvent(self, evt)
|
||||
|
||||
def keyReleaseEvent(self, evt: QtGui.QKeyEvent) -> None:
|
||||
if self.toolBoxMatches():
|
||||
self.toolBox().keyReleaseEvent(self, evt)
|
|
@ -0,0 +1,105 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
# @begin:license
|
||||
#
|
||||
# Copyright (c) 2015-2018, 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 noisidev import uitest
|
||||
# from noisicaa import music
|
||||
# from noisicaa.ui import tools
|
||||
# from . import base_track_item
|
||||
# from . import track_item_tests
|
||||
|
||||
|
||||
# class TestTool(tools.ToolBase):
|
||||
# def __init__(self, **kwargs):
|
||||
# super().__init__(
|
||||
# type=tools.ToolType.EDIT_SAMPLES,
|
||||
# group=tools.ToolGroup.EDIT,
|
||||
# **kwargs)
|
||||
|
||||
# def iconName(self):
|
||||
# return 'test-icon'
|
||||
|
||||
|
||||
# class TestToolBox(tools.ToolBox):
|
||||
# def __init__(self, **kwargs):
|
||||
# super().__init__(**kwargs)
|
||||
|
||||
# self.addTool(TestTool(context=self.context))
|
||||
|
||||
|
||||
# class TestTrackEditorItem(base_track_item.BaseTrackEditorItem):
|
||||
# toolBoxClass = TestToolBox
|
||||
|
||||
|
||||
# class BaseTrackEditorItemTest(track_item_tests.TrackEditorItemTestMixin, uitest.UITestCase):
|
||||
# async def setup_testcase(self):
|
||||
# await self.project_client.send_command(music.Command(
|
||||
# target=self.project.id,
|
||||
# add_track=music.AddTrack(
|
||||
# track_type='score',
|
||||
# parent_group_id=self.project.master_group.id)))
|
||||
|
||||
# self.tool_box = TestToolBox(context=self.context)
|
||||
|
||||
# def _createTrackItem(self, **kwargs):
|
||||
# return TestTrackEditorItem(
|
||||
# track=self.project.master_group.tracks[0],
|
||||
# player_state=self.player_state,
|
||||
# editor=self.editor,
|
||||
# context=self.context,
|
||||
# **kwargs)
|
||||
|
||||
|
||||
# class TestMeasureEditorItem(base_track_item.MeasureEditorItem):
|
||||
# layers = ['fg']
|
||||
|
||||
# def addMeasureListeners(self):
|
||||
# pass
|
||||
|
||||
# def paintPlaybackPos(self, painter):
|
||||
# pass
|
||||
|
||||
# def paintLayer(self, layer, painter):
|
||||
# assert layer == 'fg'
|
||||
|
||||
|
||||
# class TestMeasuredTrackEditorItem(base_track_item.MeasuredTrackEditorItem):
|
||||
# toolBoxClass = TestToolBox
|
||||
# measure_item_cls = TestMeasureEditorItem
|
||||
|
||||
|
||||
# class MeasuredTrackEditorItemTest(track_item_tests.TrackEditorItemTestMixin, uitest.UITestCase):
|
||||
# async def setup_testcase(self):
|
||||
# await self.project_client.send_command(music.Command(
|
||||
# target=self.project.id,
|
||||
# add_track=music.AddTrack(
|
||||
# track_type='score',
|
||||
# parent_group_id=self.project.master_group.id)))
|
||||
|
||||
# self.tool_box = TestToolBox(context=self.context)
|
||||
|
||||
# def _createTrackItem(self, **kwargs):
|
||||
# return TestMeasuredTrackEditorItem(
|
||||
# track=self.project.master_group.tracks[0],
|
||||
# player_state=self.player_state,
|
||||
# editor=self.editor,
|
||||
# context=self.context,
|
||||
# **kwargs)
|
|
@ -31,13 +31,13 @@ from noisicaa.core.typing_extra import down_cast
|
|||
from noisicaa import audioproc
|
||||
from noisicaa import music
|
||||
from noisicaa import model
|
||||
from noisicaa.ui import tools
|
||||
from . import base_track_item
|
||||
from . import measured_track_editor
|
||||
from . import tools
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class EditBeatsTool(base_track_item.MeasuredToolBase):
|
||||
class EditBeatsTool(measured_track_editor.MeasuredToolBase):
|
||||
def __init__(self, **kwargs: Any) -> None:
|
||||
super().__init__(
|
||||
type=tools.ToolType.EDIT_BEATS,
|
||||
|
@ -48,12 +48,12 @@ class EditBeatsTool(base_track_item.MeasuredToolBase):
|
|||
return 'edit-beats'
|
||||
|
||||
def mouseMoveEvent(self, target: Any, evt: QtGui.QMouseEvent) -> None:
|
||||
assert isinstance(target, BeatMeasureEditorItem), type(target).__name__
|
||||
assert isinstance(target, BeatMeasureEditor), type(target).__name__
|
||||
target.setGhostTime(target.xToTime(evt.pos().x()))
|
||||
super().mouseMoveEvent(target, evt)
|
||||
|
||||
def mousePressEvent(self, target: Any, evt: QtGui.QMouseEvent) -> None:
|
||||
assert isinstance(target, BeatMeasureEditorItem), type(target).__name__
|
||||
assert isinstance(target, BeatMeasureEditor), type(target).__name__
|
||||
|
||||
if (evt.button() == Qt.LeftButton and evt.modifiers() == Qt.NoModifier):
|
||||
click_time = target.xToTime(evt.pos().x())
|
||||
|
@ -69,14 +69,14 @@ class EditBeatsTool(base_track_item.MeasuredToolBase):
|
|||
self.send_command_async(music.Command(
|
||||
target=target.measure.id,
|
||||
add_beat=music.AddBeat(time=click_time.to_proto())))
|
||||
target.track_item.playNoteOn(target.track.pitch)
|
||||
target.track_editor.playNoteOn(target.track.pitch)
|
||||
evt.accept()
|
||||
return
|
||||
|
||||
super().mousePressEvent(target, evt)
|
||||
|
||||
def wheelEvent(self, target: Any, evt: QtGui.QWheelEvent) -> None:
|
||||
assert isinstance(target, BeatMeasureEditorItem), type(target).__name__
|
||||
assert isinstance(target, BeatMeasureEditor), type(target).__name__
|
||||
|
||||
if evt.modifiers() in (Qt.NoModifier, Qt.ShiftModifier):
|
||||
if evt.modifiers() == Qt.ShiftModifier:
|
||||
|
@ -102,18 +102,18 @@ class BeatToolBox(tools.ToolBox):
|
|||
def __init__(self, **kwargs: Any) -> None:
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.addTool(base_track_item.ArrangeMeasuresTool(context=self.context))
|
||||
self.addTool(measured_track_editor.ArrangeMeasuresTool(context=self.context))
|
||||
self.addTool(EditBeatsTool(context=self.context))
|
||||
|
||||
|
||||
class BeatMeasureEditorItem(base_track_item.MeasureEditorItem):
|
||||
class BeatMeasureEditor(measured_track_editor.MeasureEditor):
|
||||
FOREGROUND = 'fg'
|
||||
BACKGROUND = 'bg'
|
||||
GHOST = 'ghost'
|
||||
|
||||
layers = [
|
||||
BACKGROUND,
|
||||
base_track_item.MeasureEditorItem.PLAYBACK_POS,
|
||||
measured_track_editor.MeasureEditor.PLAYBACK_POS,
|
||||
FOREGROUND,
|
||||
GHOST,
|
||||
]
|
||||
|
@ -228,8 +228,8 @@ class BeatMeasureEditorItem(base_track_item.MeasureEditorItem):
|
|||
super().leaveEvent(evt)
|
||||
|
||||
|
||||
class BeatTrackEditorItem(base_track_item.MeasuredTrackEditorItem):
|
||||
measure_item_cls = BeatMeasureEditorItem
|
||||
class BeatTrackEditor(measured_track_editor.MeasuredTrackEditor):
|
||||
measure_editor_cls = BeatMeasureEditor
|
||||
|
||||
toolBoxClass = BeatToolBox
|
||||
|
|
@ -20,26 +20,26 @@
|
|||
#
|
||||
# @end:license
|
||||
|
||||
from noisidev import uitest
|
||||
from noisicaa import music
|
||||
from . import sample_track_item
|
||||
from . import track_item_tests
|
||||
# from noisidev import uitest
|
||||
# from noisicaa import music
|
||||
# from . import beat_track_item
|
||||
# from . import track_item_tests
|
||||
|
||||
|
||||
class SampleTrackEditorItemTest(track_item_tests.TrackEditorItemTestMixin, uitest.UITestCase):
|
||||
async def setup_testcase(self):
|
||||
await self.project_client.send_command(music.Command(
|
||||
target=self.project.id,
|
||||
add_track=music.AddTrack(
|
||||
track_type='sample',
|
||||
parent_group_id=self.project.master_group.id)))
|
||||
# class BeatTrackEditorItemTest(track_item_tests.TrackEditorItemTestMixin, uitest.UITestCase):
|
||||
# async def setup_testcase(self):
|
||||
# await self.project_client.send_command(music.Command(
|
||||
# target=self.project.id,
|
||||
# add_track=music.AddTrack(
|
||||
# track_type='beat',
|
||||
# parent_group_id=self.project.master_group.id)))
|
||||
|
||||
self.tool_box = sample_track_item.SampleTrackToolBox(context=self.context)
|
||||
# self.tool_box = beat_track_item.BeatToolBox(context=self.context)
|
||||
|
||||
def _createTrackItem(self, **kwargs):
|
||||
return sample_track_item.SampleTrackEditorItem(
|
||||
track=self.project.master_group.tracks[0],
|
||||
player_state=self.player_state,
|
||||
editor=self.editor,
|
||||
context=self.context,
|
||||
**kwargs)
|
||||
# def _createTrackItem(self, **kwargs):
|
||||
# return beat_track_item.BeatTrackEditorItem(
|
||||
# track=self.project.master_group.tracks[0],
|
||||
# player_state=self.player_state,
|
||||
# editor=self.editor,
|
||||
# context=self.context,
|
||||
# **kwargs)
|
|
@ -33,8 +33,9 @@ from noisicaa import audioproc
|
|||
from noisicaa import core # pylint: disable=unused-import
|
||||
from noisicaa import music
|
||||
from noisicaa import model
|
||||
from noisicaa.ui import tools
|
||||
from . import base_track_item
|
||||
from . import base_track_editor
|
||||
from . import time_view_mixin
|
||||
from . import tools
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -56,7 +57,7 @@ class EditControlPointsTool(tools.ToolBase):
|
|||
return 'edit-control-points'
|
||||
|
||||
def mousePressEvent(self, target: Any, evt: QtGui.QMouseEvent) -> None:
|
||||
assert isinstance(target, ControlTrackEditorItem), type(target).__name__
|
||||
assert isinstance(target, ControlTrackEditor), type(target).__name__
|
||||
|
||||
target.updateHighlightedPoint()
|
||||
|
||||
|
@ -73,12 +74,12 @@ class EditControlPointsTool(tools.ToolBase):
|
|||
if point_index > 0:
|
||||
range_left = target.points[point_index - 1].pos().x() + 1
|
||||
else:
|
||||
range_left = 10
|
||||
range_left = target.timeToX(audioproc.MusicalTime(0, 1))
|
||||
|
||||
if point_index < len(target.points) - 1:
|
||||
range_right = target.points[point_index + 1].pos().x() - 1
|
||||
else:
|
||||
range_right = target.width() - 11
|
||||
range_right = target.timeToX(target.projectEndTime())
|
||||
|
||||
self.__move_range = (range_left, range_right)
|
||||
|
||||
|
@ -105,7 +106,7 @@ class EditControlPointsTool(tools.ToolBase):
|
|||
super().mousePressEvent(target, evt)
|
||||
|
||||
def mouseMoveEvent(self, target: Any, evt: QtGui.QMouseEvent) -> None:
|
||||
assert isinstance(target, ControlTrackEditorItem), type(target).__name__
|
||||
assert isinstance(target, ControlTrackEditor), type(target).__name__
|
||||
|
||||
if self.__moving_point is not None:
|
||||
new_pos = evt.pos() - self.__moving_point_offset
|
||||
|
@ -146,7 +147,7 @@ class EditControlPointsTool(tools.ToolBase):
|
|||
super().mouseMoveEvent(target, evt)
|
||||
|
||||
def mouseReleaseEvent(self, target: Any, evt: QtGui.QMouseEvent) -> None:
|
||||
assert isinstance(target, ControlTrackEditorItem), type(target).__name__
|
||||
assert isinstance(target, ControlTrackEditor), type(target).__name__
|
||||
|
||||
if evt.button() == Qt.LeftButton and self.__moving_point is not None:
|
||||
pos = self.__moving_point.pos()
|
||||
|
@ -175,7 +176,7 @@ class EditControlPointsTool(tools.ToolBase):
|
|||
super().mouseReleaseEvent(target, evt)
|
||||
|
||||
def mouseDoubleClickEvent(self, target: Any, evt: QtGui.QMouseEvent) -> None:
|
||||
assert isinstance(target, ControlTrackEditorItem), type(target).__name__
|
||||
assert isinstance(target, ControlTrackEditor), type(target).__name__
|
||||
|
||||
if evt.button() == Qt.LeftButton and evt.modifiers() == Qt.NoModifier:
|
||||
# If the first half of the double click initiated a move,
|
||||
|
@ -214,13 +215,13 @@ class ControlTrackToolBox(tools.ToolBox):
|
|||
|
||||
|
||||
class ControlPoint(object):
|
||||
def __init__(self, track_item: 'ControlTrackEditorItem', point: music.ControlPoint) -> None:
|
||||
self.__track_item = track_item
|
||||
def __init__(self, track_editor: 'ControlTrackEditor', point: music.ControlPoint) -> None:
|
||||
self.__track_editor = track_editor
|
||||
self.__point = point
|
||||
|
||||
self.__pos = QtCore.QPoint(
|
||||
self.__track_item.timeToX(self.__point.time),
|
||||
self.__track_item.valueToY(self.__point.value))
|
||||
self.__track_editor.timeToX(self.__point.time),
|
||||
self.__track_editor.valueToY(self.__point.value))
|
||||
|
||||
self.__listeners = [
|
||||
self.__point.time_changed.add(self.onTimeChanged),
|
||||
|
@ -234,15 +235,15 @@ class ControlPoint(object):
|
|||
|
||||
def onTimeChanged(self, change: model.PropertyValueChange[audioproc.MusicalTime]) -> None:
|
||||
self.__pos = QtCore.QPoint(
|
||||
self.__track_item.timeToX(change.new_value),
|
||||
self.__track_editor.timeToX(change.new_value),
|
||||
self.__pos.y())
|
||||
self.__track_item.rectChanged.emit(self.__track_item.viewRect())
|
||||
self.__track_editor.rectChanged.emit(self.__track_editor.viewRect())
|
||||
|
||||
def onValueChanged(self, change: model.PropertyValueChange[float]) -> None:
|
||||
self.__pos = QtCore.QPoint(
|
||||
self.__pos.x(),
|
||||
self.__track_item.valueToY(change.new_value))
|
||||
self.__track_item.rectChanged.emit(self.__track_item.viewRect())
|
||||
self.__track_editor.valueToY(change.new_value))
|
||||
self.__track_editor.rectChanged.emit(self.__track_editor.viewRect())
|
||||
|
||||
@property
|
||||
def index(self) -> int:
|
||||
|
@ -262,18 +263,18 @@ class ControlPoint(object):
|
|||
def setPos(self, pos: QtCore.QPoint) -> None:
|
||||
if pos is None:
|
||||
self.__pos = QtCore.QPoint(
|
||||
self.__track_item.timeToX(self.__point.time),
|
||||
self.__track_item.valueToY(self.__point.value))
|
||||
self.__track_editor.timeToX(self.__point.time),
|
||||
self.__track_editor.valueToY(self.__point.value))
|
||||
else:
|
||||
self.__pos = pos
|
||||
|
||||
def recomputePos(self) -> None:
|
||||
self.__pos = QtCore.QPoint(
|
||||
self.__track_item.timeToX(self.__point.time),
|
||||
self.__track_item.valueToY(self.__point.value))
|
||||
self.__track_editor.timeToX(self.__point.time),
|
||||
self.__track_editor.valueToY(self.__point.value))
|
||||
|
||||
|
||||
class ControlTrackEditorItem(base_track_item.BaseTrackEditorItem):
|
||||
class ControlTrackEditor(time_view_mixin.ContinuousTimeMixin, base_track_editor.BaseTrackEditor):
|
||||
toolBoxClass = ControlTrackToolBox
|
||||
|
||||
def __init__(self, **kwargs: Any) -> None:
|
||||
|
@ -291,7 +292,7 @@ class ControlTrackEditorItem(base_track_item.BaseTrackEditorItem):
|
|||
|
||||
self.__listeners.append(self.track.points_changed.add(self.onPointsChanged))
|
||||
|
||||
self.updateSize()
|
||||
self.setHeight(120)
|
||||
|
||||
self.scaleXChanged.connect(self.__onScaleXChanged)
|
||||
|
||||
|
@ -306,13 +307,6 @@ class ControlTrackEditorItem(base_track_item.BaseTrackEditorItem):
|
|||
cpoint.recomputePos()
|
||||
self.rectChanged.emit(self.viewRect())
|
||||
|
||||
def updateSize(self) -> None:
|
||||
width = 20
|
||||
for mref in self.project.property_track.measure_list:
|
||||
measure = mref.measure
|
||||
width += int(self.scaleX() * measure.duration.fraction)
|
||||
self.setSize(QtCore.QSize(width, 120))
|
||||
|
||||
@property
|
||||
def track(self) -> music.ControlTrack:
|
||||
return down_cast(music.ControlTrack, super().track)
|
||||
|
@ -346,7 +340,7 @@ class ControlTrackEditorItem(base_track_item.BaseTrackEditorItem):
|
|||
self.rectChanged.emit(self.viewRect())
|
||||
|
||||
def addPoint(self, insert_index: int, point: music.ControlPoint) -> None:
|
||||
cpoint = ControlPoint(track_item=self, point=point)
|
||||
cpoint = ControlPoint(track_editor=self, point=point)
|
||||
self.points.insert(insert_index, cpoint)
|
||||
self.rectChanged.emit(self.viewRect())
|
||||
|
||||
|
@ -386,39 +380,6 @@ class ControlTrackEditorItem(base_track_item.BaseTrackEditorItem):
|
|||
def yToValue(self, y: int) -> float:
|
||||
return float(self.height() - y) / self.height()
|
||||
|
||||
def timeToX(self, time: audioproc.MusicalTime) -> int:
|
||||
x = 10
|
||||
for mref in self.project.property_track.measure_list:
|
||||
measure = mref.measure
|
||||
width = int(self.scaleX() * measure.duration.fraction)
|
||||
|
||||
if time - measure.duration <= audioproc.MusicalTime(0, 1):
|
||||
return x + int(width * (time / measure.duration).fraction)
|
||||
|
||||
x += width
|
||||
time -= measure.duration
|
||||
|
||||
return x
|
||||
|
||||
def xToTime(self, x: int) -> audioproc.MusicalTime:
|
||||
x -= 10
|
||||
time = audioproc.MusicalTime(0, 1)
|
||||
if x <= 0:
|
||||
return time
|
||||
|
||||
for mref in self.project.property_track.measure_list:
|
||||
measure = mref.measure
|
||||
width = int(self.scaleX() * measure.duration.fraction)
|
||||
|
||||
if x <= width:
|
||||
return audioproc.MusicalTime(
|
||||
time + measure.duration * fractions.Fraction(int(x), width))
|
||||
|
||||
time += measure.duration
|
||||
x -= width
|
||||
|
||||
return time
|
||||
|
||||
def leaveEvent(self, evt: QtCore.QEvent) -> None:
|
||||
self.__mouse_pos = None
|
||||
self.setHighlightedPoint(None)
|
||||
|
@ -443,24 +404,23 @@ class ControlTrackEditorItem(base_track_item.BaseTrackEditorItem):
|
|||
def paint(self, painter: QtGui.QPainter, paint_rect: QtCore.QRect) -> None:
|
||||
super().paint(painter, paint_rect)
|
||||
|
||||
x = 10
|
||||
for mref in self.project.property_track.measure_list:
|
||||
measure = down_cast(music.PropertyMeasure, mref.measure)
|
||||
width = int(self.scaleX() * measure.duration.fraction)
|
||||
painter.setPen(Qt.black)
|
||||
|
||||
if x + width > paint_rect.x() and x < paint_rect.x() + paint_rect.width():
|
||||
if mref.is_first:
|
||||
painter.fillRect(x, 0, 2, self.height(), QtGui.QColor(160, 160, 160))
|
||||
else:
|
||||
painter.fillRect(x, 0, 1, self.height(), QtGui.QColor(160, 160, 160))
|
||||
beat_time = audioproc.MusicalTime()
|
||||
beat_num = 0
|
||||
while beat_time < self.projectEndTime():
|
||||
x = self.timeToX(beat_time)
|
||||
|
||||
for i in range(1, measure.time_signature.upper):
|
||||
pos = int(width * i / measure.time_signature.lower)
|
||||
painter.fillRect(x + pos, 0, 1, self.height(), QtGui.QColor(200, 200, 200))
|
||||
if beat_num == 0:
|
||||
painter.fillRect(x, 0, 2, self.height(), Qt.black)
|
||||
else:
|
||||
painter.fillRect(x, 0, 1, self.height(), QtGui.QColor(160, 160, 160))
|
||||
|
||||
x += width
|
||||
beat_time += audioproc.MusicalDuration(1, 4)
|
||||
beat_num += 1
|
||||
|
||||
painter.fillRect(x - 2, 0, 2, self.height(), QtGui.QColor(160, 160, 160))
|
||||
x = self.timeToX(self.projectEndTime())
|
||||
painter.fillRect(x, 0, 2, self.height(), Qt.black)
|
||||
|
||||
points = self.points[:]
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
# @begin:license
|
||||
#
|
||||
# Copyright (c) 2015-2018, 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 noisidev import uitest
|
||||
# from noisicaa import music
|
||||
# from . import control_track_item
|
||||
# from . import track_item_tests
|
||||
|
||||
|
||||
# class ControlTrackEditorItemTest(track_item_tests.TrackEditorItemTestMixin, uitest.UITestCase):
|
||||
# async def setup_testcase(self):
|
||||
# await self.project_client.send_command(music.Command(
|
||||
# target=self.project.id,
|
||||
# add_track=music.AddTrack(
|
||||
# track_type='control',
|
||||
# parent_group_id=self.project.master_group.id)))
|
||||
|
||||
# self.tool_box = control_track_item.ControlTrackToolBox(context=self.context)
|
||||
|
||||
# def _createTrackItem(self, **kwargs):
|
||||
# return control_track_item.ControlTrackEditorItem(
|
||||
# track=self.project.master_group.tracks[0],
|
||||
# player_state=self.player_state,
|
||||
# editor=self.editor,
|
||||
# context=self.context,
|
||||
# **kwargs)
|
|
@ -0,0 +1,623 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
# @begin:license
|
||||
#
|
||||
# Copyright (c) 2015-2018, 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
|
||||
import logging
|
||||
from typing import cast, Any, Optional, Union, Sequence, Dict, List, Tuple, Type # pylint: disable=unused-import
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5 import QtCore
|
||||
from PyQt5 import QtGui
|
||||
from PyQt5 import QtWidgets
|
||||
|
||||
from noisicaa.core.typing_extra import down_cast
|
||||
from noisicaa import audioproc
|
||||
from noisicaa import core # pylint: disable=unused-import
|
||||
from noisicaa import music
|
||||
from noisicaa import model
|
||||
from noisicaa.ui import ui_base
|
||||
from noisicaa.ui import player_state as player_state_lib
|
||||
from . import time_view_mixin
|
||||
from . import base_track_editor
|
||||
from . import measured_track_editor
|
||||
from . import beat_track_editor
|
||||
from . import control_track_editor
|
||||
from . import score_track_editor
|
||||
from . import sample_track_editor
|
||||
from . import tools
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
track_editor_map = {
|
||||
'ScoreTrack': score_track_editor.ScoreTrackEditor,
|
||||
'BeatTrack': beat_track_editor.BeatTrackEditor,
|
||||
'ControlTrack': control_track_editor.ControlTrackEditor,
|
||||
'SampleTrack': sample_track_editor.SampleTrackEditor,
|
||||
} # type: Dict[str, Type[base_track_editor.BaseTrackEditor]]
|
||||
|
||||
|
||||
class AsyncSetupBase(object):
|
||||
async def setup(self) -> None:
|
||||
pass
|
||||
|
||||
async def cleanup(self) -> None:
|
||||
pass
|
||||
|
||||
|
||||
class Editor(
|
||||
time_view_mixin.TimeViewMixin, ui_base.ProjectMixin, AsyncSetupBase, QtWidgets.QWidget):
|
||||
maximumYOffsetChanged = QtCore.pyqtSignal(int)
|
||||
yOffsetChanged = QtCore.pyqtSignal(int)
|
||||
pageHeightChanged = QtCore.pyqtSignal(int)
|
||||
|
||||
currentToolBoxChanged = QtCore.pyqtSignal(tools.ToolBox)
|
||||
currentTrackChanged = QtCore.pyqtSignal(object)
|
||||
|
||||
def __init__(self, *, player_state: player_state_lib.PlayerState, **kwargs: Any) -> None:
|
||||
self.__player_state = player_state
|
||||
|
||||
self.__current_tool_box = None # type: tools.ToolBox
|
||||
self.__current_tool = None # type: tools.ToolBase
|
||||
self.__mouse_grabber = None # type: base_track_editor.BaseTrackEditor
|
||||
self.__current_track_editor = None # type: base_track_editor.BaseTrackEditor
|
||||
self.__hover_track_editor = None # type: base_track_editor.BaseTrackEditor
|
||||
|
||||
self.__y_offset = 0
|
||||
self.__content_height = 100
|
||||
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.setAttribute(Qt.WA_OpaquePaintEvent)
|
||||
self.setMouseTracking(True)
|
||||
self.setFocusPolicy(Qt.StrongFocus)
|
||||
self.setMinimumHeight(0)
|
||||
|
||||
self.__listeners = {} # type: Dict[str, core.Listener]
|
||||
|
||||
self.__current_track = None # type: music.Track
|
||||
self.__tracks = [] # type: List[base_track_editor.BaseTrackEditor]
|
||||
self.__track_map = {} # type: Dict[int, base_track_editor.BaseTrackEditor]
|
||||
|
||||
for node in self.project.pipeline_graph_nodes:
|
||||
self.__addNode(node)
|
||||
|
||||
self.__listeners['project:nodes'] = self.project.pipeline_graph_nodes_changed.add(
|
||||
self.__onNodesChanged)
|
||||
|
||||
for idx, track_editor in enumerate(self.__tracks):
|
||||
if idx == 0:
|
||||
self.__onCurrentTrackChanged(track_editor.track)
|
||||
|
||||
self.updateTracks()
|
||||
self.currentTrackChanged.connect(self.__onCurrentTrackChanged)
|
||||
|
||||
self.__player_state.currentTimeChanged.connect(
|
||||
lambda time: self.setPlaybackPos(time, 1))
|
||||
|
||||
self.scaleXChanged.connect(lambda _: self.updateTracks())
|
||||
|
||||
async def setup(self) -> None:
|
||||
await super().setup()
|
||||
|
||||
async def cleanup(self) -> None:
|
||||
for track_editor in list(self.__tracks):
|
||||
self.__removeNode(track_editor.track)
|
||||
|
||||
for listener in self.__listeners.values():
|
||||
listener.remove()
|
||||
self.__listeners.clear()
|
||||
|
||||
await super().cleanup()
|
||||
|
||||
def currentTrack(self) -> music.Track:
|
||||
return self.__current_track
|
||||
|
||||
def setCurrentTrack(self, track: music.Track) -> None:
|
||||
if track is self.__current_track:
|
||||
return
|
||||
|
||||
if self.__current_track is not None:
|
||||
track_editor = self.__track_map[self.__current_track.id]
|
||||
track_editor.setIsCurrent(False)
|
||||
if self.__current_track.visible:
|
||||
self.update(
|
||||
0, track_editor.viewTop() - self.yOffset(),
|
||||
self.width(), track_editor.height())
|
||||
self.__current_track = None
|
||||
|
||||
if track is not None:
|
||||
track_editor = self.__track_map[track.id]
|
||||
track_editor.setIsCurrent(True)
|
||||
self.__current_track = track
|
||||
|
||||
if self.__current_track.visible:
|
||||
self.update(
|
||||
0, track_editor.viewTop() - self.yOffset(),
|
||||
self.width(), track_editor.height())
|
||||
|
||||
if track_editor.track.visible and self.isVisible():
|
||||
yoffset = self.yOffset()
|
||||
if track_editor.viewTop() + track_editor.height() > yoffset + self.height():
|
||||
yoffset = track_editor.viewTop() + track_editor.height() - self.height()
|
||||
if track_editor.viewTop() < yoffset:
|
||||
yoffset = track_editor.viewTop()
|
||||
self.setYOffset(yoffset)
|
||||
|
||||
self.currentTrackChanged.emit(self.__current_track)
|
||||
|
||||
def __addNode(self, node: music.BasePipelineGraphNode) -> None:
|
||||
if isinstance(node, music.Track):
|
||||
track_editor = self.createTrack(node)
|
||||
self.__tracks.append(track_editor)
|
||||
self.__track_map[node.id] = track_editor
|
||||
self.__listeners['track:%s:visible' % node.id] = node.visible_changed.add(
|
||||
lambda *_: self.updateTracks())
|
||||
self.updateTracks()
|
||||
|
||||
def __removeNode(self, node: music.BasePipelineGraphNode) -> None:
|
||||
if isinstance(node, music.Track):
|
||||
self.__listeners.pop('track:%s:visible' % node.id).remove()
|
||||
|
||||
track_editor = self.__track_map.pop(node.id)
|
||||
for idx in range(len(self.__tracks)):
|
||||
if self.__tracks[idx] is track_editor:
|
||||
del self.__tracks[idx]
|
||||
break
|
||||
|
||||
track_editor.close()
|
||||
self.updateTracks()
|
||||
|
||||
def __onNodesChanged(
|
||||
self, change: model.PropertyListChange[music.BasePipelineGraphNode]) -> None:
|
||||
if isinstance(change, model.PropertyListInsert):
|
||||
self.__addNode(change.new_value)
|
||||
|
||||
elif isinstance(change, model.PropertyListDelete):
|
||||
self.__removeNode(change.old_value)
|
||||
|
||||
else: # pragma: no cover
|
||||
raise TypeError(type(change))
|
||||
|
||||
def createTrack(self, track: music.Track) -> base_track_editor.BaseTrackEditor:
|
||||
track_editor_cls = track_editor_map[type(track).__name__]
|
||||
track_editor = track_editor_cls(
|
||||
track=track,
|
||||
player_state=self.__player_state,
|
||||
editor=self,
|
||||
context=self.context)
|
||||
track_editor.rectChanged.connect(
|
||||
lambda rect: self.update(rect.translated(-self.offset())))
|
||||
track_editor.sizeChanged.connect(
|
||||
lambda size: self.updateTracks())
|
||||
return track_editor
|
||||
|
||||
def updateTracks(self) -> None:
|
||||
self.__content_height = 0
|
||||
|
||||
p = QtCore.QPoint(0, 0)
|
||||
for track_editor in self.__tracks:
|
||||
if not track_editor.track.visible:
|
||||
continue
|
||||
|
||||
track_editor.setScaleX(self.scaleX())
|
||||
track_editor.setViewTopLeft(p)
|
||||
p += QtCore.QPoint(0, track_editor.height())
|
||||
p += QtCore.QPoint(0, 3)
|
||||
|
||||
self.__content_height = p.y() + 10
|
||||
|
||||
self.maximumYOffsetChanged.emit(
|
||||
max(0, self.__content_height - self.height()))
|
||||
|
||||
self.update()
|
||||
|
||||
def __onCurrentTrackChanged(self, track: music.Track) -> None:
|
||||
if track is not None:
|
||||
track_editor = down_cast(base_track_editor.BaseTrackEditor, self.__track_map[track.id])
|
||||
self.__current_track_editor = track_editor
|
||||
|
||||
self.setCurrentToolBoxClass(track_editor.toolBoxClass)
|
||||
|
||||
else:
|
||||
self.__current_track_editor = None
|
||||
self.setCurrentToolBoxClass(None)
|
||||
|
||||
def currentToolBox(self) -> tools.ToolBox:
|
||||
return self.__current_tool_box
|
||||
|
||||
def setCurrentToolBoxClass(self, cls: Type[tools.ToolBox]) -> None:
|
||||
if type(self.__current_tool_box) is cls: # pylint: disable=unidiomatic-typecheck
|
||||
return
|
||||
logger.debug("Switching to tool box class %s", cls)
|
||||
|
||||
if self.__current_tool_box is not None:
|
||||
self.__current_tool_box.currentToolChanged.disconnect(self.__onCurrentToolChanged)
|
||||
self.__onCurrentToolChanged(None)
|
||||
self.__current_tool_box = None
|
||||
|
||||
if cls is not None:
|
||||
self.__current_tool_box = cls(context=self.context)
|
||||
self.__onCurrentToolChanged(self.__current_tool_box.currentTool())
|
||||
self.__current_tool_box.currentToolChanged.connect(self.__onCurrentToolChanged)
|
||||
|
||||
self.currentToolBoxChanged.emit(self.__current_tool_box)
|
||||
|
||||
def __onCurrentToolChanged(self, tool: tools.ToolBase) -> None:
|
||||
if tool is self.__current_tool:
|
||||
return
|
||||
|
||||
logger.debug("Current tool: %s", tool)
|
||||
|
||||
if self.__current_tool is not None:
|
||||
self.__current_tool.cursorChanged.disconnect(self.__onToolCursorChanged)
|
||||
self.__onToolCursorChanged(None)
|
||||
self.__current_tool = None
|
||||
|
||||
if tool is not None:
|
||||
self.__current_tool = tool
|
||||
self.__onToolCursorChanged(self.__current_tool.cursor())
|
||||
self.__current_tool.cursorChanged.connect(self.__onToolCursorChanged)
|
||||
|
||||
def __onToolCursorChanged(self, cursor: QtGui.QCursor) -> None:
|
||||
logger.debug("Cursor changed: %s", cursor)
|
||||
if cursor is not None:
|
||||
self.setCursor(cursor)
|
||||
else:
|
||||
self.setCursor(QtGui.QCursor(Qt.ArrowCursor))
|
||||
|
||||
def maximumYOffset(self) -> int:
|
||||
return max(0, self.__content_height - self.height())
|
||||
|
||||
def pageHeight(self) -> int:
|
||||
return self.height()
|
||||
|
||||
def yOffset(self) -> int:
|
||||
return self.__y_offset
|
||||
|
||||
def setYOffset(self, offset: int) -> None:
|
||||
if offset == self.__y_offset:
|
||||
return
|
||||
|
||||
dy = self.__y_offset - offset
|
||||
self.__y_offset = offset
|
||||
self.yOffsetChanged.emit(self.__y_offset)
|
||||
|
||||
self.scroll(0, dy)
|
||||
|
||||
def offset(self) -> QtCore.QPoint:
|
||||
return QtCore.QPoint(self.xOffset(), self.__y_offset)
|
||||
|
||||
def setPlaybackPos(self, current_time: audioproc.MusicalTime, num_samples: int) -> None:
|
||||
for track_editor in self.__tracks:
|
||||
track_editor.setPlaybackPos(current_time)
|
||||
|
||||
def onClearSelection(self) -> None:
|
||||
if self.selection_set.empty():
|
||||
return
|
||||
|
||||
self.send_command_async(music.Command(
|
||||
target=self.project.id,
|
||||
clear_measures=music.ClearMeasures(
|
||||
measure_ids=[
|
||||
mref.id for mref in sorted(
|
||||
(cast(measured_track_editor.MeasureEditor, measure_editor).measure_reference
|
||||
for measure_editor in self.selection_set),
|
||||
key=lambda mref: mref.index)])))
|
||||
|
||||
def onPaste(self, *, mode: str) -> None:
|
||||
assert mode in ('overwrite', 'link')
|
||||
|
||||
if self.selection_set.empty():
|
||||
return
|
||||
|
||||
clipboard = self.app.clipboardContent()
|
||||
if clipboard['type'] == 'measures':
|
||||
target_ids = [
|
||||
mref.id for mref in sorted(
|
||||
(cast(measured_track_editor.MeasureEditor, measure_editor).measure_reference
|
||||
for measure_editor in self.selection_set),
|
||||
key=lambda mref: mref.index)]
|
||||
|
||||
self.send_command_async(music.Command(
|
||||
target=self.project.id,
|
||||
paste_measures=music.PasteMeasures(
|
||||
mode=mode,
|
||||
src_objs=[copy['data'] for copy in clipboard['data']],
|
||||
target_ids=target_ids)))
|
||||
|
||||
else:
|
||||
raise ValueError(clipboard['type'])
|
||||
|
||||
def trackEditorAt(self, pos: QtCore.QPoint) -> base_track_editor.BaseTrackEditor:
|
||||
p = -self.offset()
|
||||
for track_editor in self.__tracks:
|
||||
if not track_editor.track.visible:
|
||||
continue
|
||||
|
||||
if p.y() <= pos.y() < p.y() + track_editor.height():
|
||||
return down_cast(base_track_editor.BaseTrackEditor, track_editor)
|
||||
|
||||
p += QtCore.QPoint(0, track_editor.height())
|
||||
p += QtCore.QPoint(0, 3)
|
||||
|
||||
return None
|
||||
|
||||
def resizeEvent(self, evt: QtGui.QResizeEvent) -> None:
|
||||
super().resizeEvent(evt)
|
||||
|
||||
self.maximumYOffsetChanged.emit(
|
||||
max(0, self.__content_height - self.height()))
|
||||
self.pageHeightChanged.emit(self.height())
|
||||
|
||||
def setHoverTrackEditor(
|
||||
self, track_editor: Optional[base_track_editor.BaseTrackEditor],
|
||||
evt: Union[None, QtGui.QEnterEvent, QtGui.QMouseEvent]
|
||||
) -> None:
|
||||
if track_editor is self.__hover_track_editor:
|
||||
return
|
||||
|
||||
if self.__hover_track_editor is not None:
|
||||
track_evt = QtCore.QEvent(QtCore.QEvent.Leave)
|
||||
self.__hover_track_editor.leaveEvent(track_evt)
|
||||
|
||||
if track_editor is not None:
|
||||
track_evt = QtGui.QEnterEvent(
|
||||
evt.localPos() + self.offset() - track_editor.viewTopLeft(),
|
||||
evt.windowPos(),
|
||||
evt.screenPos())
|
||||
track_editor.enterEvent(track_evt)
|
||||
|
||||
self.__hover_track_editor = track_editor
|
||||
|
||||
def enterEvent(self, evt: QtCore.QEvent) -> None:
|
||||
evt = down_cast(QtGui.QEnterEvent, evt)
|
||||
self.setHoverTrackEditor(self.trackEditorAt(evt.pos()), evt)
|
||||
|
||||
def leaveEvent(self, evt: QtCore.QEvent) -> None:
|
||||
self.setHoverTrackEditor(None, None)
|
||||
|
||||
def mouseMoveEvent(self, evt: QtGui.QMouseEvent) -> None:
|
||||
if self.__mouse_grabber is not None:
|
||||
track_editor = self.__mouse_grabber
|
||||
else:
|
||||
track_editor = self.trackEditorAt(evt.pos())
|
||||
self.setHoverTrackEditor(track_editor, evt)
|
||||
|
||||
if track_editor is not None:
|
||||
track_evt = QtGui.QMouseEvent(
|
||||
evt.type(),
|
||||
evt.localPos() + self.offset() - track_editor.viewTopLeft(),
|
||||
evt.windowPos(),
|
||||
evt.screenPos(),
|
||||
evt.button(),
|
||||
evt.buttons(),
|
||||
evt.modifiers())
|
||||
track_evt.setAccepted(False)
|
||||
track_editor.mouseMoveEvent(track_evt)
|
||||
evt.setAccepted(track_evt.isAccepted())
|
||||
return
|
||||
|
||||
def mousePressEvent(self, evt: QtGui.QMouseEvent) -> None:
|
||||
track_editor = self.trackEditorAt(evt.pos())
|
||||
if track_editor is not None:
|
||||
if not track_editor.isCurrent():
|
||||
self.setCurrentTrack(track_editor.track)
|
||||
evt.accept()
|
||||
return
|
||||
|
||||
if track_editor is not None:
|
||||
track_evt = QtGui.QMouseEvent(
|
||||
evt.type(),
|
||||
evt.localPos() + self.offset() - track_editor.viewTopLeft(),
|
||||
evt.windowPos(),
|
||||
evt.screenPos(),
|
||||
evt.button(),
|
||||
evt.buttons(),
|
||||
evt.modifiers())
|
||||
track_evt.setAccepted(False)
|
||||
track_editor.mousePressEvent(track_evt)
|
||||
if track_evt.isAccepted():
|
||||
self.__mouse_grabber = track_editor
|
||||
evt.setAccepted(track_evt.isAccepted())
|
||||
return
|
||||
|
||||
def mouseReleaseEvent(self, evt: QtGui.QMouseEvent) -> None:
|
||||
if self.__mouse_grabber is not None:
|
||||
track_evt = QtGui.QMouseEvent(
|
||||
evt.type(),
|
||||
evt.localPos() + self.offset() - self.__mouse_grabber.viewTopLeft(),
|
||||
evt.windowPos(),
|
||||
evt.screenPos(),
|
||||
evt.button(),
|
||||
evt.buttons(),
|
||||
evt.modifiers())
|
||||
track_evt.setAccepted(False)
|
||||
self.__mouse_grabber.mouseReleaseEvent(track_evt)
|
||||
self.__mouse_grabber = None
|
||||
evt.setAccepted(track_evt.isAccepted())
|
||||
self.setHoverTrackEditor(self.trackEditorAt(evt.pos()), evt)
|
||||
return
|
||||
|
||||
def mouseDoubleClickEvent(self, evt: QtGui.QMouseEvent) -> None:
|
||||
track_editor = self.trackEditorAt(evt.pos())
|
||||
if track_editor is not None:
|
||||
if not track_editor.isCurrent():
|
||||
self.setCurrentTrack(track_editor.track)
|
||||
evt.accept()
|
||||
return
|
||||
|
||||
if track_editor is not None:
|
||||
track_evt = QtGui.QMouseEvent(
|
||||
evt.type(),
|
||||
evt.localPos() + self.offset() - track_editor.viewTopLeft(),
|
||||
evt.windowPos(),
|
||||
evt.screenPos(),
|
||||
evt.button(),
|
||||
evt.buttons(),
|
||||
evt.modifiers())
|
||||
track_evt.setAccepted(False)
|
||||
track_editor.mouseDoubleClickEvent(track_evt)
|
||||
evt.setAccepted(track_evt.isAccepted())
|
||||
return
|
||||
|
||||
def wheelEvent(self, evt: QtGui.QWheelEvent) -> None:
|
||||
if evt.modifiers() == Qt.ShiftModifier:
|
||||
offset = self.xOffset()
|
||||
offset -= 2 * evt.angleDelta().y()
|
||||
offset = min(self.maximumXOffset(), offset)
|
||||
offset = max(0, offset)
|
||||
self.setXOffset(offset)
|
||||
evt.accept()
|
||||
return
|
||||
|
||||
elif evt.modifiers() == Qt.NoModifier:
|
||||
offset = self.yOffset()
|
||||
offset -= evt.angleDelta().y()
|
||||
offset = min(self.maximumYOffset(), offset)
|
||||
offset = max(0, offset)
|
||||
self.setYOffset(offset)
|
||||
evt.accept()
|
||||
return
|
||||
|
||||
track_editor = self.trackEditorAt(evt.pos())
|
||||
if track_editor is not None:
|
||||
track_evt = QtGui.QWheelEvent(
|
||||
evt.pos() + self.offset() - track_editor.viewTopLeft(),
|
||||
evt.globalPos(),
|
||||
evt.pixelDelta(),
|
||||
evt.angleDelta(),
|
||||
0,
|
||||
Qt.Horizontal,
|
||||
evt.buttons(),
|
||||
evt.modifiers(),
|
||||
evt.phase(),
|
||||
evt.source())
|
||||
track_evt.setAccepted(False)
|
||||
track_editor.wheelEvent(track_evt)
|
||||
evt.setAccepted(track_evt.isAccepted())
|
||||
return
|
||||
|
||||
def keyPressEvent(self, evt: QtGui.QKeyEvent) -> None:
|
||||
if evt.modifiers() == Qt.ControlModifier and evt.key() == Qt.Key_Left:
|
||||
if self.scaleX() > fractions.Fraction(10, 1):
|
||||
self.setScaleX(self.scaleX() * fractions.Fraction(2, 3))
|
||||
evt.accept()
|
||||
return
|
||||
|
||||
if evt.modifiers() == Qt.ControlModifier and evt.key() == Qt.Key_Right:
|
||||
self.setScaleX(self.scaleX() * fractions.Fraction(3, 2))
|
||||
evt.accept()
|
||||
return
|
||||
|
||||
current_track = self.currentTrack()
|
||||
if current_track is not None:
|
||||
current_track_editor = self.__track_map[current_track.id]
|
||||
track_evt = QtGui.QKeyEvent(
|
||||
evt.type(),
|
||||
evt.key(),
|
||||
evt.modifiers(),
|
||||
evt.nativeScanCode(),
|
||||
evt.nativeVirtualKey(),
|
||||
evt.nativeModifiers(),
|
||||
evt.text(),
|
||||
evt.isAutoRepeat(),
|
||||
evt.count())
|
||||
track_evt.setAccepted(False)
|
||||
current_track_editor.keyPressEvent(track_evt)
|
||||
evt.setAccepted(track_evt.isAccepted())
|
||||
return
|
||||
|
||||
def keyReleaseEvent(self, evt: QtGui.QKeyEvent) -> None:
|
||||
current_track = self.currentTrack()
|
||||
if current_track is not None:
|
||||
current_track_editor = self.__track_map[current_track.id]
|
||||
track_evt = QtGui.QKeyEvent(
|
||||
evt.type(),
|
||||
evt.key(),
|
||||
evt.modifiers(),
|
||||
evt.nativeScanCode(),
|
||||
evt.nativeVirtualKey(),
|
||||
evt.nativeModifiers(),
|
||||
evt.text(),
|
||||
evt.isAutoRepeat(),
|
||||
evt.count())
|
||||
track_evt.setAccepted(False)
|
||||
current_track_editor.keyReleaseEvent(track_evt)
|
||||
evt.setAccepted(track_evt.isAccepted())
|
||||
return
|
||||
|
||||
def contextMenuEvent(self, evt: QtGui.QContextMenuEvent) -> None:
|
||||
track_editor = self.trackEditorAt(evt.pos())
|
||||
if track_editor is not None:
|
||||
menu = QtWidgets.QMenu()
|
||||
track_editor.buildContextMenu(
|
||||
menu, evt.pos() + self.offset() - track_editor.viewTopLeft())
|
||||
if not menu.isEmpty():
|
||||
menu.exec_(evt.globalPos())
|
||||
evt.accept()
|
||||
return
|
||||
|
||||
def paintEvent(self, evt: QtGui.QPaintEvent) -> None:
|
||||
super().paintEvent(evt)
|
||||
|
||||
#t1 = time.perf_counter()
|
||||
|
||||
painter = QtGui.QPainter(self)
|
||||
try:
|
||||
p = -self.offset()
|
||||
for track_editor in self.__tracks:
|
||||
if not track_editor.track.visible:
|
||||
continue
|
||||
|
||||
track_rect = QtCore.QRect(
|
||||
0, p.y(), max(self.contentWidth(), self.width()), track_editor.height())
|
||||
track_rect = track_rect.intersected(evt.rect())
|
||||
if not track_rect.isEmpty():
|
||||
painter.save()
|
||||
try:
|
||||
painter.setClipRect(track_rect)
|
||||
painter.translate(p)
|
||||
track_editor.paint(painter, track_rect.translated(-p))
|
||||
finally:
|
||||
painter.restore()
|
||||
|
||||
# TODO: messes up display, when scrolling horizontally.
|
||||
#painter.drawText(15, p.y() + 14, track_editor.track.name)
|
||||
|
||||
p += QtCore.QPoint(0, track_editor.height())
|
||||
|
||||
painter.fillRect(
|
||||
evt.rect().left(), p.y(),
|
||||
evt.rect().width(), 3,
|
||||
QtGui.QColor(200, 200, 200))
|
||||
p += QtCore.QPoint(0, 3)
|
||||
|
||||
fill_rect = QtCore.QRect(p, evt.rect().bottomRight())
|
||||
if not fill_rect.isEmpty():
|
||||
painter.fillRect(fill_rect, Qt.white)
|
||||
|
||||
finally:
|
||||
painter.end()
|
||||
|
||||
#t2 = time.perf_counter()
|
||||
|
||||
#logger.info("Editor.paintEvent(%s): %.2fµs", evt.rect(), 1e6 * (t2 - t1))
|
|
@ -35,227 +35,38 @@ from noisicaa import audioproc
|
|||
from noisicaa import core # pylint: disable=unused-import
|
||||
from noisicaa import music
|
||||
from noisicaa import model
|
||||
from noisicaa.ui import tools
|
||||
from noisicaa.ui import ui_base
|
||||
from noisicaa.ui import selection_set
|
||||
from . import base_track_editor
|
||||
from . import tools
|
||||
|
||||
# TODO: These would create cyclic import dependencies.
|
||||
PlayerState = Any
|
||||
Editor = Any
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BaseTrackItem(ui_base.ProjectMixin, QtCore.QObject):
|
||||
rectChanged = QtCore.pyqtSignal(QtCore.QRect)
|
||||
sizeChanged = QtCore.pyqtSignal(QtCore.QSize)
|
||||
scaleXChanged = QtCore.pyqtSignal(fractions.Fraction)
|
||||
|
||||
def __init__(self, *, track: music.Track, **kwargs: Any) -> None:
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.__track = track
|
||||
self.__scale_x = fractions.Fraction(500, 1)
|
||||
self.__view_top_left = QtCore.QPoint()
|
||||
self.__is_current = False
|
||||
|
||||
self.__size = QtCore.QSize()
|
||||
|
||||
def close(self) -> None:
|
||||
pass
|
||||
|
||||
@property
|
||||
def track(self) -> music.Track:
|
||||
return self.__track
|
||||
|
||||
def scaleX(self) -> fractions.Fraction:
|
||||
return self.__scale_x
|
||||
|
||||
def setScaleX(self, scale_x: fractions.Fraction) -> None:
|
||||
if scale_x == self.__scale_x:
|
||||
return
|
||||
|
||||
self.__scale_x = scale_x
|
||||
self.updateSize()
|
||||
self.purgePaintCaches()
|
||||
self.scaleXChanged.emit(self.__scale_x)
|
||||
self.rectChanged.emit(self.viewRect())
|
||||
|
||||
def width(self) -> int:
|
||||
return self.__size.width()
|
||||
|
||||
def setWidth(self, width: int) -> None:
|
||||
self.setSize(QtCore.QSize(width, self.height()))
|
||||
|
||||
def height(self) -> int:
|
||||
return self.__size.height()
|
||||
|
||||
def setHeight(self, height: int) -> None:
|
||||
self.setSize(QtCore.QSize(self.width(), height))
|
||||
|
||||
def size(self) -> QtCore.QSize:
|
||||
return QtCore.QSize(self.__size)
|
||||
|
||||
def setSize(self, size: QtCore.QSize) -> None:
|
||||
if size != self.__size:
|
||||
self.__size = QtCore.QSize(size)
|
||||
self.sizeChanged.emit(self.__size)
|
||||
|
||||
def updateSize(self) -> None:
|
||||
pass
|
||||
|
||||
def viewTopLeft(self) -> QtCore.QPoint:
|
||||
return self.__view_top_left
|
||||
|
||||
def viewLeft(self) -> int:
|
||||
return self.__view_top_left.x()
|
||||
|
||||
def viewTop(self) -> int:
|
||||
return self.__view_top_left.y()
|
||||
|
||||
def setViewTopLeft(self, top_left: QtCore.QPoint) -> None:
|
||||
self.__view_top_left = QtCore.QPoint(top_left)
|
||||
|
||||
def viewRect(self) -> QtCore.QRect:
|
||||
return QtCore.QRect(self.__view_top_left, self.size())
|
||||
|
||||
def isCurrent(self) -> bool:
|
||||
return self.__is_current
|
||||
|
||||
def setIsCurrent(self, is_current: bool) -> None:
|
||||
if is_current != self.__is_current:
|
||||
self.__is_current = is_current
|
||||
self.rectChanged.emit(self.viewRect())
|
||||
|
||||
def buildContextMenu(self, menu: QtWidgets.QMenu, pos: QtCore.QPoint) -> None:
|
||||
remove_track_action = QtWidgets.QAction("Remove track", menu)
|
||||
remove_track_action.setStatusTip("Remove this track.")
|
||||
remove_track_action.triggered.connect(self.onRemoveTrack)
|
||||
menu.addAction(remove_track_action)
|
||||
|
||||
def onRemoveTrack(self) -> None:
|
||||
self.send_command_async(music.Command(
|
||||
target=self.project.id,
|
||||
remove_track=music.RemoveTrack(track_id=self.track.id)))
|
||||
|
||||
def purgePaintCaches(self) -> None:
|
||||
pass
|
||||
|
||||
def paint(self, painter: QtGui.QPainter, paint_rect: QtCore.QRect) -> None:
|
||||
if self.isCurrent():
|
||||
painter.fillRect(paint_rect, QtGui.QColor(240, 240, 255))
|
||||
else:
|
||||
painter.fillRect(paint_rect, Qt.white)
|
||||
|
||||
def enterEvent(self, evt: QtCore.QEvent) -> None:
|
||||
pass # pragma: no coverage
|
||||
|
||||
def leaveEvent(self, evt: QtCore.QEvent) -> None:
|
||||
pass # pragma: no coverage
|
||||
|
||||
def mousePressEvent(self, evt: QtGui.QMouseEvent) -> None:
|
||||
pass # pragma: no coverage
|
||||
|
||||
def mouseReleaseEvent(self, evt: QtGui.QMouseEvent) -> None:
|
||||
pass # pragma: no coverage
|
||||
|
||||
def mouseDoubleClickEvent(self, evt: QtGui.QMouseEvent) -> None:
|
||||
pass # pragma: no coverage
|
||||
|
||||
def mouseMoveEvent(self, evt: QtGui.QMouseEvent) -> None:
|
||||
pass # pragma: no coverage
|
||||
|
||||
def wheelEvent(self, evt: QtGui.QWheelEvent) -> None:
|
||||
pass # pragma: no coverage
|
||||
|
||||
def keyPressEvent(self, evt: QtGui.QKeyEvent) -> None:
|
||||
pass # pragma: no coverage
|
||||
|
||||
def keyReleaseEvent(self, evt: QtGui.QKeyEvent) -> None:
|
||||
pass # pragma: no coverage
|
||||
|
||||
|
||||
class BaseTrackEditorItem(BaseTrackItem):
|
||||
currentToolChanged = QtCore.pyqtSignal(tools.ToolType)
|
||||
|
||||
toolBoxClass = None # type: Type[tools.ToolBox]
|
||||
|
||||
def __init__(self, *, player_state: PlayerState, editor: Editor, **kwargs: Any) -> None:
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.__player_state = player_state
|
||||
self.__editor = editor
|
||||
|
||||
def toolBox(self) -> tools.ToolBox:
|
||||
tool_box = self.__editor.currentToolBox()
|
||||
assert isinstance(tool_box, self.toolBoxClass)
|
||||
return tool_box
|
||||
|
||||
def currentTool(self) -> tools.ToolBase:
|
||||
return self.toolBox().currentTool()
|
||||
|
||||
def currentToolType(self) -> tools.ToolType:
|
||||
return self.toolBox().currentToolType()
|
||||
|
||||
def toolBoxMatches(self) -> bool:
|
||||
return isinstance(self.__editor.currentToolBox(), self.toolBoxClass)
|
||||
|
||||
def playerState(self) -> PlayerState:
|
||||
return self.__player_state
|
||||
|
||||
def setPlaybackPos(self, time: audioproc.MusicalTime) -> None:
|
||||
pass
|
||||
|
||||
def mouseMoveEvent(self, evt: QtGui.QMouseEvent) -> None:
|
||||
if self.toolBoxMatches():
|
||||
self.toolBox().mouseMoveEvent(self, evt)
|
||||
|
||||
def mousePressEvent(self, evt: QtGui.QMouseEvent) -> None:
|
||||
if self.toolBoxMatches():
|
||||
self.toolBox().mousePressEvent(self, evt)
|
||||
|
||||
def mouseReleaseEvent(self, evt: QtGui.QMouseEvent) -> None:
|
||||
if self.toolBoxMatches():
|
||||
self.toolBox().mouseReleaseEvent(self, evt)
|
||||
|
||||
def mouseDoubleClickEvent(self, evt: QtGui.QMouseEvent) -> None:
|
||||
if self.toolBoxMatches():
|
||||
self.toolBox().mouseDoubleClickEvent(self, evt)
|
||||
|
||||
def wheelEvent(self, evt: QtGui.QWheelEvent) -> None:
|
||||
if self.toolBoxMatches():
|
||||
self.toolBox().wheelEvent(self, evt)
|
||||
|
||||
def keyPressEvent(self, evt: QtGui.QKeyEvent) -> None:
|
||||
if self.toolBoxMatches():
|
||||
self.toolBox().keyPressEvent(self, evt)
|
||||
|
||||
def keyReleaseEvent(self, evt: QtGui.QKeyEvent) -> None:
|
||||
if self.toolBoxMatches():
|
||||
self.toolBox().keyReleaseEvent(self, evt)
|
||||
|
||||
|
||||
class BaseMeasureEditorItem(ui_base.ProjectMixin, QtCore.QObject):
|
||||
class BaseMeasureEditor(ui_base.ProjectMixin, QtCore.QObject):
|
||||
rectChanged = QtCore.pyqtSignal(QtCore.QRect)
|
||||
|
||||
def __init__(self, track_item: BaseTrackEditorItem, **kwargs: Any) -> None:
|
||||
def __init__(self, track_editor: base_track_editor.BaseTrackEditor, **kwargs: Any) -> None:
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.__top_left = QtCore.QPoint()
|
||||
self.__playback_time = None # type: audioproc.MusicalTime
|
||||
self.__track_item = track_item
|
||||
self.__track_editor = track_editor
|
||||
|
||||
def close(self) -> None:
|
||||
pass
|
||||
|
||||
@property
|
||||
def track_item(self) -> BaseTrackEditorItem:
|
||||
return self.__track_item
|
||||
def track_editor(self) -> base_track_editor.BaseTrackEditor:
|
||||
return self.__track_editor
|
||||
|
||||
@property
|
||||
def track(self) -> music.MeasuredTrack:
|
||||
return down_cast(music.MeasuredTrack, self.__track_item.track)
|
||||
return down_cast(music.MeasuredTrack, self.__track_editor.track)
|
||||
|
||||
@property
|
||||
def duration(self) -> audioproc.MusicalDuration:
|
||||
|
@ -263,14 +74,14 @@ class BaseMeasureEditorItem(ui_base.ProjectMixin, QtCore.QObject):
|
|||
|
||||
@property
|
||||
def index(self) -> int:
|
||||
for idx, mitem in enumerate(self.__track_item.measure_items()):
|
||||
if mitem is self:
|
||||
for idx, meditor in enumerate(self.__track_editor.measure_editors()):
|
||||
if meditor is self:
|
||||
return idx
|
||||
raise ValueError
|
||||
|
||||
@property
|
||||
def next_sibling(self) -> 'BaseMeasureEditorItem':
|
||||
return self.__track_item.measure_items()[self.index + 1]
|
||||
def next_sibling(self) -> 'BaseMeasureEditor':
|
||||
return self.__track_editor.measure_editors()[self.index + 1]
|
||||
|
||||
def topLeft(self) -> QtCore.QPoint:
|
||||
return self.__top_left
|
||||
|
@ -279,13 +90,13 @@ class BaseMeasureEditorItem(ui_base.ProjectMixin, QtCore.QObject):
|
|||
self.__top_left = QtCore.QPoint(pos)
|
||||
|
||||
def scaleX(self) -> fractions.Fraction:
|
||||
return self.track_item.scaleX()
|
||||
return self.track_editor.scaleX()
|
||||
|
||||
def width(self) -> int:
|
||||
raise NotImplementedError
|
||||
|
||||
def height(self) -> int:
|
||||
return self.track_item.height()
|
||||
return self.track_editor.height()
|
||||
|
||||
def size(self) -> QtCore.QSize:
|
||||
return QtCore.QSize(self.width(), self.height())
|
||||
|
@ -295,7 +106,7 @@ class BaseMeasureEditorItem(ui_base.ProjectMixin, QtCore.QObject):
|
|||
|
||||
def viewRect(self) -> QtCore.QRect:
|
||||
return QtCore.QRect(
|
||||
self.track_item.viewTopLeft() + self.topLeft(), self.size())
|
||||
self.track_editor.viewTopLeft() + self.topLeft(), self.size())
|
||||
|
||||
def playbackPos(self) -> audioproc.MusicalTime:
|
||||
return self.__playback_time
|
||||
|
@ -324,7 +135,7 @@ class BaseMeasureEditorItem(ui_base.ProjectMixin, QtCore.QObject):
|
|||
pass
|
||||
|
||||
|
||||
class MeasureEditorItem(selection_set.Selectable, BaseMeasureEditorItem):
|
||||
class MeasureEditor(selection_set.Selectable, BaseMeasureEditor):
|
||||
selection_class = 'measure'
|
||||
|
||||
PLAYBACK_POS = 'playback_pos'
|
||||
|
@ -351,7 +162,7 @@ class MeasureEditorItem(selection_set.Selectable, BaseMeasureEditorItem):
|
|||
if self.__measure is not None:
|
||||
self.addMeasureListeners()
|
||||
|
||||
self.track_item.hoveredMeasureChanged.connect(self.__onHoveredMeasureChanged)
|
||||
self.track_editor.hoveredMeasureChanged.connect(self.__onHoveredMeasureChanged)
|
||||
|
||||
def close(self) -> None:
|
||||
if self.selected():
|
||||
|
@ -361,7 +172,7 @@ class MeasureEditorItem(selection_set.Selectable, BaseMeasureEditorItem):
|
|||
listener.remove()
|
||||
self.measure_listeners.clear()
|
||||
|
||||
self.track_item.hoveredMeasureChanged.disconnect(self.__onHoveredMeasureChanged)
|
||||
self.track_editor.hoveredMeasureChanged.disconnect(self.__onHoveredMeasureChanged)
|
||||
|
||||
super().close()
|
||||
|
||||
|
@ -416,16 +227,14 @@ class MeasureEditorItem(selection_set.Selectable, BaseMeasureEditorItem):
|
|||
|
||||
def onInsertMeasure(self) -> None:
|
||||
self.send_command_async(music.Command(
|
||||
target=self.project.id,
|
||||
target=self.track.id,
|
||||
insert_measure=music.InsertMeasure(
|
||||
tracks=[self.track.id],
|
||||
pos=self.measure_reference.index)))
|
||||
|
||||
def onRemoveMeasure(self) -> None:
|
||||
self.send_command_async(music.Command(
|
||||
target=self.project.id,
|
||||
target=self.track.id,
|
||||
remove_measure=music.RemoveMeasure(
|
||||
tracks=[self.track.index],
|
||||
pos=self.measure_reference.index)))
|
||||
|
||||
def setSelected(self, selected: bool) -> None:
|
||||
|
@ -496,7 +305,7 @@ class MeasureEditorItem(selection_set.Selectable, BaseMeasureEditorItem):
|
|||
painter.drawPixmap(0, 0, self.__paint_caches[layer])
|
||||
|
||||
|
||||
class Appendix(BaseMeasureEditorItem):
|
||||
class Appendix(BaseMeasureEditor):
|
||||
def __init__(self, **kwargs: Any) -> None:
|
||||
super().__init__(**kwargs)
|
||||
|
||||
|
@ -549,99 +358,101 @@ class MeasuredToolBase(tools.ToolBase): # pylint: disable=abstract-method
|
|||
def __init__(self, **kwargs: Any) -> None:
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.__mouse_grabber = None # type: BaseMeasureEditorItem
|
||||
self.__mouse_grabber = None # type: BaseMeasureEditor
|
||||
self.__mouse_pos = None # type: QtCore.QPoint
|
||||
|
||||
def _measureItemUnderMouse(self, target: Any) -> BaseMeasureEditorItem:
|
||||
def _measureEditorUnderMouse(self, target: Any) -> BaseMeasureEditor:
|
||||
if self.__mouse_pos is None:
|
||||
return None
|
||||
return target.measureItemAt(self.__mouse_pos)
|
||||
return target.measureEditorAt(self.__mouse_pos)
|
||||
|
||||
def __makeMouseEvent(
|
||||
self, measure_item: BaseMeasureEditorItem, evt: QtGui.QMouseEvent
|
||||
self, measure_editor: BaseMeasureEditor, evt: QtGui.QMouseEvent
|
||||
) -> QtGui.QMouseEvent:
|
||||
return QtGui.QMouseEvent(
|
||||
measure_evt = QtGui.QMouseEvent(
|
||||
evt.type(),
|
||||
evt.localPos() - measure_item.topLeft(),
|
||||
evt.localPos() - measure_editor.topLeft(),
|
||||
evt.windowPos(),
|
||||
evt.screenPos(),
|
||||
evt.button(),
|
||||
evt.buttons(),
|
||||
evt.modifiers())
|
||||
measure_evt.setAccepted(False)
|
||||
return measure_evt
|
||||
|
||||
def _mousePressEvent(self, target: Any, evt: QtGui.QMouseEvent) -> None:
|
||||
assert isinstance(target, MeasuredTrackEditorItem), type(target).__name__
|
||||
assert isinstance(target, MeasuredTrackEditor), type(target).__name__
|
||||
|
||||
self.__mouse_pos = evt.pos()
|
||||
|
||||
measure_item = target.measureItemAt(evt.pos())
|
||||
if isinstance(measure_item, MeasureEditorItem):
|
||||
measure_evt = self.__makeMouseEvent(measure_item, evt)
|
||||
self.mousePressEvent(measure_item, measure_evt)
|
||||
measure_editor = target.measureEditorAt(evt.pos())
|
||||
if isinstance(measure_editor, MeasureEditor):
|
||||
measure_evt = self.__makeMouseEvent(measure_editor, evt)
|
||||
self.mousePressEvent(measure_editor, measure_evt)
|
||||
if measure_evt.isAccepted():
|
||||
self.__mouse_grabber = measure_item
|
||||
self.__mouse_grabber = measure_editor
|
||||
evt.setAccepted(measure_evt.isAccepted())
|
||||
|
||||
elif isinstance(measure_item, Appendix):
|
||||
if measure_item.clickRect().contains(evt.pos() - measure_item.topLeft()):
|
||||
elif isinstance(measure_editor, Appendix):
|
||||
if measure_editor.clickRect().contains(evt.pos() - measure_editor.topLeft()):
|
||||
self.send_command_async(music.Command(
|
||||
target=target.project.id,
|
||||
insert_measure=music.InsertMeasure(tracks=[], pos=-1)))
|
||||
target=target.track.id,
|
||||
insert_measure=music.InsertMeasure(pos=-1)))
|
||||
evt.accept()
|
||||
return
|
||||
|
||||
def _mouseReleaseEvent(self, target: Any, evt: QtGui.QMouseEvent) -> None:
|
||||
assert isinstance(target, MeasuredTrackEditorItem), type(target).__name__
|
||||
assert isinstance(target, MeasuredTrackEditor), type(target).__name__
|
||||
|
||||
self.__mouse_pos = evt.pos()
|
||||
|
||||
if isinstance(self.__mouse_grabber, MeasureEditorItem):
|
||||
if isinstance(self.__mouse_grabber, MeasureEditor):
|
||||
measure_evt = self.__makeMouseEvent(self.__mouse_grabber, evt)
|
||||
self.mouseReleaseEvent(self.__mouse_grabber, measure_evt)
|
||||
self.__mouse_grabber = None
|
||||
evt.setAccepted(measure_evt.isAccepted())
|
||||
target.setHoverMeasureItem(target.measureItemAt(evt.pos()), evt)
|
||||
target.setHoverMeasureEditor(target.measureEditorAt(evt.pos()), evt)
|
||||
|
||||
def _mouseMoveEvent(self, target: Any, evt: QtGui.QMouseEvent) -> None:
|
||||
assert isinstance(target, MeasuredTrackEditorItem), type(target).__name__
|
||||
assert isinstance(target, MeasuredTrackEditor), type(target).__name__
|
||||
|
||||
self.__mouse_pos = evt.pos()
|
||||
|
||||
if self.__mouse_grabber is not None:
|
||||
measure_item = self.__mouse_grabber
|
||||
measure_editor = self.__mouse_grabber
|
||||
else:
|
||||
measure_item = target.measureItemAt(evt.pos())
|
||||
target.setHoverMeasureItem(measure_item, evt)
|
||||
measure_editor = target.measureEditorAt(evt.pos())
|
||||
target.setHoverMeasureEditor(measure_editor, evt)
|
||||
|
||||
if isinstance(measure_item, MeasureEditorItem):
|
||||
measure_evt = self.__makeMouseEvent(measure_item, evt)
|
||||
self.mouseMoveEvent(measure_item, measure_evt)
|
||||
if isinstance(measure_editor, MeasureEditor):
|
||||
measure_evt = self.__makeMouseEvent(measure_editor, evt)
|
||||
self.mouseMoveEvent(measure_editor, measure_evt)
|
||||
evt.setAccepted(measure_evt.isAccepted())
|
||||
|
||||
elif isinstance(measure_item, Appendix):
|
||||
measure_item.setHover(
|
||||
measure_item.clickRect().contains(evt.pos() - measure_item.topLeft()))
|
||||
elif isinstance(measure_editor, Appendix):
|
||||
measure_editor.setHover(
|
||||
measure_editor.clickRect().contains(evt.pos() - measure_editor.topLeft()))
|
||||
|
||||
def _mouseDoubleClickEvent(self, target: Any, evt: QtGui.QMouseEvent) -> None:
|
||||
assert isinstance(target, MeasuredTrackEditorItem), type(target).__name__
|
||||
assert isinstance(target, MeasuredTrackEditor), type(target).__name__
|
||||
|
||||
self.__mouse_pos = evt.pos()
|
||||
|
||||
measure_item = target.measureItemAt(evt.pos())
|
||||
if isinstance(measure_item, MeasureEditorItem):
|
||||
measure_evt = self.__makeMouseEvent(measure_item, evt)
|
||||
self.mouseDoubleClickEvent(measure_item, measure_evt)
|
||||
measure_editor = target.measureEditorAt(evt.pos())
|
||||
if isinstance(measure_editor, MeasureEditor):
|
||||
measure_evt = self.__makeMouseEvent(measure_editor, evt)
|
||||
self.mouseDoubleClickEvent(measure_editor, measure_evt)
|
||||
evt.setAccepted(measure_evt.isAccepted())
|
||||
|
||||
def _wheelEvent(self, target: Any, evt: QtGui.QWheelEvent) -> None:
|
||||
assert isinstance(target, MeasuredTrackEditorItem), type(target).__name__
|
||||
assert isinstance(target, MeasuredTrackEditor), type(target).__name__
|
||||
|
||||
self.__mouse_pos = evt.pos()
|
||||
|
||||
measure_item = target.measureItemAt(evt.pos())
|
||||
if isinstance(measure_item, MeasureEditorItem):
|
||||
measure_editor = target.measureEditorAt(evt.pos())
|
||||
if isinstance(measure_editor, MeasureEditor):
|
||||
measure_evt = QtGui.QWheelEvent(
|
||||
evt.pos() - measure_item.topLeft(),
|
||||
evt.pos() - measure_editor.topLeft(),
|
||||
evt.globalPos(),
|
||||
evt.pixelDelta(),
|
||||
evt.angleDelta(),
|
||||
|
@ -651,12 +462,13 @@ class MeasuredToolBase(tools.ToolBase): # pylint: disable=abstract-method
|
|||
evt.modifiers(),
|
||||
evt.phase(),
|
||||
evt.source())
|
||||
self.wheelEvent(measure_item, measure_evt)
|
||||
measure_evt.setAccepted(False)
|
||||
self.wheelEvent(measure_editor, measure_evt)
|
||||
evt.setAccepted(measure_evt.isAccepted())
|
||||
|
||||
def __makeKeyEvent(
|
||||
self, measure_item: BaseMeasureEditorItem, evt: QtGui.QKeyEvent) -> QtGui.QKeyEvent:
|
||||
return QtGui.QKeyEvent(
|
||||
self, measure_editor: BaseMeasureEditor, evt: QtGui.QKeyEvent) -> QtGui.QKeyEvent:
|
||||
measure_evt = QtGui.QKeyEvent(
|
||||
evt.type(),
|
||||
evt.key(),
|
||||
evt.modifiers(),
|
||||
|
@ -666,19 +478,21 @@ class MeasuredToolBase(tools.ToolBase): # pylint: disable=abstract-method
|
|||
evt.text(),
|
||||
evt.isAutoRepeat(),
|
||||
evt.count())
|
||||
measure_evt.setAccepted(False)
|
||||
return measure_evt
|
||||
|
||||
def _keyPressEvent(self, target: Any, evt: QtGui.QKeyEvent) -> None:
|
||||
measure_item = self._measureItemUnderMouse(target)
|
||||
if isinstance(measure_item, MeasureEditorItem):
|
||||
measure_evt = self.__makeKeyEvent(measure_item, evt)
|
||||
self.keyPressEvent(measure_item, measure_evt)
|
||||
measure_editor = self._measureEditorUnderMouse(target)
|
||||
if isinstance(measure_editor, MeasureEditor):
|
||||
measure_evt = self.__makeKeyEvent(measure_editor, evt)
|
||||
self.keyPressEvent(measure_editor, measure_evt)
|
||||
evt.setAccepted(measure_evt.isAccepted())
|
||||
|
||||
def _keyReleaseEvent(self, target: Any, evt: QtGui.QKeyEvent) -> None:
|
||||
measure_item = self._measureItemUnderMouse(target)
|
||||
if isinstance(measure_item, MeasureEditorItem):
|
||||
measure_evt = self.__makeKeyEvent(measure_item, evt)
|
||||
self.keyReleaseEvent(measure_item, measure_evt)
|
||||
measure_editor = self._measureEditorUnderMouse(target)
|
||||
if isinstance(measure_editor, MeasureEditor):
|
||||
measure_evt = self.__makeKeyEvent(measure_editor, evt)
|
||||
self.keyReleaseEvent(measure_editor, measure_evt)
|
||||
evt.setAccepted(measure_evt.isAccepted())
|
||||
|
||||
|
||||
|
@ -689,23 +503,23 @@ class ArrangeMeasuresTool(tools.ToolBase):
|
|||
group=tools.ToolGroup.ARRANGE,
|
||||
**kwargs)
|
||||
|
||||
self.__selection_first = None # type: MeasureEditorItem
|
||||
self.__selection_last = None # type: MeasureEditorItem
|
||||
self.__selection_first = None # type: MeasureEditor
|
||||
self.__selection_last = None # type: MeasureEditor
|
||||
|
||||
def iconName(self) -> str:
|
||||
return 'pointer'
|
||||
|
||||
def mousePressEvent(self, target: Any, evt: QtGui.QMouseEvent) -> None:
|
||||
assert isinstance(target, MeasuredTrackEditorItem), type(target).__name__
|
||||
assert isinstance(target, MeasuredTrackEditor), type(target).__name__
|
||||
|
||||
if evt.button() == Qt.LeftButton and evt.modifiers() == Qt.NoModifier:
|
||||
measure_item = target.measureItemAt(evt.pos())
|
||||
measure_editor = target.measureEditorAt(evt.pos())
|
||||
|
||||
if isinstance(measure_item, MeasureEditorItem):
|
||||
if isinstance(measure_editor, MeasureEditor):
|
||||
self.selection_set.clear()
|
||||
|
||||
self.selection_set.add(measure_item)
|
||||
self.__selection_first = measure_item
|
||||
self.selection_set.add(measure_editor)
|
||||
self.__selection_first = measure_editor
|
||||
self.__selection_last = None
|
||||
evt.accept()
|
||||
return
|
||||
|
@ -714,7 +528,7 @@ class ArrangeMeasuresTool(tools.ToolBase):
|
|||
super().mousePressEvent(target, evt)
|
||||
|
||||
def mouseReleaseEvent(self, target: Any, evt: QtGui.QMouseEvent) -> None:
|
||||
assert isinstance(target, MeasuredTrackEditorItem), type(target).__name__
|
||||
assert isinstance(target, MeasuredTrackEditor), type(target).__name__
|
||||
|
||||
if evt.button() == Qt.LeftButton:
|
||||
self.__selection_first = None
|
||||
|
@ -725,28 +539,28 @@ class ArrangeMeasuresTool(tools.ToolBase):
|
|||
super().mouseReleaseEvent(target, evt)
|
||||
|
||||
def mouseMoveEvent(self, target: Any, evt: QtGui.QMouseEvent) -> None:
|
||||
assert isinstance(target, MeasuredTrackEditorItem), type(target).__name__
|
||||
measure_item = target.measureItemAt(evt.pos())
|
||||
target.setHoverMeasureItem(measure_item, evt)
|
||||
assert isinstance(target, MeasuredTrackEditor), type(target).__name__
|
||||
measure_editor = target.measureEditorAt(evt.pos())
|
||||
target.setHoverMeasureEditor(measure_editor, evt)
|
||||
|
||||
if self.__selection_first is not None and isinstance(measure_item, MeasureEditorItem):
|
||||
if self.__selection_first is not None and isinstance(measure_editor, MeasureEditor):
|
||||
start_idx = self.__selection_first.measure_reference.index
|
||||
last_idx = measure_item.measure_reference.index
|
||||
last_idx = measure_editor.measure_reference.index
|
||||
|
||||
if start_idx > last_idx:
|
||||
start_idx, last_idx = last_idx, start_idx
|
||||
|
||||
for mitem in itertools.islice(target.measure_items(), start_idx, last_idx + 1):
|
||||
if isinstance(mitem, MeasureEditorItem) and not mitem.selected():
|
||||
self.selection_set.add(mitem)
|
||||
for meditor in itertools.islice(target.measure_editors(), start_idx, last_idx + 1):
|
||||
if isinstance(meditor, MeasureEditor) and not meditor.selected():
|
||||
self.selection_set.add(meditor)
|
||||
|
||||
for sitem in list(self.selection_set):
|
||||
assert isinstance(sitem, MeasureEditorItem)
|
||||
if (not (start_idx <= sitem.measure_reference.index <= last_idx)
|
||||
and sitem.selected()):
|
||||
self.selection_set.remove(sitem)
|
||||
for seditor in list(self.selection_set):
|
||||
assert isinstance(seditor, MeasureEditor)
|
||||
if (not (start_idx <= seditor.measure_reference.index <= last_idx)
|
||||
and seditor.selected()):
|
||||
self.selection_set.remove(seditor)
|
||||
|
||||
self.__selection_last = measure_item
|
||||
self.__selection_last = measure_editor
|
||||
|
||||
evt.accept()
|
||||
return
|
||||
|
@ -754,26 +568,26 @@ class ArrangeMeasuresTool(tools.ToolBase):
|
|||
super().mouseMoveEvent(target, evt)
|
||||
|
||||
|
||||
class MeasuredTrackEditorItem(BaseTrackEditorItem):
|
||||
class MeasuredTrackEditor(base_track_editor.BaseTrackEditor):
|
||||
hoveredMeasureChanged = QtCore.pyqtSignal(int)
|
||||
|
||||
measure_item_cls = None # type: Type[MeasureEditorItem]
|
||||
measure_editor_cls = None # type: Type[MeasureEditor]
|
||||
|
||||
def __init__(self, **kwargs: Any) -> None:
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.__closing = False
|
||||
self.__listeners = [] # type: List[core.Listener]
|
||||
self.__measure_item_at_playback_pos = None # type: BaseMeasureEditorItem
|
||||
self.__hover_measure_item = None # type: BaseMeasureEditorItem
|
||||
self.__measure_editor_at_playback_pos = None # type: BaseMeasureEditor
|
||||
self.__hover_measure_editor = None # type: BaseMeasureEditor
|
||||
|
||||
self.__measure_items = [] # type: List[BaseMeasureEditorItem]
|
||||
self.__measure_editors = [] # type: List[BaseMeasureEditor]
|
||||
for idx, mref in enumerate(self.track.measure_list):
|
||||
self.addMeasure(idx, mref)
|
||||
|
||||
appendix_item = Appendix(track_item=self, context=self.context)
|
||||
appendix_item.rectChanged.connect(self.rectChanged)
|
||||
self.__measure_items.append(appendix_item)
|
||||
appendix_editor = Appendix(track_editor=self, context=self.context)
|
||||
appendix_editor.rectChanged.connect(self.rectChanged)
|
||||
self.__measure_editors.append(appendix_editor)
|
||||
|
||||
self.__listeners.append(self.track.measure_list_changed.add(self.onMeasureListChanged))
|
||||
|
||||
|
@ -786,7 +600,7 @@ class MeasuredTrackEditorItem(BaseTrackEditorItem):
|
|||
listener.remove()
|
||||
self.__listeners.clear()
|
||||
|
||||
while len(self.__measure_items) > 0:
|
||||
while len(self.__measure_editors) > 0:
|
||||
self.removeMeasure(0)
|
||||
|
||||
super().close()
|
||||
|
@ -795,8 +609,8 @@ class MeasuredTrackEditorItem(BaseTrackEditorItem):
|
|||
def track(self) -> music.MeasuredTrack:
|
||||
return down_cast(music.MeasuredTrack, super().track)
|
||||
|
||||
def measure_items(self) -> List[BaseMeasureEditorItem]:
|
||||
return self.__measure_items
|
||||
def measure_editors(self) -> List[BaseMeasureEditor]:
|
||||
return self.__measure_editors
|
||||
|
||||
def onMeasureListChanged(
|
||||
self, change: model.PropertyListChange[music.MeasureReference]) -> None:
|
||||
|
@ -810,17 +624,17 @@ class MeasuredTrackEditorItem(BaseTrackEditorItem):
|
|||
raise TypeError(type(change))
|
||||
|
||||
def addMeasure(self, idx: int, mref: music.MeasureReference) -> None:
|
||||
measure_item = self.measure_item_cls( # pylint: disable=not-callable
|
||||
track_item=self, measure_reference=mref, context=self.context)
|
||||
measure_item.rectChanged.connect(self.rectChanged)
|
||||
self.__measure_items.insert(idx, measure_item)
|
||||
measure_editor = self.measure_editor_cls( # pylint: disable=not-callable
|
||||
track_editor=self, measure_reference=mref, context=self.context)
|
||||
measure_editor.rectChanged.connect(self.rectChanged)
|
||||
self.__measure_editors.insert(idx, measure_editor)
|
||||
self.updateMeasures()
|
||||
self.rectChanged.emit(self.viewRect())
|
||||
|
||||
def removeMeasure(self, idx: int) -> None:
|
||||
measure_item = self.__measure_items.pop(idx)
|
||||
measure_item.close()
|
||||
measure_item.rectChanged.disconnect(self.rectChanged)
|
||||
measure_editor = self.__measure_editors.pop(idx)
|
||||
measure_editor.close()
|
||||
measure_editor.rectChanged.disconnect(self.rectChanged)
|
||||
self.updateMeasures()
|
||||
self.rectChanged.emit(self.viewRect())
|
||||
|
||||
|
@ -829,9 +643,9 @@ class MeasuredTrackEditorItem(BaseTrackEditorItem):
|
|||
return
|
||||
|
||||
p = QtCore.QPoint(10, 0)
|
||||
for measure_item in self.measure_items():
|
||||
measure_item.setTopLeft(p)
|
||||
p += QtCore.QPoint(measure_item.width(), 0)
|
||||
for measure_editor in self.measure_editors():
|
||||
measure_editor.setTopLeft(p)
|
||||
p += QtCore.QPoint(measure_editor.width(), 0)
|
||||
|
||||
self.setWidth(p.x() + 10)
|
||||
|
||||
|
@ -840,41 +654,40 @@ class MeasuredTrackEditorItem(BaseTrackEditorItem):
|
|||
self.updateMeasures()
|
||||
|
||||
def setPlaybackPos(self, time: audioproc.MusicalTime) -> None:
|
||||
if self.__measure_item_at_playback_pos is not None:
|
||||
self.__measure_item_at_playback_pos.clearPlaybackPos()
|
||||
self.__measure_item_at_playback_pos = None
|
||||
if self.__measure_editor_at_playback_pos is not None:
|
||||
self.__measure_editor_at_playback_pos.clearPlaybackPos()
|
||||
self.__measure_editor_at_playback_pos = None
|
||||
|
||||
measure_time = audioproc.MusicalTime()
|
||||
for measure_item in self.measure_items():
|
||||
if measure_time <= time < measure_time + measure_item.duration:
|
||||
measure_item.setPlaybackPos(time - measure_time)
|
||||
self.__measure_item_at_playback_pos = measure_item
|
||||
for measure_editor in self.measure_editors():
|
||||
if measure_time <= time < measure_time + measure_editor.duration:
|
||||
measure_editor.setPlaybackPos(time - measure_time)
|
||||
self.__measure_editor_at_playback_pos = measure_editor
|
||||
break
|
||||
measure_time += measure_item.duration
|
||||
measure_time += measure_editor.duration
|
||||
|
||||
def measureItemAt(self, pos: QtCore.QPoint) -> BaseMeasureEditorItem:
|
||||
def measureEditorAt(self, pos: QtCore.QPoint) -> BaseMeasureEditor:
|
||||
p = QtCore.QPoint(10, 0)
|
||||
for measure_item in self.measure_items():
|
||||
if p.x() <= pos.x() < p.x() + measure_item.width():
|
||||
return measure_item
|
||||
for measure_editor in self.measure_editors():
|
||||
if p.x() <= pos.x() < p.x() + measure_editor.width():
|
||||
return measure_editor
|
||||
|
||||
p += QtCore.QPoint(measure_item.width(), 0)
|
||||
p += QtCore.QPoint(measure_editor.width(), 0)
|
||||
|
||||
return None
|
||||
|
||||
def buildContextMenu(self, menu: QtWidgets.QMenu, pos: QtCore.QPoint) -> None:
|
||||
super().buildContextMenu(menu, pos)
|
||||
|
||||
measure_item = self.measureItemAt(pos)
|
||||
if measure_item is not None:
|
||||
measure_item.buildContextMenu(
|
||||
menu, pos - measure_item.topLeft())
|
||||
measure_editor = self.measureEditorAt(pos)
|
||||
if measure_editor is not None:
|
||||
measure_editor.buildContextMenu(
|
||||
menu, pos - measure_editor.topLeft())
|
||||
|
||||
def onInsertMeasure(self) -> None:
|
||||
self.send_command_async(music.Command(
|
||||
target=self.project.id,
|
||||
target=self.track.id,
|
||||
insert_measure=music.InsertMeasure(
|
||||
tracks=[self.track.id],
|
||||
pos=self.measure_reference.index)))
|
||||
|
||||
def onRemoveMeasure(self) -> None:
|
||||
|
@ -884,55 +697,55 @@ class MeasuredTrackEditorItem(BaseTrackEditorItem):
|
|||
tracks=[self.track.index],
|
||||
pos=self.measure_reference.index)))
|
||||
|
||||
def setHoverMeasureItem(
|
||||
self, measure_item: Optional[BaseMeasureEditorItem],
|
||||
def setHoverMeasureEditor(
|
||||
self, measure_editor: Optional[BaseMeasureEditor],
|
||||
evt: Union[None, QtGui.QEnterEvent, QtGui.QMouseEvent]
|
||||
) -> None:
|
||||
if measure_item is self.__hover_measure_item:
|
||||
if measure_editor is self.__hover_measure_editor:
|
||||
return
|
||||
|
||||
if self.__hover_measure_item is not None:
|
||||
if self.__hover_measure_editor is not None:
|
||||
measure_evt = QtCore.QEvent(QtCore.QEvent.Leave)
|
||||
self.__hover_measure_item.leaveEvent(measure_evt)
|
||||
self.__hover_measure_editor.leaveEvent(measure_evt)
|
||||
|
||||
if measure_item is not None:
|
||||
if measure_editor is not None:
|
||||
measure_evt = QtGui.QEnterEvent(
|
||||
evt.localPos() - measure_item.topLeft(),
|
||||
evt.localPos() - measure_editor.topLeft(),
|
||||
evt.windowPos(),
|
||||
evt.screenPos())
|
||||
measure_item.enterEvent(measure_evt)
|
||||
measure_editor.enterEvent(measure_evt)
|
||||
|
||||
self.__hover_measure_item = measure_item
|
||||
self.__hover_measure_editor = measure_editor
|
||||
self.hoveredMeasureChanged.emit(
|
||||
measure_item.measure_reference.measure.id
|
||||
if self.isCurrent() and isinstance(measure_item, MeasureEditorItem)
|
||||
measure_editor.measure_reference.measure.id
|
||||
if self.isCurrent() and isinstance(measure_editor, MeasureEditor)
|
||||
else 0)
|
||||
|
||||
def enterEvent(self, evt: QtCore.QEvent) -> None:
|
||||
evt = down_cast(QtGui.QEnterEvent, evt)
|
||||
self.setHoverMeasureItem(self.measureItemAt(evt.pos()), evt)
|
||||
self.setHoverMeasureEditor(self.measureEditorAt(evt.pos()), evt)
|
||||
|
||||
def leaveEvent(self, evt: QtCore.QEvent) -> None:
|
||||
self.setHoverMeasureItem(None, None)
|
||||
self.setHoverMeasureEditor(None, None)
|
||||
|
||||
def purgePaintCaches(self) -> None:
|
||||
super().purgePaintCaches()
|
||||
for measure_item in self.measure_items():
|
||||
measure_item.purgePaintCaches()
|
||||
for measure_editor in self.measure_editors():
|
||||
measure_editor.purgePaintCaches()
|
||||
|
||||
def paint(self, painter: QtGui.QPainter, paint_rect: QtCore.QRect) -> None:
|
||||
super().paint(painter, paint_rect)
|
||||
|
||||
p = QtCore.QPoint(10, 0)
|
||||
for measure_item in self.measure_items():
|
||||
measure_rect = QtCore.QRect(p.x(), 0, measure_item.width(), self.height())
|
||||
for measure_editor in self.measure_editors():
|
||||
measure_rect = QtCore.QRect(p.x(), 0, measure_editor.width(), self.height())
|
||||
measure_rect = measure_rect.intersected(paint_rect)
|
||||
if not measure_rect.isEmpty():
|
||||
painter.save()
|
||||
try:
|
||||
painter.setClipRect(measure_rect)
|
||||
painter.translate(p)
|
||||
measure_item.paint(painter, measure_rect.translated(-p))
|
||||
measure_editor.paint(painter, measure_rect.translated(-p))
|
||||
finally:
|
||||
painter.restore()
|
||||
p += QtCore.QPoint(measure_item.width(), 0)
|
||||
p += QtCore.QPoint(measure_editor.width(), 0)
|
|
@ -36,8 +36,9 @@ from noisicaa import core # pylint: disable=unused-import
|
|||
from noisicaa import music
|
||||
from noisicaa import model
|
||||
from noisicaa.model import project_pb2
|
||||
from noisicaa.ui import tools
|
||||
from . import base_track_item
|
||||
from . import base_track_editor
|
||||
from . import time_view_mixin
|
||||
from . import tools
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -57,7 +58,7 @@ class EditSamplesTool(tools.ToolBase):
|
|||
return 'edit-samples'
|
||||
|
||||
def mousePressEvent(self, target: Any, evt: QtGui.QMouseEvent) -> None:
|
||||
assert isinstance(target, SampleTrackEditorItem), type(target).__name__
|
||||
assert isinstance(target, SampleTrackEditor), type(target).__name__
|
||||
|
||||
if (evt.button() == Qt.LeftButton
|
||||
and evt.modifiers() == Qt.NoModifier
|
||||
|
@ -89,7 +90,7 @@ class EditSamplesTool(tools.ToolBase):
|
|||
super().mousePressEvent(target, evt)
|
||||
|
||||
def mouseMoveEvent(self, target: Any, evt: QtGui.QMouseEvent) -> None:
|
||||
assert isinstance(target, SampleTrackEditorItem), type(target).__name__
|
||||
assert isinstance(target, SampleTrackEditor), type(target).__name__
|
||||
|
||||
if self.__moving_sample is not None:
|
||||
new_pos = QtCore.QPoint(
|
||||
|
@ -111,7 +112,7 @@ class EditSamplesTool(tools.ToolBase):
|
|||
super().mouseMoveEvent(target, evt)
|
||||
|
||||
def mouseReleaseEvent(self, target: Any, evt: QtGui.QMouseEvent) -> None:
|
||||
assert isinstance(target, SampleTrackEditorItem), type(target).__name__
|
||||
assert isinstance(target, SampleTrackEditor), type(target).__name__
|
||||
|
||||
if evt.button() == Qt.LeftButton and self.__moving_sample is not None:
|
||||
pos = self.__moving_sample.pos()
|
||||
|
@ -137,15 +138,15 @@ class SampleTrackToolBox(tools.ToolBox):
|
|||
|
||||
|
||||
class SampleItem(object):
|
||||
def __init__(self, track_item: 'SampleTrackEditorItem', sample: music.SampleRef) -> None:
|
||||
self.__track_item = track_item
|
||||
def __init__(self, track_editor: 'SampleTrackEditor', sample: music.SampleRef) -> None:
|
||||
self.__track_editor = track_editor
|
||||
self.__sample = sample
|
||||
|
||||
self.__render_result = ('init', ) # type: Tuple[Any, ...]
|
||||
self.__highlighted = False
|
||||
|
||||
self.__pos = QtCore.QPoint(
|
||||
self.__track_item.timeToX(self.__sample.time), 0)
|
||||
self.__track_editor.timeToX(self.__sample.time), 0)
|
||||
self.__width = 50
|
||||
|
||||
self.__listeners = [
|
||||
|
@ -162,13 +163,13 @@ class SampleItem(object):
|
|||
return self.__sample.id
|
||||
|
||||
def scaleX(self) -> fractions.Fraction:
|
||||
return self.__track_item.scaleX()
|
||||
return self.__track_editor.scaleX()
|
||||
|
||||
def width(self) -> int:
|
||||
return self.__width
|
||||
|
||||
def height(self) -> int:
|
||||
return self.__track_item.height()
|
||||
return self.__track_editor.height()
|
||||
|
||||
def size(self) -> QtCore.QSize:
|
||||
return QtCore.QSize(self.width(), self.height())
|
||||
|
@ -184,14 +185,14 @@ class SampleItem(object):
|
|||
|
||||
def onTimeChanged(self, change: model.PropertyValueChange[audioproc.MusicalTime]) -> None:
|
||||
self.__pos = QtCore.QPoint(
|
||||
self.__track_item.timeToX(change.new_value), 0)
|
||||
self.__track_item.rectChanged.emit(self.__track_item.viewRect())
|
||||
self.__track_editor.timeToX(change.new_value), 0)
|
||||
self.__track_editor.rectChanged.emit(self.__track_editor.viewRect())
|
||||
|
||||
def setHighlighted(self, highlighted: bool) -> None:
|
||||
if highlighted != self.__highlighted:
|
||||
self.__highlighted = highlighted
|
||||
self.__track_item.rectChanged.emit(
|
||||
self.rect().translated(self.__track_item.viewTopLeft()))
|
||||
self.__track_editor.rectChanged.emit(
|
||||
self.rect().translated(self.__track_editor.viewTopLeft()))
|
||||
|
||||
def renderSample(self, render_result: Tuple[Any, ...]) -> None:
|
||||
status, *args = render_result
|
||||
|
@ -210,12 +211,12 @@ class SampleItem(object):
|
|||
self.__width = len(samples)
|
||||
|
||||
self.__render_result = render_result
|
||||
self.__track_item.rectChanged.emit(self.__track_item.viewRect())
|
||||
self.__track_editor.rectChanged.emit(self.__track_editor.viewRect())
|
||||
|
||||
def purgePaintCaches(self) -> None:
|
||||
self.__render_result = ('init', )
|
||||
self.__pos = QtCore.QPoint(
|
||||
self.__track_item.timeToX(self.__sample.time), 0)
|
||||
self.__track_editor.timeToX(self.__sample.time), 0)
|
||||
self.__width = 50
|
||||
|
||||
def paint(self, painter: QtGui.QPainter, paint_rect: QtCore.QRect) -> None:
|
||||
|
@ -224,7 +225,7 @@ class SampleItem(object):
|
|||
if status in ('init', 'waiting'):
|
||||
if status == 'init':
|
||||
scale_x = self.scaleX()
|
||||
self.__track_item.send_command_async(
|
||||
self.__track_editor.send_command_async(
|
||||
music.Command(
|
||||
target=self.__sample.id,
|
||||
render_sample=music.RenderSample(
|
||||
|
@ -289,7 +290,7 @@ class SampleItem(object):
|
|||
painter.drawLine(x, ycenter - h // 2, x, ycenter + h // 2)
|
||||
|
||||
|
||||
class SampleTrackEditorItem(base_track_item.BaseTrackEditorItem):
|
||||
class SampleTrackEditor(time_view_mixin.ContinuousTimeMixin, base_track_editor.BaseTrackEditor):
|
||||
toolBoxClass = SampleTrackToolBox
|
||||
|
||||
def __init__(self, **kwargs: Any) -> None:
|
||||
|
@ -307,7 +308,7 @@ class SampleTrackEditorItem(base_track_item.BaseTrackEditorItem):
|
|||
|
||||
self.__listeners.append(self.track.samples_changed.add(self.onSamplesChanged))
|
||||
|
||||
self.updateSize()
|
||||
self.setHeight(120)
|
||||
|
||||
@property
|
||||
def track(self) -> music.SampleTrack:
|
||||
|
@ -324,45 +325,6 @@ class SampleTrackEditorItem(base_track_item.BaseTrackEditorItem):
|
|||
|
||||
super().close()
|
||||
|
||||
def updateSize(self) -> None:
|
||||
width = 20
|
||||
for mref in self.project.property_track.measure_list:
|
||||
measure = mref.measure
|
||||
width += int(self.scaleX() * measure.duration.fraction)
|
||||
self.setSize(QtCore.QSize(width, 120))
|
||||
|
||||
def timeToX(self, time: audioproc.MusicalTime) -> int:
|
||||
x = 10
|
||||
for mref in self.project.property_track.measure_list:
|
||||
measure = mref.measure
|
||||
width = int(self.scaleX() * measure.duration.fraction)
|
||||
|
||||
if time - measure.duration <= audioproc.MusicalTime(0, 1):
|
||||
return x + int(width * (time / measure.duration).fraction)
|
||||
|
||||
x += width
|
||||
time -= measure.duration
|
||||
|
||||
return x
|
||||
|
||||
def xToTime(self, x: int) -> audioproc.MusicalTime:
|
||||
x -= 10
|
||||
time = audioproc.MusicalTime(0, 1)
|
||||
if x <= 0:
|
||||
return time
|
||||
|
||||
for mref in self.project.property_track.measure_list:
|
||||
measure = mref.measure
|
||||
width = int(self.scaleX() * measure.duration.fraction)
|
||||
|
||||
if x <= width:
|
||||
return time + measure.duration * fractions.Fraction(int(x), width)
|
||||
|
||||
time += measure.duration
|
||||
x -= width
|
||||
|
||||
return time
|
||||
|
||||
def onSamplesChanged(self, change: model.PropertyListChange[music.SampleRef]) -> None:
|
||||
if isinstance(change, model.PropertyListInsert):
|
||||
self.addSample(change.index, change.new_value)
|
||||
|
@ -374,7 +336,7 @@ class SampleTrackEditorItem(base_track_item.BaseTrackEditorItem):
|
|||
raise TypeError(type(change))
|
||||
|
||||
def addSample(self, insert_index: int, sample: music.SampleRef) -> None:
|
||||
item = SampleItem(track_item=self, sample=sample)
|
||||
item = SampleItem(track_editor=self, sample=sample)
|
||||
self.__samples.insert(insert_index, item)
|
||||
self.rectChanged.emit(self.viewRect())
|
||||
|
||||
|
@ -491,27 +453,24 @@ class SampleTrackEditorItem(base_track_item.BaseTrackEditorItem):
|
|||
|
||||
painter.setPen(QtGui.QColor(160, 160, 160))
|
||||
painter.drawLine(
|
||||
10, self.height() // 2,
|
||||
self.width() - 11, self.height() // 2)
|
||||
self.timeToX(audioproc.MusicalTime(0, 1)), self.height() // 2,
|
||||
self.timeToX(self.projectEndTime()), self.height() // 2)
|
||||
|
||||
x = 10
|
||||
for mref in self.project.property_track.measure_list:
|
||||
measure = down_cast(music.PropertyMeasure, mref.measure)
|
||||
width = int(self.scaleX() * measure.duration.fraction)
|
||||
beat_time = audioproc.MusicalTime()
|
||||
beat_num = 0
|
||||
while beat_time < self.projectEndTime():
|
||||
x = self.timeToX(beat_time)
|
||||
|
||||
if x + width > paint_rect.x() and x < paint_rect.x() + paint_rect.width():
|
||||
if mref.is_first:
|
||||
painter.fillRect(x, 0, 2, self.height(), QtGui.QColor(160, 160, 160))
|
||||
else:
|
||||
painter.fillRect(x, 0, 1, self.height(), QtGui.QColor(160, 160, 160))
|
||||
if beat_num == 0:
|
||||
painter.fillRect(x, 0, 2, self.height(), Qt.black)
|
||||
else:
|
||||
painter.fillRect(x, 0, 1, self.height(), QtGui.QColor(160, 160, 160))
|
||||
|
||||
for i in range(1, measure.time_signature.upper):
|
||||
pos = int(width * i / measure.time_signature.lower)
|
||||
painter.fillRect(x + pos, 0, 1, self.height(), QtGui.QColor(200, 200, 200))
|
||||
beat_time += audioproc.MusicalDuration(1, 4)
|
||||
beat_num += 1
|
||||
|
||||
x += width
|
||||
|
||||
painter.fillRect(x - 2, 0, 2, self.height(), QtGui.QColor(160, 160, 160))
|
||||
x = self.timeToX(self.projectEndTime())
|
||||
painter.fillRect(x, 0, 2, self.height(), Qt.black)
|
||||
|
||||
for item in self.__samples:
|
||||
sample_rect = item.rect().intersected(paint_rect)
|
|
@ -0,0 +1,45 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
# @begin:license
|
||||
#
|
||||
# Copyright (c) 2015-2018, 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 noisidev import uitest
|
||||
# from noisicaa import music
|
||||
# from . import sample_track_item
|
||||
# from . import track_item_tests
|
||||
|
||||
|
||||
# class SampleTrackEditorItemTest(track_item_tests.TrackEditorItemTestMixin, uitest.UITestCase):
|
||||
# async def setup_testcase(self):
|
||||
# await self.project_client.send_command(music.Command(
|
||||
# target=self.project.id,
|
||||
# add_track=music.AddTrack(
|
||||
# track_type='sample',
|
||||
# parent_group_id=self.project.master_group.id)))
|
||||
|
||||
# self.tool_box = sample_track_item.SampleTrackToolBox(context=self.context)
|
||||
|
||||
# def _createTrackItem(self, **kwargs):
|
||||
# return sample_track_item.SampleTrackEditorItem(
|
||||
# track=self.project.master_group.tracks[0],
|
||||
# player_state=self.player_state,
|
||||
# editor=self.editor,
|
||||
# context=self.context,
|
||||
# **kwargs)
|
|
@ -37,13 +37,13 @@ from noisicaa import model
|
|||
from noisicaa import music
|
||||
from noisicaa.bindings import lv2
|
||||
from noisicaa.ui import svg_symbol
|
||||
from noisicaa.ui import tools
|
||||
from . import base_track_item
|
||||
from . import tools
|
||||
from . import measured_track_editor
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ScoreToolBase(base_track_item.MeasuredToolBase):
|
||||
class ScoreToolBase(measured_track_editor.MeasuredToolBase):
|
||||
def __init__(self, *, icon_name: str, hotspot: Tuple[int, int], **kwargs: Any) -> None:
|
||||
super().__init__(**kwargs)
|
||||
|
||||
|
@ -63,11 +63,11 @@ class ScoreToolBase(base_track_item.MeasuredToolBase):
|
|||
def cursor(self) -> QtGui.QCursor:
|
||||
return self.__cursor
|
||||
|
||||
def _updateGhost(self, target: 'ScoreMeasureEditorItem', pos: QtCore.QPoint) -> None:
|
||||
def _updateGhost(self, target: 'ScoreMeasureEditor', pos: QtCore.QPoint) -> None:
|
||||
target.setGhost(None)
|
||||
|
||||
def mouseMoveEvent(self, target: Any, evt: QtGui.QMouseEvent) -> None:
|
||||
assert isinstance(target, ScoreMeasureEditorItem), type(target).__name__
|
||||
assert isinstance(target, ScoreMeasureEditor), type(target).__name__
|
||||
|
||||
self._updateGhost(target, evt.pos())
|
||||
|
||||
|
@ -121,7 +121,7 @@ class InsertNoteTool(ScoreToolBase):
|
|||
}[type],
|
||||
**kwargs)
|
||||
|
||||
def _updateGhost(self, target: 'ScoreMeasureEditorItem', pos: QtCore.QPoint) -> None:
|
||||
def _updateGhost(self, target: 'ScoreMeasureEditor', pos: QtCore.QPoint) -> None:
|
||||
if pos is None:
|
||||
target.setGhost(None)
|
||||
return
|
||||
|
@ -140,7 +140,7 @@ class InsertNoteTool(ScoreToolBase):
|
|||
ymid - 10 * (stave_line - target.measure.clef.center_pitch.stave_line)))
|
||||
|
||||
def mousePressEvent(self, target: Any, evt: QtGui.QMouseEvent) -> None:
|
||||
assert isinstance(target, ScoreMeasureEditorItem), type(target).__name__
|
||||
assert isinstance(target, ScoreMeasureEditor), type(target).__name__
|
||||
|
||||
ymid = target.height() // 2
|
||||
stave_line = (
|
||||
|
@ -182,13 +182,13 @@ class InsertNoteTool(ScoreToolBase):
|
|||
cmd = music.Command(
|
||||
target=target.measure.id,
|
||||
add_pitch=music.AddPitch(idx=idx, pitch=pitch))
|
||||
target.track_item.playNoteOn(model.Pitch(pitch))
|
||||
target.track_editor.playNoteOn(model.Pitch(pitch))
|
||||
else:
|
||||
cmd = music.Command(
|
||||
target=target.measure.id,
|
||||
insert_note=music.InsertNote(
|
||||
idx=idx, pitch=pitch, duration=duration.to_proto()))
|
||||
target.track_item.playNoteOn(model.Pitch(pitch))
|
||||
target.track_editor.playNoteOn(model.Pitch(pitch))
|
||||
|
||||
if cmd is not None:
|
||||
self.send_command_async(cmd)
|
||||
|
@ -198,9 +198,9 @@ class InsertNoteTool(ScoreToolBase):
|
|||
super().mousePressEvent(target, evt)
|
||||
|
||||
def mouseReleaseEvent(self, target: Any, evt: QtGui.QMouseEvent) -> None:
|
||||
assert isinstance(target, ScoreMeasureEditorItem), type(target).__name__
|
||||
assert isinstance(target, ScoreMeasureEditor), type(target).__name__
|
||||
|
||||
target.track_item.playNoteOff()
|
||||
target.track_editor.playNoteOff()
|
||||
return super().mouseReleaseEvent(target, evt)
|
||||
|
||||
|
||||
|
@ -231,7 +231,7 @@ class ModifyNoteTool(ScoreToolBase):
|
|||
}[type],
|
||||
**kwargs)
|
||||
|
||||
def _updateGhost(self, target: 'ScoreMeasureEditorItem', pos: QtCore.QPoint) -> None:
|
||||
def _updateGhost(self, target: 'ScoreMeasureEditor', pos: QtCore.QPoint) -> None:
|
||||
if pos is None:
|
||||
target.setGhost(None)
|
||||
return
|
||||
|
@ -250,7 +250,7 @@ class ModifyNoteTool(ScoreToolBase):
|
|||
ymid - 10 * (stave_line - target.measure.clef.center_pitch.stave_line)))
|
||||
|
||||
def mousePressEvent(self, target: Any, evt: QtGui.QMouseEvent) -> None:
|
||||
assert isinstance(target, ScoreMeasureEditorItem), type(target).__name__
|
||||
assert isinstance(target, ScoreMeasureEditor), type(target).__name__
|
||||
ymid = target.height() // 2
|
||||
stave_line = (
|
||||
int(ymid + 5 - evt.pos().y()) // 10 + target.measure.clef.center_pitch.stave_line)
|
||||
|
@ -340,7 +340,7 @@ class ScoreToolBox(tools.ToolBox):
|
|||
def __init__(self, **kwargs: Any) -> None:
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.addTool(base_track_item.ArrangeMeasuresTool(context=self.context))
|
||||
self.addTool(measured_track_editor.ArrangeMeasuresTool(context=self.context))
|
||||
self.addTool(InsertNoteTool(type=tools.ToolType.NOTE_WHOLE, context=self.context))
|
||||
self.addTool(InsertNoteTool(type=tools.ToolType.NOTE_HALF, context=self.context))
|
||||
self.addTool(InsertNoteTool(type=tools.ToolType.NOTE_QUARTER, context=self.context))
|
||||
|
@ -531,14 +531,14 @@ class ScoreToolBox(tools.ToolBox):
|
|||
# super().keyPressEvent(evt)
|
||||
|
||||
|
||||
class ScoreMeasureEditorItem(base_track_item.MeasureEditorItem):
|
||||
class ScoreMeasureEditor(measured_track_editor.MeasureEditor):
|
||||
FOREGROUND = 'fg'
|
||||
BACKGROUND = 'bg'
|
||||
GHOST = 'ghost'
|
||||
|
||||
layers = [
|
||||
BACKGROUND,
|
||||
base_track_item.MeasureEditorItem.PLAYBACK_POS,
|
||||
measured_track_editor.MeasureEditor.PLAYBACK_POS,
|
||||
FOREGROUND,
|
||||
GHOST,
|
||||
]
|
||||
|
@ -551,7 +551,7 @@ class ScoreMeasureEditorItem(base_track_item.MeasureEditorItem):
|
|||
self.__mouse_pos = None # type: QtCore.QPoint
|
||||
self.__ghost_pos = None # type: QtCore.QPoint
|
||||
|
||||
self.track_item.currentToolChanged.connect(
|
||||
self.track_editor.currentToolChanged.connect(
|
||||
lambda _: self.updateGhost(self.__mouse_pos))
|
||||
|
||||
@property
|
||||
|
@ -861,7 +861,7 @@ class ScoreMeasureEditorItem(base_track_item.MeasureEditorItem):
|
|||
|
||||
ymid = self.height() // 2
|
||||
|
||||
tool = self.track_item.currentToolType()
|
||||
tool = self.track_editor.currentToolType()
|
||||
pos = self.__ghost_pos
|
||||
painter.setOpacity(0.4)
|
||||
|
||||
|
@ -973,7 +973,7 @@ class ScoreMeasureEditorItem(base_track_item.MeasureEditorItem):
|
|||
self.setGhost(None)
|
||||
return
|
||||
|
||||
tool = self.track_item.currentToolType()
|
||||
tool = self.track_editor.currentToolType()
|
||||
if tool.is_note or tool.is_rest:
|
||||
self.setGhost(
|
||||
QtCore.QPoint(
|
||||
|
@ -995,8 +995,8 @@ class ScoreMeasureEditorItem(base_track_item.MeasureEditorItem):
|
|||
super().leaveEvent(evt)
|
||||
|
||||
|
||||
class ScoreTrackEditorItem(base_track_item.MeasuredTrackEditorItem):
|
||||
measure_item_cls = ScoreMeasureEditorItem
|
||||
class ScoreTrackEditor(measured_track_editor.MeasuredTrackEditor):
|
||||
measure_editor_cls = ScoreMeasureEditor
|
||||
|
||||
toolBoxClass = ScoreToolBox
|
||||
|
||||
|
@ -1009,23 +1009,23 @@ class ScoreTrackEditorItem(base_track_item.MeasuredTrackEditorItem):
|
|||
def buildContextMenu(self, menu: QtWidgets.QMenu, pos: QtCore.QPoint) -> None:
|
||||
super().buildContextMenu(menu, pos)
|
||||
|
||||
affected_measure_items = [] # type: List[ScoreMeasureEditorItem]
|
||||
affected_measure_editors = [] # type: List[ScoreMeasureEditor]
|
||||
if not self.selection_set.empty():
|
||||
affected_measure_items.extend(
|
||||
down_cast(ScoreMeasureEditorItem, sitem) for sitem in self.selection_set)
|
||||
affected_measure_editors.extend(
|
||||
down_cast(ScoreMeasureEditor, seditor) for seditor in self.selection_set)
|
||||
else:
|
||||
mitem = self.measureItemAt(pos)
|
||||
if isinstance(mitem, ScoreMeasureEditorItem):
|
||||
affected_measure_items.append(mitem)
|
||||
meditor = self.measureEditorAt(pos)
|
||||
if isinstance(meditor, ScoreMeasureEditor):
|
||||
affected_measure_editors.append(meditor)
|
||||
|
||||
enable_measure_actions = bool(affected_measure_items)
|
||||
enable_measure_actions = bool(affected_measure_editors)
|
||||
|
||||
clef_menu = menu.addMenu("Set clef")
|
||||
for clef in model.Clef:
|
||||
clef_action = QtWidgets.QAction(clef.value, menu)
|
||||
clef_action.setEnabled(enable_measure_actions)
|
||||
clef_action.triggered.connect(
|
||||
lambda _, clef=clef: self.onSetClef(affected_measure_items, clef))
|
||||
lambda _, clef=clef: self.onSetClef(affected_measure_editors, clef))
|
||||
clef_menu.addAction(clef_action)
|
||||
|
||||
key_signature_menu = menu.addMenu("Set key signature")
|
||||
|
@ -1065,7 +1065,7 @@ class ScoreTrackEditorItem(base_track_item.MeasuredTrackEditorItem):
|
|||
key_signature_action = QtWidgets.QAction(key_signature, menu)
|
||||
key_signature_action.setEnabled(enable_measure_actions)
|
||||
key_signature_action.triggered.connect(
|
||||
lambda _, sig=key_signature: self.onSetKeySignature(affected_measure_items, sig))
|
||||
lambda _, sig=key_signature: self.onSetKeySignature(affected_measure_editors, sig))
|
||||
key_signature_menu.addAction(key_signature_action)
|
||||
|
||||
time_signature_menu = menu.addMenu("Set time signature")
|
||||
|
@ -1078,7 +1078,7 @@ class ScoreTrackEditorItem(base_track_item.MeasuredTrackEditorItem):
|
|||
time_signature_action.setEnabled(enable_measure_actions)
|
||||
time_signature_action.triggered.connect(
|
||||
lambda _, upper=upper, lower=lower: (
|
||||
self.onSetTimeSignature(affected_measure_items, upper, lower)))
|
||||
self.onSetTimeSignature(affected_measure_editors, upper, lower)))
|
||||
time_signature_menu.addAction(time_signature_action)
|
||||
|
||||
transpose_menu = menu.addMenu("Transpose")
|
||||
|
@ -1087,14 +1087,16 @@ class ScoreTrackEditorItem(base_track_item.MeasuredTrackEditorItem):
|
|||
octave_up_action.setEnabled(enable_measure_actions)
|
||||
octave_up_action.setShortcut('Ctrl+Shift+Up')
|
||||
octave_up_action.setShortcutContext(Qt.WidgetWithChildrenShortcut)
|
||||
octave_up_action.triggered.connect(lambda _: self.onTranspose(affected_measure_items, 12))
|
||||
octave_up_action.triggered.connect(
|
||||
lambda _: self.onTranspose(affected_measure_editors, 12))
|
||||
transpose_menu.addAction(octave_up_action)
|
||||
|
||||
halfnote_up_action = QtWidgets.QAction("Half-note up", self)
|
||||
halfnote_up_action.setEnabled(enable_measure_actions)
|
||||
halfnote_up_action.setShortcut('Ctrl+Up')
|
||||
halfnote_up_action.setShortcutContext(Qt.WidgetWithChildrenShortcut)
|
||||
halfnote_up_action.triggered.connect(lambda _: self.onTranspose(affected_measure_items, 1))
|
||||
halfnote_up_action.triggered.connect(
|
||||
lambda _: self.onTranspose(affected_measure_editors, 1))
|
||||
transpose_menu.addAction(halfnote_up_action)
|
||||
|
||||
halfnote_down_action = QtWidgets.QAction("Half-note down", self)
|
||||
|
@ -1102,7 +1104,7 @@ class ScoreTrackEditorItem(base_track_item.MeasuredTrackEditorItem):
|
|||
halfnote_down_action.setShortcut('Ctrl+Down')
|
||||
halfnote_down_action.setShortcutContext(Qt.WidgetWithChildrenShortcut)
|
||||
halfnote_down_action.triggered.connect(
|
||||
lambda _: self.onTranspose(affected_measure_items, -1))
|
||||
lambda _: self.onTranspose(affected_measure_editors, -1))
|
||||
transpose_menu.addAction(halfnote_down_action)
|
||||
|
||||
octave_down_action = QtWidgets.QAction("Octave down", self)
|
||||
|
@ -1110,42 +1112,42 @@ class ScoreTrackEditorItem(base_track_item.MeasuredTrackEditorItem):
|
|||
octave_down_action.setShortcut('Ctrl+Shift+Down')
|
||||
octave_down_action.setShortcutContext(Qt.WidgetWithChildrenShortcut)
|
||||
octave_down_action.triggered.connect(
|
||||
lambda _: self.onTranspose(affected_measure_items, -12))
|
||||
lambda _: self.onTranspose(affected_measure_editors, -12))
|
||||
transpose_menu.addAction(octave_down_action)
|
||||
|
||||
def onSetClef(
|
||||
self, affected_measure_items: List[ScoreMeasureEditorItem], clef: model.Clef) -> None:
|
||||
self, affected_measure_editors: List[ScoreMeasureEditor], clef: model.Clef) -> None:
|
||||
self.send_command_async(music.Command(
|
||||
target=self.track.id,
|
||||
set_clef=music.SetClef(
|
||||
measure_ids=[mitem.measure.id for mitem in affected_measure_items],
|
||||
measure_ids=[meditor.measure.id for meditor in affected_measure_editors],
|
||||
clef=clef.value)))
|
||||
|
||||
def onSetKeySignature(
|
||||
self, affected_measure_items: List[ScoreMeasureEditorItem],
|
||||
self, affected_measure_editors: List[ScoreMeasureEditor],
|
||||
key_signature: model.KeySignature) -> None:
|
||||
self.send_command_async(music.Command(
|
||||
target=self.track.id,
|
||||
set_key_signature=music.SetKeySignature(
|
||||
measure_ids=[mitem.measure.id for mitem in affected_measure_items],
|
||||
measure_ids=[meditor.measure.id for meditor in affected_measure_editors],
|
||||
key_signature=key_signature.to_proto())))
|
||||
|
||||
def onSetTimeSignature(
|
||||
self, affected_measure_items: List[ScoreMeasureEditorItem], upper: int, lower: int
|
||||
self, affected_measure_editors: List[ScoreMeasureEditor], upper: int, lower: int
|
||||
) -> None:
|
||||
self.send_command_async(music.Command(
|
||||
target=self.property_track.id,
|
||||
set_time_signature=music.SetTimeSignature(
|
||||
measure_ids=[
|
||||
self.property_track.measure_list[mitem.measure_reference.index].measure.id
|
||||
for mitem in affected_measure_items],
|
||||
self.property_track.measure_list[meditor.measure_reference.index].measure.id
|
||||
for meditor in affected_measure_editors],
|
||||
upper=upper, lower=lower)))
|
||||
|
||||
def onTranspose(
|
||||
self, affected_measure_items: List[ScoreMeasureEditorItem], half_notes: int) -> None:
|
||||
self, affected_measure_editors: List[ScoreMeasureEditor], half_notes: int) -> None:
|
||||
note_ids = set()
|
||||
for mitem in affected_measure_items:
|
||||
for note in mitem.measure.notes:
|
||||
for meditor in affected_measure_editors:
|
||||
for note in meditor.measure.notes:
|
||||
note_ids.add(note.id)
|
||||
|
||||
self.send_command_async(music.Command(
|
|
@ -20,26 +20,26 @@
|
|||
#
|
||||
# @end:license
|
||||
|
||||
from noisidev import uitest
|
||||
from noisicaa import music
|
||||
from . import control_track_item
|
||||
from . import track_item_tests
|
||||
# from noisidev import uitest
|
||||
# from noisicaa import music
|
||||
# from . import score_track_item
|
||||
# from . import track_item_tests
|
||||
|
||||
|
||||
class ControlTrackEditorItemTest(track_item_tests.TrackEditorItemTestMixin, uitest.UITestCase):
|
||||
async def setup_testcase(self):
|
||||
await self.project_client.send_command(music.Command(
|
||||
target=self.project.id,
|
||||
add_track=music.AddTrack(
|
||||
track_type='control',
|
||||
parent_group_id=self.project.master_group.id)))
|
||||
# class ScoreTrackEditorItemTest(track_item_tests.TrackEditorItemTestMixin, uitest.UITestCase):
|
||||
# async def setup_testcase(self):
|
||||
# await self.project_client.send_command(music.Command(
|
||||
# target=self.project.id,
|
||||
# add_track=music.AddTrack(
|
||||
# track_type='score',
|
||||
# parent_group_id=self.project.master_group.id)))
|
||||
|
||||
self.tool_box = control_track_item.ControlTrackToolBox(context=self.context)
|
||||
# self.tool_box = score_track_item.ScoreToolBox(context=self.context)
|
||||
|
||||
def _createTrackItem(self, **kwargs):
|
||||
return control_track_item.ControlTrackEditorItem(
|
||||
track=self.project.master_group.tracks[0],
|
||||
player_state=self.player_state,
|
||||
editor=self.editor,
|
||||
context=self.context,
|
||||
**kwargs)
|
||||
# def _createTrackItem(self, **kwargs):
|
||||
# return score_track_item.ScoreTrackEditorItem(
|
||||
# track=self.project.master_group.tracks[0],
|
||||
# player_state=self.player_state,
|
||||
# editor=self.editor,
|
||||
# context=self.context,
|
||||
# **kwargs)
|
|
@ -0,0 +1,241 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
# @begin:license
|
||||
#
|
||||
# Copyright (c) 2015-2018, 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, Optional, Union, Sequence, Dict, List, Tuple, Type # pylint: disable=unused-import
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5 import QtCore
|
||||
from PyQt5 import QtGui
|
||||
from PyQt5 import QtWidgets
|
||||
|
||||
from noisicaa import audioproc
|
||||
from noisicaa import model
|
||||
from noisicaa.ui import ui_base
|
||||
from noisicaa.ui import player_state as player_state_lib
|
||||
from . import time_view_mixin
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# TODO: fix cyclic dependency
|
||||
ProjectView = Any
|
||||
|
||||
|
||||
class TimeLine(
|
||||
time_view_mixin.ContinuousTimeMixin,
|
||||
time_view_mixin.TimeViewMixin,
|
||||
ui_base.ProjectMixin,
|
||||
QtWidgets.QWidget):
|
||||
def __init__(
|
||||
self, *,
|
||||
project_view: ProjectView,
|
||||
player_state: player_state_lib.PlayerState,
|
||||
**kwargs: Any
|
||||
) -> None:
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.setAttribute(Qt.WA_OpaquePaintEvent)
|
||||
self.setMinimumHeight(20)
|
||||
self.setMaximumHeight(20)
|
||||
|
||||
self.__project_view = project_view
|
||||
self.__player_state = player_state
|
||||
self.__player_id = None # type: str
|
||||
self.__move_time = False
|
||||
self.__old_player_state = None # type: bool
|
||||
|
||||
self.__player_state.currentTimeChanged.connect(self.onCurrentTimeChanged)
|
||||
self.__player_state.loopStartTimeChanged.connect(lambda _: self.update())
|
||||
self.__player_state.loopEndTimeChanged.connect(lambda _: self.update())
|
||||
|
||||
self.__duration_listener = self.project.duration_changed.add(self.onDurationChanged)
|
||||
|
||||
self.scaleXChanged.connect(lambda _: self.update())
|
||||
|
||||
def setPlayerID(self, player_id: str) -> None:
|
||||
self.__player_id = player_id
|
||||
|
||||
def onSetLoopStart(self, loop_start_time: audioproc.MusicalTime) -> None:
|
||||
self.call_async(
|
||||
self.project_client.update_player_state(
|
||||
self.__player_id,
|
||||
audioproc.PlayerState(loop_start_time=loop_start_time.to_proto())))
|
||||
|
||||
def onSetLoopEnd(self, loop_end_time: audioproc.MusicalTime) -> None:
|
||||
self.call_async(
|
||||
self.project_client.update_player_state(
|
||||
self.__player_id,
|
||||
audioproc.PlayerState(loop_end_time=loop_end_time.to_proto())))
|
||||
|
||||
def onClearLoop(self) -> None:
|
||||
pass
|
||||
|
||||
def onCurrentTimeChanged(self, current_time: audioproc.MusicalTime) -> None:
|
||||
if self.isVisible():
|
||||
x = self.timeToX(self.__player_state.currentTime())
|
||||
|
||||
left = self.xOffset() + 1 * self.width() // 5
|
||||
right = self.xOffset() + 4 * self.width() // 5
|
||||
if x < left:
|
||||
self.setXOffset(max(0, x - 1 * self.width() // 5))
|
||||
elif x > right:
|
||||
self.setXOffset(x - 4 * self.width() // 5)
|
||||
|
||||
self.update()
|
||||
|
||||
def onDurationChanged(self, change: model.PropertyValueChange[float]) -> None:
|
||||
self.update()
|
||||
|
||||
def mousePressEvent(self, evt: QtGui.QMouseEvent) -> None:
|
||||
if (self.__player_id is not None
|
||||
and evt.button() == Qt.LeftButton
|
||||
and evt.modifiers() == Qt.NoModifier):
|
||||
self.__move_time = True
|
||||
self.__old_player_state = self.__player_state.playing()
|
||||
x = evt.pos().x() + self.xOffset()
|
||||
current_time = self.xToTime(x)
|
||||
self.call_async(
|
||||
self.project_client.update_player_state(
|
||||
self.__player_id,
|
||||
audioproc.PlayerState(playing=False)))
|
||||
self.__project_view.setPlaybackPosMode('manual')
|
||||
self.__player_state.setCurrentTime(current_time)
|
||||
evt.accept()
|
||||
return
|
||||
|
||||
super().mousePressEvent(evt)
|
||||
|
||||
def mouseMoveEvent(self, evt: QtGui.QMouseEvent) -> None:
|
||||
if self.__move_time:
|
||||
x = evt.pos().x() + self.xOffset()
|
||||
current_time = min(self.xToTime(x), self.projectEndTime())
|
||||
self.__player_state.setCurrentTime(current_time)
|
||||
evt.accept()
|
||||
return
|
||||
|
||||
super().mouseMoveEvent(evt)
|
||||
|
||||
def mouseReleaseEvent(self, evt: QtGui.QMouseEvent) -> None:
|
||||
if self.__move_time and evt.button() == Qt.LeftButton and evt.modifiers() == Qt.NoModifier:
|
||||
self.__move_time = False
|
||||
x = evt.pos().x() + self.xOffset()
|
||||
current_time = min(self.xToTime(x), self.projectEndTime())
|
||||
self.call_async(
|
||||
self.project_client.update_player_state(
|
||||
self.__player_id,
|
||||
audioproc.PlayerState(
|
||||
playing=self.__old_player_state,
|
||||
current_time=current_time.to_proto())))
|
||||
self.__project_view.setPlaybackPosMode('follow')
|
||||
evt.accept()
|
||||
return
|
||||
|
||||
super().mouseReleaseEvent(evt)
|
||||
|
||||
def contextMenuEvent(self, evt: QtGui.QContextMenuEvent) -> None:
|
||||
menu = QtWidgets.QMenu()
|
||||
|
||||
if not self.__player_state.playing() and self.__player_state.currentTime() is not None:
|
||||
set_loop_start = QtWidgets.QAction("Set loop start", menu)
|
||||
set_loop_start.triggered.connect(
|
||||
lambda _: self.onSetLoopStart(self.__player_state.currentTime()))
|
||||
menu.addAction(set_loop_start)
|
||||
|
||||
set_loop_end = QtWidgets.QAction("Set loop end", menu)
|
||||
set_loop_end.triggered.connect(
|
||||
lambda _: self.onSetLoopEnd(self.__player_state.currentTime()))
|
||||
menu.addAction(set_loop_end)
|
||||
|
||||
clear_loop = QtWidgets.QAction("Clear loop", menu)
|
||||
clear_loop.triggered.connect(lambda _: self.onClearLoop())
|
||||
menu.addAction(clear_loop)
|
||||
|
||||
if not menu.isEmpty():
|
||||
menu.exec_(evt.globalPos())
|
||||
evt.accept()
|
||||
return
|
||||
|
||||
def paintEvent(self, evt: QtGui.QPaintEvent) -> None:
|
||||
super().paintEvent(evt)
|
||||
|
||||
painter = QtGui.QPainter(self)
|
||||
try:
|
||||
painter.fillRect(evt.rect(), Qt.white)
|
||||
|
||||
painter.setPen(Qt.black)
|
||||
painter.translate(-self.xOffset(), 0)
|
||||
|
||||
# beat markers
|
||||
beat_time = audioproc.MusicalTime()
|
||||
beat_num = 0
|
||||
while beat_time < self.projectEndTime():
|
||||
x = self.timeToX(beat_time)
|
||||
|
||||
if beat_num == 0:
|
||||
painter.fillRect(x, 0, 2, self.height(), Qt.black)
|
||||
else:
|
||||
painter.fillRect(x, 0, 1, self.height(), Qt.black)
|
||||
|
||||
if beat_num % 4 == 0:
|
||||
painter.drawText(x + 5, 12, '%d' % (beat_num + 1))
|
||||
|
||||
beat_time += audioproc.MusicalDuration(1, 4)
|
||||
beat_num += 1
|
||||
|
||||
x = self.timeToX(self.projectEndTime())
|
||||
painter.fillRect(x, 0, 2, self.height(), Qt.black)
|
||||
|
||||
# loop markers
|
||||
loop_start_time = self.__player_state.loopStartTime()
|
||||
if loop_start_time is not None:
|
||||
x = self.timeToX(loop_start_time)
|
||||
painter.setBrush(Qt.black)
|
||||
painter.setPen(Qt.NoPen)
|
||||
polygon = QtGui.QPolygon()
|
||||
polygon.append(QtCore.QPoint(x, 0))
|
||||
polygon.append(QtCore.QPoint(x + 7, 0))
|
||||
polygon.append(QtCore.QPoint(x + 2, 5))
|
||||
polygon.append(QtCore.QPoint(x + 2, self.height() - 5))
|
||||
polygon.append(QtCore.QPoint(x + 7, self.height()))
|
||||
polygon.append(QtCore.QPoint(x, self.height()))
|
||||
painter.drawPolygon(polygon)
|
||||
|
||||
loop_end_time = self.__player_state.loopEndTime()
|
||||
if loop_end_time is not None:
|
||||
x = self.timeToX(loop_end_time)
|
||||
painter.setBrush(Qt.black)
|
||||
painter.setPen(Qt.NoPen)
|
||||
polygon = QtGui.QPolygon()
|
||||
polygon.append(QtCore.QPoint(x - 6, 0))
|
||||
polygon.append(QtCore.QPoint(x + 2, 0))
|
||||
polygon.append(QtCore.QPoint(x + 2, self.height()))
|
||||
polygon.append(QtCore.QPoint(x - 6, self.height()))
|
||||
polygon.append(QtCore.QPoint(x, self.height() - 6))
|
||||
polygon.append(QtCore.QPoint(x, 6))
|
||||
painter.drawPolygon(polygon)
|
||||
|
||||
# playback time
|
||||
x = self.timeToX(self.__player_state.currentTime())
|
||||
painter.fillRect(x, 0, 2, self.height(), QtGui.QColor(0, 0, 160))
|
||||
|
||||
finally:
|
||||
painter.end()
|
|
@ -0,0 +1,144 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
# @begin:license
|
||||
#
|
||||
# Copyright (c) 2015-2018, 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
|
||||
import logging
|
||||
from typing import cast, Any, Optional, Union, Sequence, Dict, List, Tuple, Type # pylint: disable=unused-import
|
||||
|
||||
from PyQt5 import QtCore
|
||||
from PyQt5 import QtGui
|
||||
|
||||
from noisicaa import audioproc
|
||||
from noisicaa.ui import ui_base
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# This should be a subclass of QtCore.QObject, but PyQt5 doesn't support
|
||||
# multiple inheritance of QObjects.
|
||||
class ScaledTimeMixin(ui_base.ProjectMixin):
|
||||
scaleXChanged = QtCore.pyqtSignal(fractions.Fraction)
|
||||
contentWidthChanged = QtCore.pyqtSignal(int)
|
||||
|
||||
def __init__(self, **kwargs: Any) -> None:
|
||||
super().__init__(**kwargs)
|
||||
|
||||
# pixels per beat
|
||||
self.__scale_x = fractions.Fraction(500, 1)
|
||||
self.__content_width = 100
|
||||
self.project.duration_changed.add(lambda _: self.__updateContentWidth())
|
||||
|
||||
self.__updateContentWidth()
|
||||
|
||||
def __updateContentWidth(self) -> None:
|
||||
width = int(self.project.duration.fraction * self.__scale_x) + 120
|
||||
self.setContentWidth(width)
|
||||
|
||||
def projectEndTime(self) -> audioproc.MusicalTime:
|
||||
return audioproc.MusicalTime() + self.project.duration
|
||||
|
||||
def contentWidth(self) -> int:
|
||||
return self.__content_width
|
||||
|
||||
def setContentWidth(self, width: int) -> None:
|
||||
if width == self.__content_width:
|
||||
return
|
||||
|
||||
self.__content_width = width
|
||||
assert isinstance(self, QtCore.QObject)
|
||||
self.contentWidthChanged.emit(self.__content_width)
|
||||
|
||||
def scaleX(self) -> fractions.Fraction:
|
||||
return self.__scale_x
|
||||
|
||||
def setScaleX(self, scale_x: fractions.Fraction) -> None:
|
||||
if scale_x == self.__scale_x:
|
||||
return
|
||||
|
||||
self.__scale_x = scale_x
|
||||
self.__updateContentWidth()
|
||||
assert isinstance(self, QtCore.QObject)
|
||||
self.scaleXChanged.emit(self.__scale_x)
|
||||
|
||||
|
||||
class ContinuousTimeMixin(ScaledTimeMixin):
|
||||
def timeToX(self, time: audioproc.MusicalTime) -> int:
|
||||
return 10 + int(self.scaleX() * time.fraction)
|
||||
|
||||
def xToTime(self, x: int) -> audioproc.MusicalTime:
|
||||
x -= 10
|
||||
if x <= 0:
|
||||
return audioproc.MusicalTime(0, 1)
|
||||
|
||||
return audioproc.MusicalTime(x / self.scaleX())
|
||||
|
||||
|
||||
# TODO: This should really be a subclass of QtWidgets.QWidget, but somehow this screws up the
|
||||
# signals... Because of that, there are a bunch of 'type: ignore' overrides below.
|
||||
class TimeViewMixin(ScaledTimeMixin):
|
||||
maximumXOffsetChanged = QtCore.pyqtSignal(int)
|
||||
xOffsetChanged = QtCore.pyqtSignal(int)
|
||||
pageWidthChanged = QtCore.pyqtSignal(int)
|
||||
|
||||
def __init__(self, **kwargs: Any) -> None:
|
||||
super().__init__(**kwargs)
|
||||
|
||||
# pixels per beat
|
||||
self.__x_offset = 0
|
||||
|
||||
self.setMinimumWidth(100) # type: ignore
|
||||
|
||||
assert isinstance(self, QtCore.QObject)
|
||||
self.contentWidthChanged.connect(self.__contentWidthChanged)
|
||||
|
||||
def __contentWidthChanged(self, width: int) -> None:
|
||||
assert isinstance(self, QtCore.QObject)
|
||||
self.maximumXOffsetChanged.emit(self.maximumXOffset())
|
||||
self.setXOffset(min(self.xOffset(), self.maximumXOffset()))
|
||||
|
||||
def maximumXOffset(self) -> int:
|
||||
return max(0, self.contentWidth() - self.width()) # type: ignore
|
||||
|
||||
def pageWidth(self) -> int:
|
||||
return self.width() # type: ignore
|
||||
|
||||
def xOffset(self) -> int:
|
||||
return self.__x_offset
|
||||
|
||||
def setXOffset(self, offset: int) -> None:
|
||||
offset = max(0, min(offset, self.maximumXOffset()))
|
||||
if offset == self.__x_offset:
|
||||
return
|
||||
|
||||
dx = self.__x_offset - offset
|
||||
self.__x_offset = offset
|
||||
assert isinstance(self, QtCore.QObject)
|
||||
self.xOffsetChanged.emit(self.__x_offset)
|
||||
|
||||
self.scroll(dx, 0) # type: ignore
|
||||
|
||||
def resizeEvent(self, evt: QtGui.QResizeEvent) -> None:
|
||||
super().resizeEvent(evt) # type: ignore
|
||||
|
||||
assert isinstance(self, QtCore.QObject)
|
||||
self.maximumXOffsetChanged.emit(self.maximumXOffset())
|
||||
self.pageWidthChanged.emit(self.width()) # type: ignore
|
|
@ -23,38 +23,26 @@
|
|||
import logging
|
||||
from typing import Any, Optional
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5 import QtCore
|
||||
from PyQt5 import QtGui
|
||||
from PyQt5 import QtWidgets
|
||||
|
||||
from . import flowlayout
|
||||
from . import dock_widget
|
||||
from noisicaa.ui import flowlayout
|
||||
from noisicaa.ui import ui_base
|
||||
from . import tools
|
||||
from . import ui_base
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ToolsDockWidget(ui_base.ProjectMixin, dock_widget.DockWidget):
|
||||
class Toolbox(ui_base.ProjectMixin, QtWidgets.QWidget):
|
||||
def __init__(self, **kwargs: Any) -> None:
|
||||
super().__init__(
|
||||
identifier='tools',
|
||||
title="Tools",
|
||||
allowed_areas=Qt.AllDockWidgetAreas,
|
||||
initial_area=Qt.RightDockWidgetArea,
|
||||
initial_visible=True,
|
||||
**kwargs)
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.__tool_box = None # type: Optional[tools.ToolBox]
|
||||
|
||||
self.__group = QtWidgets.QButtonGroup()
|
||||
self.__group.buttonClicked.connect(self.__onButtonClicked)
|
||||
|
||||
self.__main_area = QtWidgets.QWidget()
|
||||
self.__main_area.setContentsMargins(QtCore.QMargins(0, 0, 0, 0))
|
||||
self.setWidget(self.__main_area)
|
||||
|
||||
def __onButtonClicked(self, button: QtWidgets.QAbstractButton) -> None:
|
||||
assert self.__tool_box is not None
|
||||
tool_type = tools.ToolType(self.__group.id(button))
|
||||
|
@ -66,7 +54,7 @@ class ToolsDockWidget(ui_base.ProjectMixin, dock_widget.DockWidget):
|
|||
button.setChecked(True)
|
||||
|
||||
def setCurrentToolBox(self, tool_box: Optional[tools.ToolBox]) -> None:
|
||||
logger.debug("Updating tool dock for tool_box=%s", type(tool_box).__name__)
|
||||
logger.error("Using tool_box=%s", type(tool_box).__name__)
|
||||
|
||||
if self.__tool_box is not None:
|
||||
self.__tool_box.toolTypeChanged.disconnect(self.__onToolTypeChanged)
|
||||
|
@ -78,10 +66,12 @@ class ToolsDockWidget(ui_base.ProjectMixin, dock_widget.DockWidget):
|
|||
|
||||
for button in self.__group.buttons():
|
||||
self.__group.removeButton(button)
|
||||
if self.__main_area.layout() is not None:
|
||||
QtWidgets.QWidget().setLayout(self.__main_area.layout())
|
||||
if self.layout() is not None:
|
||||
QtWidgets.QWidget().setLayout(self.layout())
|
||||
|
||||
layout = flowlayout.FlowLayout(spacing=1)
|
||||
layout = flowlayout.FlowLayout()
|
||||
layout.setContentsMargins(2, 2, 2, 2)
|
||||
layout.setSpacing(2)
|
||||
|
||||
if self.__tool_box is not None:
|
||||
for tool in self.__tool_box.tools():
|
||||
|
@ -94,5 +84,5 @@ class ToolsDockWidget(ui_base.ProjectMixin, dock_widget.DockWidget):
|
|||
self.__group.addButton(button, tool.type.value)
|
||||
layout.addWidget(button)
|
||||
|
||||
self.__main_area.setLayout(layout)
|
||||
layout.doLayout(self.__main_area.geometry(), False)
|
||||
self.setLayout(layout)
|
||||
layout.doLayout(self.geometry(), False)
|
|
@ -30,7 +30,7 @@ from PyQt5 import QtCore
|
|||
from PyQt5 import QtGui
|
||||
|
||||
from noisicaa import constants
|
||||
from . import ui_base
|
||||
from noisicaa.ui import ui_base
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
|
@ -33,8 +33,7 @@ from PyQt5 import QtGui
|
|||
from PyQt5 import QtWidgets
|
||||
|
||||
from noisidev import uitest
|
||||
from noisicaa import music
|
||||
from noisicaa.ui import project_view
|
||||
from noisicaa.ui import player_state
|
||||
|
||||
|
||||
class HIDState(object):
|
||||
|
@ -239,7 +238,7 @@ class ReleaseKey(Event):
|
|||
|
||||
class TrackEditorItemTestMixin(uitest.ProjectMixin, uitest.UITestCase):
|
||||
async def setup_testcase(self):
|
||||
self.player_state = project_view.PlayerState(context=self.context)
|
||||
self.player_state = player_state.PlayerState(context=self.context)
|
||||
self.tool_box = None
|
||||
self.editor = mock.Mock()
|
||||
self.editor.currentToolBox.side_effect = lambda: self.tool_box
|
||||
|
@ -350,16 +349,6 @@ class TrackEditorItemTestMixin(uitest.ProjectMixin, uitest.UITestCase):
|
|||
ReleaseKey(Qt.Key_Shift),
|
||||
])
|
||||
|
||||
def test_onRemoveTrack(self):
|
||||
with self._trackItem() as ti:
|
||||
track_id = ti.track.id
|
||||
ti.onRemoveTrack()
|
||||
self.assertEqual(
|
||||
self.commands,
|
||||
[music.Command(
|
||||
target=self.project.id,
|
||||
remove_track=music.RemoveTrack(track_id=track_id))])
|
||||
|
||||
def test_buildContextMenu(self):
|
||||
with self._trackItem() as ti:
|
||||
menu = QtWidgets.QMenu()
|
|
@ -0,0 +1,186 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
# @begin:license
|
||||
#
|
||||
# Copyright (c) 2015-2018, 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
|
||||
import logging
|
||||
import time as time_lib
|
||||
from typing import cast, Any, Optional, Union, Sequence, Dict, List, Tuple, Type # pylint: disable=unused-import
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5 import QtCore
|
||||
from PyQt5 import QtWidgets
|
||||
|
||||
from noisicaa import music
|
||||
from noisicaa.ui import ui_base
|
||||
from noisicaa.ui import slots
|
||||
from noisicaa.ui import player_state as player_state_lib
|
||||
from . import editor
|
||||
from . import time_line
|
||||
from . import toolbox
|
||||
from . import tools
|
||||
|
||||
# TODO: fix cyclic dependency
|
||||
ProjectView = Any
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Frame(QtWidgets.QFrame):
|
||||
def __init__(self, parent: Optional[QtWidgets.QWidget]) -> None:
|
||||
super().__init__(parent)
|
||||
|
||||
self.setFrameStyle(QtWidgets.QFrame.Sunken | QtWidgets.QFrame.Panel)
|
||||
self.__layout = QtWidgets.QVBoxLayout()
|
||||
self.__layout.setSizeConstraint(QtWidgets.QLayout.SetMinAndMaxSize)
|
||||
self.__layout.setContentsMargins(1, 1, 1, 1)
|
||||
self.setLayout(self.__layout)
|
||||
|
||||
def setWidget(self, widget: QtWidgets.QWidget) -> None:
|
||||
self.__layout.addWidget(widget, 1)
|
||||
|
||||
|
||||
class TrackListView(ui_base.ProjectMixin, slots.SlotContainer, QtWidgets.QSplitter):
|
||||
currentToolBoxChanged = QtCore.pyqtSignal(tools.ToolBox)
|
||||
playingChanged = QtCore.pyqtSignal(bool)
|
||||
loopEnabledChanged = QtCore.pyqtSignal(bool)
|
||||
|
||||
currentTrack, setCurrentTrack, currentTrackChanged = slots.slot(
|
||||
music.Track, 'currentTrack')
|
||||
|
||||
def __init__(
|
||||
self, *,
|
||||
project_view: ProjectView,
|
||||
player_state: player_state_lib.PlayerState,
|
||||
**kwargs: Any
|
||||
) -> None:
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.__project_view = project_view
|
||||
self.__player_state = player_state
|
||||
|
||||
self.__session_prefix = 'tracklist:%s:' % self.project.id
|
||||
self.__session_data_last_update = {} # type: Dict[str, float]
|
||||
|
||||
editor_frame = Frame(self)
|
||||
self.__editor = editor.Editor(
|
||||
player_state=self.__player_state,
|
||||
parent=editor_frame, context=self.context)
|
||||
editor_frame.setWidget(self.__editor)
|
||||
|
||||
self.__editor.currentTrackChanged.connect(self.setCurrentTrack)
|
||||
self.currentTrackChanged.connect(self.__editor.setCurrentTrack)
|
||||
|
||||
self.__editor.setScaleX(self.__get_session_value('scale_x', self.__editor.scaleX()))
|
||||
self.__editor.setXOffset(self.__get_session_value('x_offset', 0))
|
||||
self.__editor.setYOffset(self.__get_session_value('y_offset', 0))
|
||||
|
||||
self.__editor.currentToolBoxChanged.connect(self.currentToolBoxChanged)
|
||||
self.__editor.scaleXChanged.connect(self.__updateScaleX)
|
||||
|
||||
time_line_frame = Frame(self)
|
||||
self.__time_line = time_line.TimeLine(
|
||||
project_view=self.__project_view, player_state=self.__player_state,
|
||||
parent=time_line_frame, context=self.context)
|
||||
time_line_frame.setWidget(self.__time_line)
|
||||
|
||||
self.__time_line.setScaleX(self.__editor.scaleX())
|
||||
self.__time_line.setXOffset(self.__editor.xOffset())
|
||||
self.__editor.scaleXChanged.connect(self.__time_line.setScaleX)
|
||||
|
||||
scroll_x = QtWidgets.QScrollBar(orientation=Qt.Horizontal, parent=self)
|
||||
scroll_x.setRange(0, self.__editor.maximumXOffset())
|
||||
scroll_x.setSingleStep(50)
|
||||
scroll_x.setPageStep(self.__editor.pageWidth())
|
||||
scroll_x.setValue(self.__editor.xOffset())
|
||||
scroll_y = QtWidgets.QScrollBar(orientation=Qt.Vertical, parent=self)
|
||||
scroll_y.setRange(0, self.__editor.maximumYOffset())
|
||||
scroll_y.setSingleStep(20)
|
||||
scroll_y.setPageStep(self.__editor.pageHeight())
|
||||
scroll_y.setValue(self.__editor.yOffset())
|
||||
|
||||
self.__editor.maximumXOffsetChanged.connect(scroll_x.setMaximum)
|
||||
self.__editor.pageWidthChanged.connect(scroll_x.setPageStep)
|
||||
self.__editor.xOffsetChanged.connect(scroll_x.setValue)
|
||||
self.__time_line.xOffsetChanged.connect(scroll_x.setValue)
|
||||
scroll_x.valueChanged.connect(self.__editor.setXOffset)
|
||||
scroll_x.valueChanged.connect(self.__time_line.setXOffset)
|
||||
scroll_x.valueChanged.connect(self.__updateXOffset)
|
||||
|
||||
self.__editor.maximumYOffsetChanged.connect(scroll_y.setMaximum)
|
||||
self.__editor.pageHeightChanged.connect(scroll_y.setPageStep)
|
||||
self.__editor.yOffsetChanged.connect(scroll_y.setValue)
|
||||
scroll_y.valueChanged.connect(self.__editor.setYOffset)
|
||||
scroll_y.valueChanged.connect(self.__updateYOffset)
|
||||
|
||||
self.setMinimumHeight(time_line_frame.minimumHeight())
|
||||
|
||||
editor_pane = QtWidgets.QWidget(self)
|
||||
layout = QtWidgets.QGridLayout()
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.setSpacing(1)
|
||||
layout.addWidget(time_line_frame, 0, 0, 1, 1)
|
||||
layout.addWidget(editor_frame, 1, 0, 1, 1)
|
||||
layout.addWidget(scroll_x, 2, 0, 1, 1)
|
||||
layout.addWidget(scroll_y, 1, 1, 1, 1)
|
||||
editor_pane.setLayout(layout)
|
||||
|
||||
self.__toolbox = toolbox.Toolbox(parent=self, context=self.context)
|
||||
self.__toolbox.setCurrentToolBox(self.__editor.currentToolBox())
|
||||
self.__editor.currentToolBoxChanged.connect(self.__toolbox.setCurrentToolBox)
|
||||
|
||||
self.addWidget(self.__toolbox)
|
||||
self.setStretchFactor(0, 0)
|
||||
self.addWidget(editor_pane)
|
||||
self.setStretchFactor(1, 1)
|
||||
self.setCollapsible(1, False)
|
||||
|
||||
def __get_session_value(self, key: str, default: Any) -> Any:
|
||||
return self.get_session_value(self.__session_prefix + key, default)
|
||||
|
||||
def __set_session_value(self, key: str, value: Any) -> None:
|
||||
self.set_session_value(self.__session_prefix + key, value)
|
||||
|
||||
def __lazy_set_session_value(self, key: str, value: Any) -> None:
|
||||
# TODO: value should be stored to session 5sec after most recent change. I.e. need
|
||||
# some timer...
|
||||
last_time = self.__session_data_last_update.get(key, 0)
|
||||
if time_lib.time() - last_time > 5:
|
||||
self.__set_session_value(key, value)
|
||||
self.__session_data_last_update[key] = time_lib.time()
|
||||
|
||||
def __updateScaleX(self, scale: fractions.Fraction) -> None:
|
||||
self.__lazy_set_session_value('scale_x', scale)
|
||||
|
||||
def __updateXOffset(self, offset: int) -> None:
|
||||
self.__lazy_set_session_value('x_offset', offset)
|
||||
|
||||
def __updateYOffset(self, offset: int) -> None:
|
||||
self.__lazy_set_session_value('y_offset', offset)
|
||||
|
||||
def setPlayerID(self, player_id: str) -> None:
|
||||
self.__time_line.setPlayerID(player_id)
|
||||
|
||||
def close(self) -> None:
|
||||
self.__editor.close()
|
||||
|
||||
def onPaste(self, *, mode: str) -> None:
|
||||
self.__editor.onPaste(mode=mode)
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue