Compare commits

...

39 Commits

Author SHA1 Message Date
Ben Niemann 38fc807609 Lint fix. 3 years ago
Ben Niemann 879149fb41 Do project log writing synchronously (in writer process). 3 years ago
Ben Niemann e3140b78fd Fix broken port exposing. 3 years ago
Ben Niemann 2b32d0c015 Fix recursive mutation when changing control values from plugin UI. 3 years ago
Ben Niemann 1f41f545ab Fix PROTO properties. 3 years ago
Ben Niemann 9da2421ffa Fix broken session value storage. 3 years ago
Ben Niemann 7bcff8dcde Call Project.undo()/redo() directly (not through ProjectClient). 3 years ago
Ben Niemann 25481c6c2b Add some logging. 3 years ago
Ben Niemann b0fdeca943 Add PropertyConnector classes, which know how to connect widgets to properties. 3 years ago
Ben Niemann 83d3257dca Make property getters/setters overrideable. 3 years ago
Ben Niemann c9a153d396 Add missed dep on jinja2 3 years ago
Ben Niemann 4b39a0380e Track change callbacks in a dict. 3 years ago
Ben Niemann d89de1949f Simplify code for callback tracking and object cleanup. 3 years ago
Ben Niemann da30e0065d Also autogenerate the boilerplate for core model classes. 3 years ago
Ben Niemann 0547d8aaea Autogenerate the model boilerplate for builtin nodes. 3 years ago
Ben Niemann 580094bf11 Give mutation lists a descriptive name and track version and timestamp. 3 years ago
Ben Niemann 11bb8bbc4a Detect model changes outside of Project.apply_mutations(). 3 years ago
Ben Niemann c951a9d03a Remove now obsolete command related code. 3 years ago
Ben Niemann 2a7209e864 builtin_nodes.midi_cc_to_cv: switch to direct mutations instead of sending a command. 3 years ago
Ben Niemann a92cc97bdf builtin_nodes.custom_csound: switch to direct mutations instead of sending a command. 3 years ago
Ben Niemann 656278b0c9 builtin_nodes.score_track: switch to direct mutations instead of sending a command. 3 years ago
Ben Niemann 9a5c3268f3 builtin_nodes.sample_track: switch to direct mutations instead of sending a command. 3 years ago
Ben Niemann c4e9523c6c builtin_nodes.control_track: switch to direct mutations instead of sending a command. 3 years ago
Ben Niemann e6d47026c7 builtin_nodes.beat_track: switch to direct mutations instead of sending a command. 3 years ago
Ben Niemann 5821c16b08 builtin_nodes.step_sequencer: switch to direct mutations instead of sending a command. 3 years ago
Ben Niemann 7527ffcfa3 Replace remaining commands from noisicaa.music with direct mutations. 3 years ago
Ben Niemann d4a809bb6d Replace more commands (mostly graph related) by direct mutations. 3 years ago
Ben Niemann 0264f4ac9e Replace the CreateNode command with direct mutations. 3 years ago
Ben Niemann c9deacbaeb builtin_nodes.midi_source: switch to direct mutations instead of sending a command. 3 years ago
Ben Niemann 417dac0732 builtin_nodes.Instrument: switch to direct mutations instead of sending a command. 3 years ago
Ben Niemann 7949e141c1 Add an apply_mutations context manager. 3 years ago
Ben Niemann bd76ded35a Move common code to create URID mapper to a mixin. 3 years ago
Ben Niemann 581e078435 Move ProtoValue to noisicaa.value_types 3 years ago
Ben Niemann ddf5d05591 Inline ProjectProcessContext again. 3 years ago
Ben Niemann 91bde63870 Remove obsolete files. 3 years ago
Ben Niemann 7e64ad7454 Rename builtin_nodes/*/client_impl_test.py to model_test.py. 3 years ago
Ben Niemann 4d072f83f5 The great model merge itself. 3 years ago
Ben Niemann 1ab72dba20 Move the project ownership into ProjectClient, get rid of ProjectProcess. 3 years ago
Ben Niemann 208ef00eea Move project storage IO to a separate process. 3 years ago
  1. 26
      CMakeLists.txt
  2. 1
      listdeps
  3. 2
      noisicaa/CMakeLists.txt
  4. 19
      noisicaa/audioproc/audioproc_client_test.py
  5. 5
      noisicaa/audioproc/engine/plugin_host_process.py
  6. 7
      noisicaa/builtin_nodes/CMakeLists.txt
  7. 9
      noisicaa/builtin_nodes/beat_track/CMakeLists.txt
  8. 64
      noisicaa/builtin_nodes/beat_track/client_impl.py
  9. 48
      noisicaa/builtin_nodes/beat_track/commands.proto
  10. 65
      noisicaa/builtin_nodes/beat_track/commands.py
  11. 64
      noisicaa/builtin_nodes/beat_track/model.desc.pb
  12. 41
      noisicaa/builtin_nodes/beat_track/model.proto
  13. 119
      noisicaa/builtin_nodes/beat_track/model.py
  14. 61
      noisicaa/builtin_nodes/beat_track/model_test.py
  15. 38
      noisicaa/builtin_nodes/beat_track/node_ui.py
  16. 203
      noisicaa/builtin_nodes/beat_track/server_impl.py
  17. 63
      noisicaa/builtin_nodes/beat_track/server_impl_test.py
  18. 39
      noisicaa/builtin_nodes/beat_track/track_ui.py
  19. 54
      noisicaa/builtin_nodes/client_registry.py
  20. 85
      noisicaa/builtin_nodes/commands_registry.proto
  21. 10
      noisicaa/builtin_nodes/control_track/CMakeLists.txt
  22. 82
      noisicaa/builtin_nodes/control_track/client_impl_test.py
  23. 43
      noisicaa/builtin_nodes/control_track/commands.proto
  24. 59
      noisicaa/builtin_nodes/control_track/commands.py
  25. 47
      noisicaa/builtin_nodes/control_track/model.desc.pb
  26. 37
      noisicaa/builtin_nodes/control_track/model.proto
  27. 134
      noisicaa/builtin_nodes/control_track/model.py
  28. 55
      noisicaa/builtin_nodes/control_track/model_test.py
  29. 214
      noisicaa/builtin_nodes/control_track/server_impl.py
  30. 79
      noisicaa/builtin_nodes/control_track/track_ui.py
  31. 10
      noisicaa/builtin_nodes/custom_csound/CMakeLists.txt
  32. 77
      noisicaa/builtin_nodes/custom_csound/client_impl_test.py
  33. 46
      noisicaa/builtin_nodes/custom_csound/commands.proto
  34. 75
      noisicaa/builtin_nodes/custom_csound/commands.py
  35. 48
      noisicaa/builtin_nodes/custom_csound/model.desc.pb
  36. 35
      noisicaa/builtin_nodes/custom_csound/model.proto
  37. 124
      noisicaa/builtin_nodes/custom_csound/model.py
  38. 37
      noisicaa/builtin_nodes/custom_csound/model_test.py
  39. 163
      noisicaa/builtin_nodes/custom_csound/node_ui.py
  40. 142
      noisicaa/builtin_nodes/custom_csound/node_ui_test.py
  41. 191
      noisicaa/builtin_nodes/custom_csound/server_impl.py
  42. 10
      noisicaa/builtin_nodes/instrument/CMakeLists.txt
  43. 48
      noisicaa/builtin_nodes/instrument/client_impl_test.py
  44. 30
      noisicaa/builtin_nodes/instrument/commands.proto
  45. 24
      noisicaa/builtin_nodes/instrument/model.desc.pb
  46. 29
      noisicaa/builtin_nodes/instrument/model.proto
  47. 52
      noisicaa/builtin_nodes/instrument/model.py
  48. 65
      noisicaa/builtin_nodes/instrument/model_test.py
  49. 34
      noisicaa/builtin_nodes/instrument/node_ui.py
  50. 108
      noisicaa/builtin_nodes/instrument/server_impl.py
  51. 35
      noisicaa/builtin_nodes/instrument/server_impl_test.py
  52. 10
      noisicaa/builtin_nodes/midi_cc_to_cv/CMakeLists.txt
  53. 94
      noisicaa/builtin_nodes/midi_cc_to_cv/client_impl_test.py
  54. 50
      noisicaa/builtin_nodes/midi_cc_to_cv/commands.proto
  55. 83
      noisicaa/builtin_nodes/midi_cc_to_cv/commands.py
  56. 77
      noisicaa/builtin_nodes/midi_cc_to_cv/model.desc.pb
  57. 41
      noisicaa/builtin_nodes/midi_cc_to_cv/model.proto
  58. 147
      noisicaa/builtin_nodes/midi_cc_to_cv/model.py
  59. 58
      noisicaa/builtin_nodes/midi_cc_to_cv/model_test.py
  60. 189
      noisicaa/builtin_nodes/midi_cc_to_cv/node_ui.py
  61. 43
      noisicaa/builtin_nodes/midi_cc_to_cv/node_ui_test.py
  62. 271
      noisicaa/builtin_nodes/midi_cc_to_cv/server_impl.py
  63. 9
      noisicaa/builtin_nodes/midi_source/CMakeLists.txt
  64. 31
      noisicaa/builtin_nodes/midi_source/commands.proto
  65. 40
      noisicaa/builtin_nodes/midi_source/commands.py
  66. 30
      noisicaa/builtin_nodes/midi_source/model.desc.pb
  67. 30
      noisicaa/builtin_nodes/midi_source/model.proto
  68. 57
      noisicaa/builtin_nodes/midi_source/model.py
  69. 68
      noisicaa/builtin_nodes/midi_source/model_test.py
  70. 68
      noisicaa/builtin_nodes/midi_source/node_ui.py
  71. 122
      noisicaa/builtin_nodes/midi_source/server_impl.py
  72. 81
      noisicaa/builtin_nodes/midi_source/server_impl_test.py
  73. 28
      noisicaa/builtin_nodes/mixer/node_ui.py
  74. 79
      noisicaa/builtin_nodes/model.tmpl.py
  75. 2
      noisicaa/builtin_nodes/model_registry.proto
  76. 70
      noisicaa/builtin_nodes/model_registry.py
  77. 10
      noisicaa/builtin_nodes/sample_track/CMakeLists.txt
  78. 44
      noisicaa/builtin_nodes/sample_track/client_impl.py
  79. 83
      noisicaa/builtin_nodes/sample_track/client_impl_test.py
  80. 42
      noisicaa/builtin_nodes/sample_track/commands.proto
  81. 53
      noisicaa/builtin_nodes/sample_track/commands.py
  82. 2
      noisicaa/builtin_nodes/sample_track/ipc.proto
  83. 50
      noisicaa/builtin_nodes/sample_track/model.desc.pb
  84. 36
      noisicaa/builtin_nodes/sample_track/model.proto
  85. 169
      noisicaa/builtin_nodes/sample_track/model.py
  86. 95
      noisicaa/builtin_nodes/sample_track/model_test.py
  87. 257
      noisicaa/builtin_nodes/sample_track/server_impl.py
  88. 88
      noisicaa/builtin_nodes/sample_track/track_ui.py
  89. 10
      noisicaa/builtin_nodes/score_track/CMakeLists.txt
  90. 84
      noisicaa/builtin_nodes/score_track/client_impl.py
  91. 236
      noisicaa/builtin_nodes/score_track/client_impl_test.py
  92. 66
      noisicaa/builtin_nodes/score_track/commands.proto
  93. 108
      noisicaa/builtin_nodes/score_track/commands.py
  94. 88
      noisicaa/builtin_nodes/score_track/model.desc.pb
  95. 217
      noisicaa/builtin_nodes/score_track/model.py
  96. 197
      noisicaa/builtin_nodes/score_track/model_test.py
  97. 49
      noisicaa/builtin_nodes/score_track/node_ui.py
  98. 309
      noisicaa/builtin_nodes/score_track/server_impl.py
  99. 136
      noisicaa/builtin_nodes/score_track/server_impl_test.py
  100. 254
      noisicaa/builtin_nodes/score_track/track_ui.py
  101. Some files were not shown because too many files have changed in this diff Show More

26
CMakeLists.txt

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

1
listdeps

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

2
noisicaa/CMakeLists.txt

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

19
noisicaa/audioproc/audioproc_client_test.py

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

5
noisicaa/audioproc/engine/plugin_host_process.py

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

7
noisicaa/builtin_nodes/CMakeLists.txt

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

9
noisicaa/builtin_nodes/beat_track/CMakeLists.txt

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

64
noisicaa/builtin_nodes/beat_track/client_impl.py

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

48
noisicaa/builtin_nodes/beat_track/commands.proto

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

65
noisicaa/builtin_nodes/beat_track/commands.py

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

64
noisicaa/builtin_nodes/control_track/client_impl.py → noisicaa/builtin_nodes/beat_track/model.desc.pb

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

41
noisicaa/builtin_nodes/beat_track/model.proto

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

119
noisicaa/builtin_nodes/beat_track/model.py

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

61
noisicaa/builtin_nodes/beat_track/client_impl_test.py → noisicaa/builtin_nodes/beat_track/model_test.py

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

38
noisicaa/builtin_nodes/beat_track/node_ui.py

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

203
noisicaa/builtin_nodes/beat_track/server_impl.py

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

63
noisicaa/builtin_nodes/beat_track/server_impl_test.py

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

39
noisicaa/builtin_nodes/beat_track/track_ui.py

@ -29,11 +29,10 @@ from PyQt5 import QtGui