The great "model merge" refactoring.

- Merge the base/client/server model class trees into a single tree.
- Move the model into the UI process.
- Autogenerate model boilerplate code.
- Replace commands by arbitrary mutations directly from the UI.
- Assorted other cleanups.
looper
Ben Niemann 4 years ago
parent 6cd4d024d8
commit accff25ed0

@ -103,12 +103,12 @@ endmacro(add_cython_module)
macro(py_proto src)
string(REGEX REPLACE "\\.proto$" "" base ${src})
file(RELATIVE_PATH pkg_path ${CMAKE_SOURCE_DIR} ${CMAKE_CURRENT_LIST_DIR})
add_custom_command(
OUTPUT ${base}_pb2.py
COMMAND LD_LIBRARY_PATH=$ENV{VIRTUAL_ENV}/lib $ENV{VIRTUAL_ENV}/bin/protoc --python_out=${CMAKE_BINARY_DIR} --mypy_out=${CMAKE_BINARY_DIR} --proto_path=${CMAKE_SOURCE_DIR} ${CMAKE_CURRENT_LIST_DIR}/${src}
DEPENDS ${CMAKE_CURRENT_LIST_DIR}/${src}
COMMAND LD_LIBRARY_PATH=$ENV{VIRTUAL_ENV}/lib $ENV{VIRTUAL_ENV}/bin/protoc --python_out=${CMAKE_BINARY_DIR} --mypy_out=${CMAKE_BINARY_DIR} --proto_path=${CMAKE_SOURCE_DIR} --proto_path=${CMAKE_BINARY_DIR} ${pkg_path}/${src}
DEPENDS ${src}
)
file(RELATIVE_PATH pkg_path ${CMAKE_SOURCE_DIR} ${CMAKE_CURRENT_LIST_DIR})
string(REGEX REPLACE "/" "." pkg_target ${pkg_path})
add_custom_target(${pkg_target}.${src} ALL DEPENDS ${base}_pb2.py)
endmacro(py_proto)
@ -117,8 +117,8 @@ macro(cpp_proto src)
string(REGEX REPLACE "\\.proto$" "" base ${src})
add_custom_command(
OUTPUT ${base}.pb.cc ${base}.pb.h
COMMAND LD_LIBRARY_PATH=$ENV{VIRTUAL_ENV}/lib $ENV{VIRTUAL_ENV}/bin/protoc --cpp_out=${CMAKE_BINARY_DIR} --proto_path=${CMAKE_SOURCE_DIR} ${CMAKE_CURRENT_LIST_DIR}/${src}
DEPENDS ${CMAKE_CURRENT_LIST_DIR}/${src}
COMMAND LD_LIBRARY_PATH=$ENV{VIRTUAL_ENV}/lib $ENV{VIRTUAL_ENV}/bin/protoc --cpp_out=${CMAKE_BINARY_DIR} --proto_path=${CMAKE_SOURCE_DIR} --proto_path=${CMAKE_BINARY_DIR} ${pkg_path}/${src}
DEPENDS ${src}
)
endmacro(cpp_proto)
@ -148,6 +148,22 @@ macro(faust_dsp clsName src)
)
endmacro(faust_dsp)
macro(build_model src out template)
file(RELATIVE_PATH pkg_path ${CMAKE_SOURCE_DIR} ${CMAKE_CURRENT_LIST_DIR})
add_custom_command(
OUTPUT ${out} model.proto
COMMAND python noisidev/build_model.py --output ${CMAKE_BINARY_DIR} --template ${CMAKE_SOURCE_DIR}/${template} ${pkg_path}/${src}
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
DEPENDS
${CMAKE_CURRENT_LIST_DIR}/${src}
${CMAKE_SOURCE_DIR}/noisidev/build_model.py
noisidev.model_desc.proto
${CMAKE_SOURCE_DIR}/${template}
)
string(REGEX REPLACE "/" "." pkg_target ${pkg_path})
add_custom_target("model-${pkg_target}" ALL DEPENDS ${out})
endmacro(build_model)
add_subdirectory(noisicaa)
add_subdirectory(noisidev)
add_subdirectory(data)

