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
Ben Niemann 2018-12-21 17:03:16 +01:00
parent f898f8b922
commit 66d7f8c6af
109 changed files with 8021 additions and 7687 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

75
noisicaa/model/color.py Normal file
View File

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

View File

@ -28,7 +28,7 @@ message ObjectBase {
required string type = 1;
required uint64 id = 2;
extensions 1000 to max;
extensions 100000 to max;
}
message ObjectTree {

View File

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

View File

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

View File

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

View File

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

View File

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

67
noisicaa/model/sizef.py Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

156
noisicaa/ui/player_state.py Normal file
View File

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

View File

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

View File

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

74
noisicaa/ui/slots.py Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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