@ -71,6 +71,7 @@ PIP_DEPS = {
PKG('./3rdparty/faustlibraries/'),
PKG('cssutils'),
PKG('cython==0.29.6'),
PKG('jinja2'),
PKG('pkgconfig'),
PKG('pyyaml'),
# TODO: get my changes upstream and use regular mypy-protobuf package from pip.

@ -38,7 +38,7 @@ add_subdirectory(host_system)
add_subdirectory(instr)
add_subdirectory(instrument_db)
add_subdirectory(lv2)
add_subdirectory(model)
add_subdirectory(value_types)
add_subdirectory(music)
add_subdirectory(node_db)
add_subdirectory(ui)

@ -27,10 +27,7 @@ import async_generator
from noisidev import unittest
from noisidev import unittest_mixins
from noisicaa.constants import TEST_OPTS
from noisicaa import lv2
from noisicaa import node_db
from noisicaa import editor_main_pb2
from . import audioproc_client
from .public import engine_notification_pb2
@ -40,6 +37,7 @@ logger = logging.getLogger(__name__)
class AudioProcClientTest(
unittest_mixins.ServerMixin,
unittest_mixins.NodeDBMixin,
unittest_mixins.URIDMapperMixin,
unittest_mixins.ProcessManagerMixin,
unittest.AsyncTestCase):
def __init__(self, *args, **kwargs):
@ -78,7 +76,6 @@ class AudioProcClientTest(
@async_generator.asynccontextmanager
@async_generator.async_generator
async def create_process(self, *, inline_plugin_host=True, inline_audioproc=True):
self.setup_urid_mapper_process(inline=True)
self.setup_plugin_host_process(inline=inline_plugin_host)
if inline_audioproc:
@ -90,17 +87,7 @@ class AudioProcClientTest(
name='audioproc',
entry='noisicaa.audioproc.audioproc_process.AudioProcSubprocess')
create_urid_mapper_response = editor_main_pb2.CreateProcessResponse()
await self.process_manager_client.call(
'CREATE_URID_MAPPER_PROCESS', None, create_urid_mapper_response)
urid_mapper_address = create_urid_mapper_response.address
urid_mapper = lv2.ProxyURIDMapper(
server_address=urid_mapper_address,
tmp_dir=TEST_OPTS.TMP_DIR)
await urid_mapper.setup(self.loop)
client = audioproc_client.AudioProcClient(self.loop, self.server, urid_mapper)
client = audioproc_client.AudioProcClient(self.loop, self.server, self.urid_mapper)
await client.setup()
await client.connect(proc.address)
try:
@ -111,8 +98,6 @@ class AudioProcClientTest(
await client.disconnect()
await client.cleanup()
await urid_mapper.cleanup(self.loop)
await proc.shutdown()
async def test_realms(self):

@ -364,7 +364,10 @@ class PluginHostProcess(core.ProcessBase):
task.add_done_callback(self.__control_value_change_done)
def __control_value_change_done(self, task: concurrent.futures.Future) -> None:
task.result()
try:
task.result()
except: # pylint: disable=bare-except
logger.error("Exception in CONTROL_VALUE_CHANGE call:\n%s", traceback.format_exc())
class PluginHostSubprocess(core.SubprocessMixin, PluginHostProcess):

@ -21,12 +21,13 @@
add_python_package(
node_description_registry.py
ui_registry.py
client_registry.py
server_registry.py
model_registry.py
)
py_proto(model_registry.proto)
py_proto(commands_registry.proto)
add_dependencies(noisicaa.builtin_nodes.model_registry.proto noisicaa.builtin_nodes.midi_source.model.proto)
add_dependencies(noisicaa.builtin_nodes.model_registry.proto noisicaa.builtin_nodes.beat_track.model.proto)
py_proto(processor_message_registry.proto)
cpp_proto(processor_message_registry.proto)

@ -21,15 +21,12 @@
add_python_package(
node_description.py
model.py
commands.py
client_impl.py
client_impl_test.py
server_impl.py
server_impl_test.py
model_test.py
node_ui.py
track_ui.py
track_ui_test.py
)
build_model(model.desc.pb _model.py noisicaa/builtin_nodes/model.tmpl.py)
py_proto(model.proto)
py_proto(commands.proto)

@ -1,64 +0,0 @@
#!/usr/bin/python3
# @begin:license
#
# Copyright (c) 2015-2019, Benjamin Niemann <pink@odahoda.de>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# @end:license
from typing import Sequence
from noisicaa.core.typing_extra import down_cast
from noisicaa import audioproc
from noisicaa import model
from noisicaa.music import project_client_model
from . import model as beat_track_model
class Beat(
project_client_model.ProjectChild,
beat_track_model.Beat,
project_client_model.ObjectBase):
@property
def time(self) -> audioproc.MusicalDuration:
return audioproc.MusicalDuration.from_proto(self.get_property_value('time'))
@property
def velocity(self) -> int:
return self.get_property_value('velocity')
@property
def measure(self) -> 'BeatMeasure':
return down_cast(BeatMeasure, super().measure)
class BeatMeasure(
project_client_model.Measure,
beat_track_model.BeatMeasure,
project_client_model.ObjectBase):
@property
def beats(self) -> Sequence[Beat]:
return self.get_property_value('beats')
class BeatTrack(
project_client_model.MeasuredTrack,
beat_track_model.BeatTrack,
project_client_model.ObjectBase):
@property
def pitch(self) -> model.Pitch:
return self.get_property_value('pitch')

@ -1,48 +0,0 @@
/*
* @begin:license
*
* Copyright (c) 2015-2019, Benjamin Niemann <pink@odahoda.de>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*
* @end:license
*/
syntax = "proto2";
import "noisicaa/audioproc/public/musical_time.proto";
import "noisicaa/model/project.proto";
package noisicaa.pb;
message UpdateBeatTrack {
required uint64 track_id = 1;
optional Pitch set_pitch = 2;
}
message CreateBeat {
required uint64 measure_id = 1;
optional MusicalDuration time = 2;
optional uint32 velocity = 3;
}
message UpdateBeat {
required uint64 beat_id = 1;
optional uint32 set_velocity = 2;
}
message DeleteBeat {
optional uint64 beat_id = 1;
}

@ -1,65 +0,0 @@
#!/usr/bin/python3
# @begin:license
#
# Copyright (c) 2015-2019, Benjamin Niemann <pink@odahoda.de>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# @end:license
from noisicaa import audioproc
from noisicaa import model
from noisicaa import music
from noisicaa.builtin_nodes import commands_registry_pb2
from . import client_impl
def update(
track: client_impl.BeatTrack, *, set_pitch: model.Pitch = None) -> music.Command:
cmd = music.Command(command='update_beat_track')
pb = cmd.Extensions[commands_registry_pb2.update_beat_track]
pb.track_id = track.id
if set_pitch is not None:
pb.set_pitch.CopyFrom(set_pitch.to_proto())
return cmd
def create_beat(
measure: client_impl.BeatMeasure, *,
time: audioproc.MusicalTime,
velocity: int = None
) -> music.Command:
cmd = music.Command(command='create_beat')
pb = cmd.Extensions[commands_registry_pb2.create_beat]
pb.measure_id = measure.id
pb.time.CopyFrom(time.to_proto())
if velocity is not None:
pb.velocity = velocity
return cmd
def update_beat(
beat: client_impl.Beat, *, set_velocity: int = None) -> music.Command:
cmd = music.Command(command='update_beat')
pb = cmd.Extensions[commands_registry_pb2.update_beat]
pb.beat_id = beat.id
if set_velocity is not None:
pb.set_velocity = set_velocity
return cmd
def delete_beat(beat: client_impl.Beat) -> music.Command:
cmd = music.Command(command='delete_beat')
pb = cmd.Extensions[commands_registry_pb2.delete_beat]
pb.beat_id = beat.id
return cmd

@ -1,5 +1,3 @@
#!/usr/bin/python3
# @begin:license
#
# Copyright (c) 2015-2019, Benjamin Niemann <pink@odahoda.de>
@ -20,25 +18,43 @@
#
# @end:license
from typing import Sequence
from noisicaa import audioproc
from noisicaa.music import project_client_model
from . import model
class ControlPoint(
project_client_model.ProjectChild, model.ControlPoint, project_client_model.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(project_client_model.Track, model.ControlTrack):
@property
def points(self) -> Sequence[ControlPoint]:
return self.get_property_value('points')
classes {
name: "Beat"
super_class: "noisicaa.music.model_base.ProjectChild"
proto_ext_name: "beat"
properties {
name: "time"
type: WRAPPED_PROTO
wrapped_type: "noisicaa.audioproc.MusicalDuration"
proto_id: 1
}
properties {
name: "velocity"
type: UINT32
proto_id: 2
}
}
classes {
name: "BeatMeasure"
super_class: "noisicaa.music.base_track.Measure"
proto_ext_name: "beat_measure"
properties {
name: "beats"
type: OBJECT_LIST
obj_type: "Beat"
proto_id: 1
}
}
classes {
name: "BeatTrack"
super_class: "noisicaa.music.base_track.MeasuredTrack"
proto_ext_name: "beat_track"
properties {
name: "pitch"
type: WRAPPED_PROTO
wrapped_type: "noisicaa.value_types.Pitch"
proto_id: 1
}
}

@ -1,41 +0,0 @@
/*
* @begin:license
*
* Copyright (c) 2015-2019, Benjamin Niemann <pink@odahoda.de>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*
* @end:license
*/
syntax = "proto2";
import "noisicaa/audioproc/public/musical_time.proto";
import "noisicaa/model/project.proto";
package noisicaa.pb;
message Beat {
optional MusicalDuration time = 1;
optional uint32 velocity = 2;
}
message BeatMeasure {
repeated uint64 beats = 1;
}
message BeatTrack {
optional Pitch pitch = 1;
}

@ -20,72 +20,125 @@
#
# @end:license
from typing import cast, Any
from typing import cast, Any, Optional, Callable, Iterator
from noisicaa.core.typing_extra import down_cast
from noisicaa import core
from noisicaa import node_db
from noisicaa import model
from noisicaa.audioproc.public import musical_time_pb2
from noisicaa.builtin_nodes import model_registry_pb2
from noisicaa import audioproc
from noisicaa import music
from noisicaa import value_types
from noisicaa.music import base_track
from . import node_description
from . import _model
class Beat(model.ProjectChild):
class BeatSpec(model.ObjectSpec):
proto_type = 'beat'
proto_ext = model_registry_pb2.beat
class BeatTrackConnector(base_track.MeasuredTrackConnector):
_node = None # type: BeatTrack
time = model.ProtoProperty(musical_time_pb2.MusicalDuration)
velocity = model.Property(int)
def _add_track_listeners(self) -> None:
self._listeners['pitch'] = self._node.pitch_changed.add(self.__pitch_changed)
def __init__(self, **kwargs: Any) -> None:
super().__init__(**kwargs)
def _add_measure_listeners(self, mref: base_track.MeasureReference) -> None:
measure = down_cast(BeatMeasure, mref.measure)
self._listeners['measure:%s:beats' % mref.id] = measure.content_changed.add(
lambda _=None: self.__measure_beats_changed(mref)) # type: ignore
def _remove_measure_listeners(self, mref: base_track.MeasureReference) -> None:
del self._listeners['measure:%s:beats' % mref.id]
def _create_events(
self, time: audioproc.MusicalTime, measure: base_track.Measure
) -> Iterator[base_track.PianoRollInterval]:
measure = down_cast(BeatMeasure, measure)
for beat in measure.beats:
beat_time = time + beat.time
event = base_track.PianoRollInterval(
beat_time, beat_time + audioproc.MusicalDuration(1, 4),
self._node.pitch, 127)
yield event
def __pitch_changed(self, change: music.PropertyChange) -> None:
self._update_measure_range(0, len(self._node.measure_list))
self.time_changed = \
core.Callback[model.PropertyChange[musical_time_pb2.MusicalDuration]]()
self.velocity_changed = core.Callback[model.PropertyChange[int]]()
def __measure_beats_changed(self, mref: base_track.MeasureReference) -> None:
self._update_measure_range(mref.index, mref.index + 1)
class Beat(_model.Beat):
def create(
self, *,
time: Optional[audioproc.MusicalDuration] = None,
velocity: Optional[int] = None,
**kwargs: Any) -> None:
super().create(**kwargs)
self.time = time
self.velocity = velocity
@property
def measure(self) -> 'BeatMeasure':
return cast(BeatMeasure, self.parent)
def property_changed(self, change: model.PropertyChange) -> None:
def property_changed(self, change: music.PropertyChange) -> None:
super().property_changed(change)
if self.measure is not None:
self.measure.content_changed.call()
class BeatMeasure(model.Measure):
class BeatMeasureSpec(model.ObjectSpec):
proto_type = 'beat_measure'
proto_ext = model_registry_pb2.beat_measure
beats = model.ObjectListProperty(Beat)
class BeatMeasure(_model.BeatMeasure):
def __init__(self, **kwargs: Any) -> None:
super().__init__(**kwargs)
self.beats_changed = core.Callback[model.PropertyListChange[Beat]]()
self.content_changed = core.Callback[None]()
def setup(self) -> None:
super().setup()
self.beats_changed.add(lambda _: self.content_changed.call())
@property
def empty(self) -> bool:
return len(self.beats) == 0
class BeatTrack(model.MeasuredTrack):
class BeatTrackSpec(model.ObjectSpec):
proto_type = 'beat_track'
proto_ext = model_registry_pb2.beat_track
def create_beat(self, time: audioproc.MusicalDuration, velocity: int = 100) -> Beat:
assert audioproc.MusicalDuration(0, 1) <= time < self.duration
assert 0 <= velocity <= 127
beat = self._pool.create(
Beat,
time=time,
velocity=velocity)
self.beats.append(beat)
return beat
pitch = model.WrappedProtoProperty(model.Pitch)
def delete_beat(self, beat: Beat) -> None:
del self.beats[beat.index]
def __init__(self, **kwargs: Any) -> None:
super().__init__(**kwargs)
self.pitch_changed = core.Callback[model.PropertyChange[model.Pitch]]()
class BeatTrack(_model.BeatTrack):
measure_cls = BeatMeasure
def create(
self, *,
pitch: Optional[value_types.Pitch] = None,
num_measures: int = 1, **kwargs: Any) -> None:
super().create(**kwargs)
if pitch is None:
self.pitch = value_types.Pitch('B2')
else:
self.pitch = pitch
for _ in range(num_measures):
self.append_measure()
@property
def description(self) -> node_db.NodeDescription:
return node_description.BeatTrackDescription
def create_node_connector(
self,
message_cb: Callable[[audioproc.ProcessorMessage], None],
audioproc_client: audioproc.AbstractAudioProcClient,
) -> BeatTrackConnector:
return BeatTrackConnector(
node=self, message_cb=message_cb, audioproc_client=audioproc_client)

@ -22,31 +22,28 @@
from noisidev import unittest
from noisicaa import audioproc
from noisicaa import model
from noisicaa import music
from noisicaa.music import base_track_test
from . import client_impl
from . import commands
from . import model
class BeatTrackTest(base_track_test.TrackTestMixin, unittest.AsyncTestCase):
node_uri = 'builtin://beat-track'
track_cls = client_impl.BeatTrack
track_cls = model.BeatTrack
async def test_create_measure(self):
track = await self._add_track()
self.assertEqual(len(track.measure_list), 1)
await self.client.send_command(music.create_measure(
track, pos=0))
with self.project.apply_mutations('test'):
track.insert_measure(0)
self.assertEqual(len(track.measure_list), 2)
async def test_delete_measure(self):
track = await self._add_track()
self.assertEqual(len(track.measure_list), 1)
await self.client.send_command(music.delete_measure(
track.measure_list[0]))
with self.project.apply_mutations('test'):
track.remove_measure(0)
self.assertEqual(len(track.measure_list), 0)
async def test_clear_measures(self):
@ -54,56 +51,30 @@ class BeatTrackTest(base_track_test.TrackTestMixin, unittest.AsyncTestCase):
self.assertEqual(len(track.measure_list), 1)
old_measure = track.measure_list[0].measure
await self.client.send_command(music.update_measure(
measure=track.measure_list[0],
clear=True))
with self.project.apply_mutations('test'):
track.measure_list[0].clear_measure()
self.assertIsNot(old_measure, track.measure_list[0].measure)
async def test_set_pitch(self):
track = await self._add_track()
await self.client.send_command(commands.update(
track,
set_pitch=model.Pitch('C2')))
self.assertEqual(track.pitch, model.Pitch('C2'))
async def test_add_beat(self):
track = await self._add_track()
measure = track.measure_list[0].measure
await self.client.send_command(commands.create_beat(
measure,
time=audioproc.MusicalDuration(1, 4)))
with self.project.apply_mutations('test'):
measure.create_beat(audioproc.MusicalDuration(1, 4))
self.assertEqual(measure.beats[0].time, audioproc.MusicalDuration(1, 4))
self.assertEqual(measure.beats[0].velocity, 100)
await self.client.send_command(commands.create_beat(
measure,
time=audioproc.MusicalDuration(2, 4),
velocity=120))
with self.project.apply_mutations('test'):
measure.create_beat(audioproc.MusicalDuration(2, 4), 120)
self.assertEqual(measure.beats[1].time, audioproc.MusicalDuration(2, 4))
self.assertEqual(measure.beats[1].velocity, 120)
async def test_delete_beat(self):
track = await self._add_track()
measure = track.measure_list[0].measure
await self.client.send_command(commands.create_beat(
measure,
time=audioproc.MusicalDuration(1, 4)))
with self.project.apply_mutations('test'):
measure.create_beat(audioproc.MusicalDuration(1, 4))
await self.client.send_command(commands.delete_beat(
measure.beats[0]))
with self.project.apply_mutations('test'):
measure.delete_beat(measure.beats[0])
self.assertEqual(len(measure.beats), 0)
async def test_beat_set_velocity(self):
track = await self._add_track()
measure = track.measure_list[0].measure
await self.client.send_command(commands.create_beat(
measure,
time=audioproc.MusicalDuration(1, 4)))
beat = measure.beats[0]
await self.client.send_command(commands.update_beat(
beat,
set_velocity=57))
self.assertEqual(beat.velocity, 57)

@ -20,7 +20,7 @@
#
# @end:license
from typing import Any, Dict
from typing import Any
import logging
import os.path
@ -30,23 +30,23 @@ from PyQt5 import QtSvg
from noisicaa.constants import DATA_DIR
from noisicaa import core
from noisicaa import model
from noisicaa import value_types
from noisicaa import music
from noisicaa.ui.graph import track_node
from noisicaa.ui import ui_base
from . import commands
from . import client_impl
from . import model
logger = logging.getLogger(__name__)
class BeatTrackWidget(ui_base.ProjectMixin, QtWidgets.QScrollArea):
def __init__(self, track: client_impl.BeatTrack, **kwargs: Any) -> None:
class BeatTrackWidget(ui_base.ProjectMixin, core.AutoCleanupMixin, QtWidgets.QScrollArea):
def __init__(self, track: model.BeatTrack, **kwargs: Any) -> None:
super().__init__(**kwargs)
self.__track = track
self.__listeners = {} # type: Dict[str, core.Listener]
self.__listeners = core.ListenerMap[str]()
self.add_cleanup_function(self.__listeners.cleanup)
body = QtWidgets.QWidget(self)
body.setAutoFillBackground(False)
@ -68,43 +68,33 @@ class BeatTrackWidget(ui_base.ProjectMixin, QtWidgets.QScrollArea):
self.setFrameShape(QtWidgets.QFrame.NoFrame)
self.setWidget(body)
def cleanup(self) -> None:
for listener in self.__listeners.values():
listener.remove()
self.__listeners.clear()
def __pitchChanged(self, change: model.PropertyValueChange[str]) -> None:
def __pitchChanged(self, change: music.PropertyValueChange[str]) -> None:
self.__pitch.setText(str(change.new_value))
def __pitchEdited(self) -> None:
try:
pitch = model.Pitch(self.__pitch.text())
pitch = value_types.Pitch(self.__pitch.text())
except ValueError:
self.__pitch.setText(str(self.__track.pitch))
else:
if pitch != self.__track.pitch:
self.send_command_async(commands.update(
self.__track,
set_pitch=pitch))
with self.project.apply_mutations('%s: Change pitch' % self.__track.name):
self.__track.pitch = pitch
class BeatTrackNode(track_node.TrackNode):
def __init__(self, node: music.BaseNode, **kwargs: Any) -> None:
assert isinstance(node, client_impl.BeatTrack)
assert isinstance(node, model.BeatTrack)
self.__widget = None # type: BeatTrackWidget
self.__track = node # type: client_impl.BeatTrack
self.__track = node # type: model.BeatTrack
super().__init__(
node=node,
icon=QtSvg.QSvgRenderer(os.path.join(DATA_DIR, 'icons', 'track-type-beat.svg')),
**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 = BeatTrackWidget(track=self.__track, context=self.context)
self.add_cleanup_function(self.__widget.cleanup)
return self.__widget

@ -1,203 +0,0 @@
#!/usr/bin/python3
# @begin:license
#
# Copyright (c) 2015-2019, Benjamin Niemann <pink@odahoda.de>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# @end:license
import logging
from typing import Any, MutableSequence, Optional, Iterator, Callable
from noisicaa.core.typing_extra import down_cast
from noisicaa import audioproc
from noisicaa import model
from noisicaa.music import commands
from noisicaa.music import pmodel
from noisicaa.music import base_track
from noisicaa.builtin_nodes import commands_registry_pb2
from . import commands_pb2
from . import model as beat_track_model
logger = logging.getLogger(__name__)
class UpdateBeatTrack(commands.Command):
proto_type = 'update_beat_track'
proto_ext = commands_registry_pb2.update_beat_track
def run(self) -> None:
pb = down_cast(commands_pb2.UpdateBeatTrack, self.pb)
track = down_cast(BeatTrack, self.pool[pb.track_id])
if pb.HasField('set_pitch'):
track.pitch = model.Pitch.from_proto(pb.set_pitch)
class CreateBeat(commands.Command):
proto_type = 'create_beat'
proto_ext = commands_registry_pb2.create_beat
def run(self) -> None:
pb = down_cast(commands_pb2.CreateBeat, self.pb)
measure = down_cast(BeatMeasure, self.pool[pb.measure_id])
time = audioproc.MusicalDuration.from_proto(pb.time)
assert audioproc.MusicalDuration(0, 1) <= time < measure.duration
if pb.HasField('velocity'):
velocity = pb.velocity
else:
velocity = 100
beat = self.pool.create(
Beat,
time=time,
velocity=velocity)
measure.beats.append(beat)
class UpdateBeat(commands.Command):
proto_type = 'update_beat'
proto_ext = commands_registry_pb2.update_beat
def run(self) -> None:
pb = down_cast(commands_pb2.UpdateBeat, self.pb)
beat = down_cast(Beat, self.pool[pb.beat_id])
if pb.HasField('set_velocity'):
beat.velocity = pb.set_velocity
class DeleteBeat(commands.Command):
proto_type = 'delete_beat'
proto_ext = commands_registry_pb2.delete_beat
def run(self) -> None:
pb = down_cast(commands_pb2.DeleteBeat, self.pb)
beat = down_cast(Beat, self.pool[pb.beat_id])
measure = beat.measure
del measure.beats[beat.index]
class Beat(pmodel.ProjectChild, beat_track_model.Beat, pmodel.ObjectBase):
def create(
self, *,
time: Optional[audioproc.MusicalDuration] = None,
velocity: Optional[int] = None,
**kwargs: Any) -> None:
super().create(**kwargs)
self.time = time
self.velocity = velocity
@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(base_track.Measure, beat_track_model.BeatMeasure, pmodel.ObjectBase):
@property
def beats(self) -> MutableSequence[Beat]:
return self.get_property_value('beats')
@property
def empty(self) -> bool:
return len(self.beats) == 0
class BeatTrackConnector(base_track.MeasuredTrackConnector):
_node = None # type: BeatTrack
def _add_track_listeners(self) -> None:
self._listeners['pitch'] = self._node.pitch_changed.add(self.__pitch_changed)
def _add_measure_listeners(self, mref: pmodel.MeasureReference) -> None:
measure = down_cast(BeatMeasure, mref.measure)
self._listeners['measure:%s:beats' % mref.id] = measure.content_changed.add(
lambda _=None: self.__measure_beats_changed(mref)) # type: ignore
def _remove_measure_listeners(self, mref: pmodel.MeasureReference) -> None:
self._listeners.pop('measure:%s:beats' % mref.id).remove()
def _create_events(
self, time: audioproc.MusicalTime, measure: pmodel.Measure
) -> Iterator[base_track.PianoRollInterval]:
measure = down_cast(BeatMeasure, measure)
for beat in measure.beats:
beat_time = time + beat.time
event = base_track.PianoRollInterval(
beat_time, beat_time + audioproc.MusicalDuration(1, 4),
self._node.pitch, 127)
yield event
def __pitch_changed(self, change: model.PropertyChange) -> None:
self._update_measure_range(0, len(self._node.measure_list))
def __measure_beats_changed(self, mref: pmodel.MeasureReference) -> None:
self._update_measure_range(mref.index, mref.index + 1)
class BeatTrack(base_track.MeasuredTrack, beat_track_model.BeatTrack, pmodel.ObjectBase):
measure_cls = BeatMeasure
def create(
self, *,
pitch: Optional[model.Pitch] = None,
num_measures: int = 1, **kwargs: Any) -> None:
super().create(**kwargs)
if pitch is None:
self.pitch = model.Pitch('B2')
else:
self.pitch = pitch
for _ in range(num_measures):
self.append_measure()
@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)
def create_node_connector(
self,
message_cb: Callable[[audioproc.ProcessorMessage], None],
audioproc_client: audioproc.AbstractAudioProcClient,
) -> BeatTrackConnector:
return BeatTrackConnector(
node=self, message_cb=message_cb, audioproc_client=audioproc_client)

@ -1,63 +0,0 @@
#!/usr/bin/python3
# @begin:license
#
# Copyright (c) 2015-2019, Benjamin Niemann <pink@odahoda.de>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# @end:license
from noisicaa import audioproc
from noisicaa import model
from noisicaa.music import pmodel_test
from . import server_impl
T0 = audioproc.MusicalDuration(0, 4)
class BeatTrackTest(pmodel_test.MeasuredTrackMixin, pmodel_test.ModelTest):
cls = server_impl.BeatTrack
create_args = {'name': 'test'}
measure_cls = server_impl.BeatMeasure
def test_pitch(self):
track = self.pool.create(self.cls, **self.create_args)
track.pitch = model.Pitch('F4')
self.assertEqual(track.pitch, model.Pitch('F4'))
class BeatMeasureTest(pmodel_test.ModelTest):
def test_beats(self):
measure = self.pool.create(server_impl.BeatMeasure)
beat = self.pool.create(server_impl.Beat, time=T0, velocity=100)
measure.beats.append(beat)
self.assertIs(measure.beats[0], beat)
class BeatTest(pmodel_test.ModelTest):
def test_time(self):
beat = self.pool.create(server_impl.Beat, time=T0, velocity=100)
beat.time = audioproc.MusicalDuration(1, 4)
self.assertEqual(beat.time, audioproc.MusicalDuration(1, 4))
def test_velocity(self):
beat = self.pool.create(server_impl.Beat, time=T0, velocity=100)
beat.velocity = 120
self.assertEqual(beat.velocity, 120)

@ -29,11 +29,10 @@ from PyQt5 import QtGui
from noisicaa.core.typing_extra import down_cast
from noisicaa import audioproc
from noisicaa import model
from noisicaa import value_types
from noisicaa.ui.track_list import measured_track_editor
from noisicaa.ui.track_list import tools
from . import commands
from . import client_impl
from . import model
logger = logging.getLogger(__name__)
@ -61,13 +60,13 @@ class EditBeatsTool(measured_track_editor.MeasuredToolBase):
for beat in target.measure.beats:
if beat.time == click_time:
self.send_command_async(commands.delete_beat(beat))
with self.project.apply_mutations('%s: Remove beat' % target.track.name):
target.measure.delete_beat(beat)
evt.accept()
return
self.send_command_async(commands.create_beat(
target.measure,
time=click_time))
with self.project.apply_mutations('%s: Insert beat' % target.track.name):
target.measure.create_beat(click_time)
target.track_editor.playNoteOn(target.track.pitch)
evt.accept()
return
@ -87,9 +86,9 @@ class EditBeatsTool(measured_track_editor.MeasuredToolBase):
for beat in target.measure.beats:
if beat.time == click_time:
self.send_command_async(commands.update_beat(
beat,
set_velocity=max(0, min(127, beat.velocity + vel_delta))))
with self.project.apply_mutations(
'%s: Change beat velocity' % target.track.name):
beat.velocity = max(0, min(127, beat.velocity + vel_delta))
evt.accept()
return
@ -122,12 +121,12 @@ class BeatMeasureEditor(measured_track_editor.MeasureEditor):
self.__ghost_time = None # type: audioproc.MusicalDuration
@property
def track(self) -> client_impl.BeatTrack:
return down_cast(client_impl.BeatTrack, super().track)
def track(self) -> model.BeatTrack:
return down_cast(model.BeatTrack, super().track)
@property
def measure(self) -> client_impl.BeatMeasure:
return down_cast(client_impl.BeatMeasure, super().measure)
def measure(self) -> model.BeatMeasure:
return down_cast(model.BeatMeasure, super().measure)
def xToTime(self, x: int) -> audioproc.MusicalTime:
return audioproc.MusicalDuration(
@ -135,9 +134,9 @@ class BeatMeasureEditor(measured_track_editor.MeasureEditor):
8 * self.measure.time_signature.upper)
def addMeasureListeners(self) -> None:
self.measure_listeners.append(self.measure.content_changed.add(
self._measure_listeners.add(self.measure.content_changed.add(
lambda _=None: self.invalidatePaintCache(self.FOREGROUND))) # type: ignore
self.measure_listeners.append(self.measure.beats_changed.add(
self._measure_listeners.add(self.measure.beats_changed.add(
lambda _: self.invalidatePaintCache(self.FOREGROUND)))
def setGhostTime(self, time: audioproc.MusicalDuration) -> None:
@ -234,15 +233,15 @@ class BeatTrackEditor(measured_track_editor.MeasuredTrackEditor):
def __init__(self, **kwargs: Any) -> None:
super().__init__(**kwargs)
self.__play_last_pitch = None # type: model.Pitch
self.__play_last_pitch = None # type: value_types.Pitch
self.setHeight(60)
@property
def track(self) -> client_impl.BeatTrack:
return down_cast(client_impl.BeatTrack, super().track)
def track(self) -> model.BeatTrack:
return down_cast(model.BeatTrack, super().track)
def playNoteOn(self, pitch: model.Pitch) -> None:
def playNoteOn(self, pitch: value_types.Pitch) -> None:
self.playNoteOff()
# TODO: use messages instead

@ -1,54 +0,0 @@
#!/usr/bin/python3
# @begin:license
#
# Copyright (c) 2015-2019, Benjamin Niemann <pink@odahoda.de>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# @end:license
from noisicaa import model
from .score_track.client_impl import Note, ScoreMeasure, ScoreTrack
from .beat_track.client_impl import Beat, BeatMeasure, BeatTrack
from .control_track.client_impl import ControlPoint, ControlTrack
from .sample_track.client_impl import SampleRef, SampleTrack
from .instrument.client_impl import Instrument
from .custom_csound.client_impl import CustomCSound, CustomCSoundPort
from .midi_source.client_impl import MidiSource
from .step_sequencer.client_impl import StepSequencer, StepSequencerChannel, StepSequencerStep
from .midi_cc_to_cv.client_impl import MidiCCtoCV, MidiCCtoCVChannel
def register_classes(pool: model.AbstractPool) -> None:
pool.register_class(Note)
pool.register_class(ScoreMeasure)
pool.register_class(ScoreTrack)
pool.register_class(Beat)
pool.register_class(BeatMeasure)
pool.register_class(BeatTrack)
pool.register_class(ControlPoint)
pool.register_class(ControlTrack)
pool.register_class(SampleRef)
pool.register_class(SampleTrack)
pool.register_class(Instrument)
pool.register_class(CustomCSoundPort)
pool.register_class(CustomCSound)
pool.register_class(MidiSource)
pool.register_class(StepSequencer)
pool.register_class(StepSequencerChannel)
pool.register_class(StepSequencerStep)
pool.register_class(MidiCCtoCV)
pool.register_class(MidiCCtoCVChannel)

@ -1,85 +0,0 @@
/*
* @begin:license
*
* Copyright (c) 2015-2019, Benjamin Niemann <pink@odahoda.de>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*
* @end:license
*/
syntax = "proto2";
import "noisicaa/music/commands.proto";
import "noisicaa/builtin_nodes/score_track/commands.proto";
import "noisicaa/builtin_nodes/beat_track/commands.proto";
import "noisicaa/builtin_nodes/control_track/commands.proto";
import "noisicaa/builtin_nodes/sample_track/commands.proto";
import "noisicaa/builtin_nodes/instrument/commands.proto";
import "noisicaa/builtin_nodes/custom_csound/commands.proto";
import "noisicaa/builtin_nodes/midi_source/commands.proto";
import "noisicaa/builtin_nodes/step_sequencer/commands.proto";
import "noisicaa/builtin_nodes/midi_cc_to_cv/commands.proto";
package noisicaa.pb;
extend Command {
// Score track (401xxx)
optional UpdateScoreTrack update_score_track = 401000;
optional UpdateScoreMeasure update_score_measure = 401001;
optional CreateNote create_note = 401002;
optional UpdateNote update_note = 401003;
optional DeleteNote delete_note = 401004;
// Beat track (402xxx)
optional UpdateBeatTrack update_beat_track = 402000;
optional CreateBeat create_beat = 402001;
optional UpdateBeat update_beat = 402002;
optional DeleteBea