New instrument library.

looper
Ben Niemann 6 years ago
parent 1893577b13
commit 9a04e8723d

@ -1,5 +1,17 @@
# -*- org-tags-column: -98 -*-
* TODO More flexible instrument handling :FR:
Use display_name in track_property_dock
- query instrument_db for description
- fallback to URI, if description not found
Have a list of instrument types
- with scanners to list instruments
- that know how to create the node for it (which node_uri and which parameters)
- when opening a file, ask each intrument type if it can handle it. if more than one, ask which
one to use.
- configure search paths for each type
* Session state :FR:
- store binary log for efficiency
- replay log on open
@ -26,21 +38,13 @@ Blacklist crashing nodes
* Graceful AudioStream shutdown :FR:
send close message to backend
* InstrumentLibrary: remember the selected MIDI source :FR:
* Plugin support :FR:
** LV2
* Fix removing measures :BUG:
- remove measure on SheetPropertyTrack causes exception
- no way to remove trailing measures from sheet
* More flexible instrument handling :FR:
Have a list of instrument types
- with scanners to list instruments
- that know how to create the node for it (which node_uri and which parameters)
- when opening a file, ask each intrument type if it can handle it. if more than one, ask which
one to use.
- separate instrument db (like node db)?
- configure search paths for each type
* Unify instrument handling in ScoreTrack and BeatTrack :CLEANUP:
* Move BackendManager to noisicaa.core :CLEANUP:
* Review licenses of all used modules :FR:

@ -35,12 +35,18 @@ class Main(object):
self.manager.server.add_command_handler(
'CREATE_NODE_DB_PROCESS',
self.handle_create_node_db_process)
self.manager.server.add_command_handler(
'CREATE_INSTRUMENT_DB_PROCESS',
self.handle_create_instrument_db_process)
self.stop_event = asyncio.Event()
self.returncode = 0
self.node_db_process = None
self.node_db_process_lock = asyncio.Lock(loop=self.event_loop)
self.instrument_db_process = None
self.instrument_db_process_lock = asyncio.Lock(loop=self.event_loop)
def run(self, argv):
self.parse_args(argv)
@ -153,6 +159,15 @@ class Main(object):
return self.node_db_process.address
async def handle_create_instrument_db_process(self):
async with self.instrument_db_process_lock:
if self.instrument_db_process is None:
self.instrument_db_process = await self.manager.start_process(
'instrument_db',
'noisicaa.instrument_db.process.InstrumentDBProcess')
return self.instrument_db_process.address
if __name__ == '__main__':
sys.exit(Main().run(sys.argv))

@ -1,116 +0,0 @@
#!/usr/bin/python3
import glob
import hashlib
import os.path
import logging
from . import soundfont
# TODO:
# - removing instruments from collection, hides it
# - removing collection removes all instruments
# - make UI
# - UI actions use commands
logger = logging.getLogger(__name__)
class Error(Exception):
pass
class Collection(object):
def __init__(self, name):
self.name = name
class SoundFontCollection(Collection):
def __init__(self, name, path):
super().__init__(name)
self.path = path
def create_instruments(self):
sf = soundfont.SoundFont()
sf.parse(self.path)
for preset in sf.presets:
yield SoundFontInstrument(
preset.name, self, self.path, preset.bank, preset.preset)
class Instrument(object):
def __init__(self, name, collection, id):
self.name = name
self.collection = collection
self.id = id
class SoundFontInstrument(Instrument):
def __init__(self, name, collection, path, bank, preset):
super().__init__(
name,
collection,
hashlib.md5(('%s:%d:%d' % (path, bank, preset)).encode('utf-8')).hexdigest())
self.path = path
self.bank = bank
self.preset = preset
def __str__(self):
return '<SoundFontInstrument "%s" path="%s" bank=%d preset=%d>' % (
self.name, self.path, self.bank, self.preset)
class SampleInstrument(Instrument):
def __init__(self, name, path):
super().__init__(
name,
None,
hashlib.md5(('%s' % path).encode('utf-8')).hexdigest())
self.path = path
def __str__(self):
return '<SampleInstrument "%s" path="%s">' % (
self.name, self.path)
class InstrumentLibrary(object):
def __init__(self, add_default_instruments=True):
self.instruments = []
self.collections = []
self.default_instrument = None
if add_default_instruments:
for p in sorted(glob.glob('/usr/share/sounds/sf2/*.sf2')):
self.add_soundfont(p)
for d in sorted(glob.glob('/storage/home/share/samples/ST-0?')):
if not os.path.isdir(d):
continue
for p in sorted(glob.glob(os.path.join(d, '*.wav'))):
self.add_sample(p)
if self.default_instrument is None:
self.default_instrument = self.instruments[0]
logger.info("Default instrument: %s", self.default_instrument)
def add_instrument(self, instr):
logger.info("Adding instrument %s to library...", instr)
self.instruments.append(instr)
def add_soundfont(self, path):
sf = soundfont.SoundFont()
sf.parse(path)
collection = SoundFontCollection(sf.bank_name, path)
self.collections.append(collection)
for instr in collection.create_instruments():
self.add_instrument(instr)
if instr.bank == 0 and instr.preset == 0 and self.default_instrument is None:
self.default_instrument = instr
def add_sample(self, path):
instr = SampleInstrument(
os.path.splitext(os.path.basename(path))[0], path)
self.add_instrument(instr)

@ -1,35 +0,0 @@
#!/usr/bin/python3
import json
import unittest
#from .library import InstrumentLibrary
# class LibraryTest(unittest.TestCase):
# def testAddSoundFont(self):
# lib = InstrumentLibrary(add_default_instruments=False)
# lib.add_soundfont('/usr/share/sounds/sf2/TimGM6mb.sf2')
# state = json.loads(json.dumps(lib.serialize()))
# lib2 = InstrumentLibrary(state=state)
# lib2.init_references()
# def testAddSample(self):
# path = '/storage/home/share/samples/ST-01/MonsterBass.wav'
# lib = InstrumentLibrary(add_default_instruments=False)
# lib.add_sample(path)
# self.assertEqual(len(lib.instruments), 1)
# self.assertEqual(lib.instruments[0].name, 'MonsterBass')
# self.assertEqual(lib.instruments[0].path, path)
# state = json.loads(json.dumps(lib.serialize()))
# lib = InstrumentLibrary(state=state)
# lib.init_references()
# self.assertEqual(len(lib.instruments), 1)
# self.assertEqual(lib.instruments[0].name, 'MonsterBass')
# self.assertEqual(lib.instruments[0].path, path)
if __name__ == '__main__':
unittest.main()

@ -0,0 +1,9 @@
from .client import InstrumentDBClientMixin
from .instrument_description import (
InstrumentDescription,
parse_uri,
)
from .mutations import (
AddInstrumentDescription,
RemoveInstrumentDescription,
)

@ -0,0 +1,66 @@
#!/usr/bin/python3
import logging
from noisicaa import core
from noisicaa.core import ipc
from . import mutations
logger = logging.getLogger(__name__)
class InstrumentDBClientMixin(object):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._stub = None
self._session_id = None
self._instruments = {}
self.listeners = core.CallbackRegistry()
@property
def instruments(self):
return sorted(
self._instruments.values(), key=lambda i: i.display_name.lower())
def get_instrument_description(self, uri):
return self._instrument[uri]
async def setup(self):
await super().setup()
self.server.add_command_handler(
'INSTRUMENTDB_MUTATION', self.handle_mutation)
async def connect(self, address, flags=None):
assert self._stub is None
self._stub = ipc.Stub(self.event_loop, address)
await self._stub.connect()
self._session_id = await self._stub.call(
'START_SESSION', self.server.address, flags)
async def disconnect(self, shutdown=False):
if self._session_id is not None:
await self._stub.call('END_SESSION', self._session_id)
self._session_id = None
if self._stub is not None:
if shutdown:
await self.shutdown()
await self._stub.close()
self._stub = None
async def shutdown(self):
await self._stub.call('SHUTDOWN')
async def start_scan(self):
return await self._stub.call('START_SCAN', self._session_id)
def handle_mutation(self, mutation):
logger.info("Mutation received: %s" % mutation)
if isinstance(mutation, mutations.AddInstrumentDescription):
assert mutation.description.uri not in self._instruments
self._instruments[mutation.description.uri] = mutation.description
else:
raise ValueError(mutation)
self.listeners.call('mutation', mutation)

@ -0,0 +1,73 @@
#!/usr/bin/python3
import asyncio
import time
import unittest
from unittest import mock
import asynctest
from noisicaa import core
from noisicaa.core import ipc
from . import process
from . import client
class TestClientImpl(object):
def __init__(self, event_loop):
super().__init__()
self.event_loop = event_loop
self.server = ipc.Server(self.event_loop, 'client')
async def setup(self):
await self.server.setup()
async def cleanup(self):
await self.server.cleanup()
class TestClient(client.InstrumentDBClientMixin, TestClientImpl):
pass
class TestProcessImpl(object):
def __init__(self, event_loop):
super().__init__()
self.event_loop = event_loop
self.server = ipc.Server(self.event_loop, 'audioproc')
async def setup(self):
await self.server.setup()
async def cleanup(self):
await self.server.cleanup()
class TestProcess(process.InstrumentDBProcessMixin, TestProcessImpl):
pass
class InstrumentDBClientTest(asynctest.TestCase):
async def setUp(self):
self.process = TestProcess(self.loop)
await self.process.setup()
self.process_task = self.loop.create_task(
self.process.run())
self.client = TestClient(self.loop)
await self.client.setup()
await self.client.connect(self.process.server.address)
async def tearDown(self):
await self.client.disconnect(shutdown=True)
await self.client.cleanup()
await asyncio.wait_for(self.process_task, None)
await self.process.cleanup()
async def test_start_scan(self):
await self.client.start_scan()
if __name__ == '__main__':
unittest.main()

@ -0,0 +1,33 @@
#!/usr/bin/python3
import enum
import urllib.parse
class InstrumentDescription(object):
def __init__(self, uri, display_name):
self.uri = uri
self.display_name = display_name
def parse_uri(uri):
fmt, _, path, _, args, _ = urllib.parse.urlparse(uri)
path = urllib.parse.unquote(path)
if args:
args = dict(urllib.parse.parse_qsl(args, strict_parsing=True))
else:
args = {}
if fmt == 'sf2':
return 'fluidsynth', {
'soundfont_path': path,
'bank': int(args['bank']),
'preset': int(args['preset'])
}
elif fmt == 'sample':
return 'sample_player', {
'sample_path': path,
}
else:
raise ValueError(fmt)

@ -0,0 +1,22 @@
#!/usr/bin/python3
class Mutation(object):
pass
class AddInstrumentDescription(Mutation):
def __init__(self, description):
self.description = description
def __str__(self):
return '<AddInstrumentDescription uri="%s">' % self.description.uri
class RemoveInstrumentDescription(Mutation):
def __init__(self, uri):
self.uri = uri
def __str__(self):
return '<RemoveInstrumentDescription uri="%s">' % self.uri

@ -0,0 +1,52 @@
#!/usr/bin/python3
import glob
import os
import os.path
import logging
from noisicaa import core
from noisicaa import instrument_db
from . import sample_scanner
from . import soundfont_scanner
logger = logging.getLogger(__name__)
class InstrumentDB(object):
def __init__(self):
self._instruments = {}
self.listeners = core.CallbackRegistry()
def setup(self):
scanners = [
sample_scanner.SampleScanner(),
soundfont_scanner.SoundFontScanner(),
]
search_paths = [
'/usr/share/sounds/sf2/',
] + sorted(glob.glob('/storage/home/share/samples/ST-0?'))
for root_path in search_paths:
logger.info("Scanning instruments in %s...", root_path)
for dname, dirs, files in os.walk(root_path):
for fname in sorted(files):
path = os.path.join(dname, fname)
logger.info("Scanning file %s...", path)
for scanner in scanners:
for description in scanner.scan(path):
assert description.uri not in self._instruments
self._instruments[description.uri] = description
def cleanup(self):
pass
def initial_mutations(self):
for uri, description in sorted(self._instruments.items()):
yield instrument_db.AddInstrumentDescription(description)
def start_scan(self):
pass

@ -0,0 +1,13 @@
#!/usr/bin/python3
import unittest
from . import db
class NodeDBTest(unittest.TestCase):
pass
if __name__ == '__main__':
unittest.main()

@ -0,0 +1,30 @@
#!/usr/bin/python3
import logging
import os
import os.path
from noisicaa import constants
from noisicaa import instrument_db
from . import scanner
logger = logging.getLogger(__name__)
class SampleScanner(scanner.Scanner):
def __init__(self, **kwargs):
super().__init__(**kwargs)
def scan(self, path):
if not path.endswith('.wav'):
return
uri = self.make_uri('sample', path)
logger.info("Adding sample instrument %s...", uri)
description = instrument_db.InstrumentDescription(
uri=uri,
display_name=os.path.basename(path)[:-4])
yield description

@ -0,0 +1,20 @@
#!/usr/bin/python3
import urllib.parse
class Scanner(object):
def __init__(self):
pass
def make_uri(self, fmt, path, **kwargs):
return urllib.parse.urlunparse((
fmt,
None,
urllib.parse.quote(path),
None,
urllib.parse.urlencode(sorted((k, str(v)) for k, v in kwargs.items()), True),
None))
def scan(self):
raise NotImplementedError

@ -0,0 +1,35 @@
#!/usr/bin/python3
import logging
import os
import os.path
import urllib.parse
from noisicaa import constants
from noisicaa import instrument_db
from noisicaa.instr import soundfont
from . import scanner
logger = logging.getLogger(__name__)
class SoundFontScanner(scanner.Scanner):
def __init__(self, **kwargs):
super().__init__(**kwargs)
def scan(self, path):
if not path.endswith('.sf2'):
return
sf = soundfont.SoundFont()
sf.parse(path)
for preset in sf.presets:
uri = self.make_uri('sf2', path, bank=preset.bank, preset=preset.preset)
logger.info("Adding soundfont instrument %s...", uri)
description = instrument_db.InstrumentDescription(
uri=uri,
display_name=preset.name)
yield description

@ -0,0 +1,135 @@
#!/usr/bin/python3
import asyncio
import functools
import logging
import uuid
from noisicaa import core
from noisicaa.core import ipc
from .private import db
logger = logging.getLogger(__name__)
class InvalidSessionError(Exception): pass
class Session(object):
def __init__(self, event_loop, callback_stub, flags):
self.event_loop = event_loop
self.callback_stub = callback_stub
self.flags = flags or set()
self.id = uuid.uuid4().hex
self.pending_mutations = []
def cleanup(self):
pass
def publish_mutation(self, mutation):
if not self.callback_stub.connected:
self.pending_mutations.append(mutation)
return
callback_task = self.event_loop.create_task(
self.callback_stub.call('INSTRUMENTDB_MUTATION', mutation))
callback_task.add_done_callback(self.publish_mutation_done)
def publish_mutation_done(self, callback_task):
assert callback_task.done()
exc = callback_task.exception()
if exc is not None:
logger.error(
"INSTRUMENTDB_MUTATION failed with exception: %s", exc)
def callback_stub_connected(self):
assert self.callback_stub.connected
while self.pending_mutations:
self.publish_mutation(self.pending_mutations.pop(0))
class InstrumentDBProcessMixin(object):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.sessions = {}
self.db = db.InstrumentDB()
async def setup(self):
await super().setup()
self._shutting_down = asyncio.Event()
self._shutdown_complete = asyncio.Event()
self.server.add_command_handler(
'START_SESSION', self.handle_start_session)
self.server.add_command_handler(
'END_SESSION', self.handle_end_session)
self.server.add_command_handler('SHUTDOWN', self.handle_shutdown)
self.server.add_command_handler(
'START_SCAN', self.handle_start_scan)
self.db.setup()
async def cleanup(self):
self.db.cleanup()
await super().cleanup()
async def run(self):
await self._shutting_down.wait()
logger.info("Shutting down...")
self._shutdown_complete.set()
def get_session(self, session_id):
try:
return self.sessions[session_id]
except KeyError:
raise InvalidSessionError
def publish_mutation(self, mutation):
for session in self.sessions.values():
session.publish_mutation(mutation)
def handle_start_session(self, client_address, flags):
client_stub = ipc.Stub(self.event_loop, client_address)
connect_task = self.event_loop.create_task(client_stub.connect())
session = Session(self.event_loop, client_stub, flags)
connect_task.add_done_callback(
functools.partial(self._client_connected, session))
self.sessions[session.id] = session
# Send initial mutations to build up the current pipeline
# state.
for mutation in self.db.initial_mutations():
session.publish_mutation(mutation)
return session.id
def _client_connected(self, session, connect_task):
assert connect_task.done()
exc = connect_task.exception()
if exc is not None:
logger.error("Failed to connect to callback client: %s", exc)
return
session.callback_stub_connected()
def handle_end_session(self, session_id):
session = self.get_session(session_id)
session.cleanup()
del self.sessions[session_id]
async def handle_shutdown(self):
logger.info("Shutdown received.")
self._shutting_down.set()
logger.info("Waiting for shutdown to complete...")
await self._shutdown_complete.wait()
logger.info("Shutdown complete.")
async def handle_start_scan(self, session_id):
self.get_session(session_id)
return self.db.start_scan()
class InstrumentDBProcess(InstrumentDBProcessMixin, core.ProcessImpl):
pass

@ -11,7 +11,6 @@ from .pitch import Pitch
from . import model
from . import state
from . import commands
from . import instruments
from . import mutations
from . import pipeline_graph
from . import misc
@ -21,27 +20,17 @@ logger = logging.getLogger(__name__)
class SetBeatTrackInstrument(commands.Command):
instrument_type = core.Property(str)
instrument_args = core.DictProperty()
instrument = core.Property(str)
def __init__(self, instrument_type=None, instrument_args=None,
state=None):
def __init__(self, instrument=None, state=None):
super().__init__(state=state)
if state is None:
self.instrument_type = instrument_type
self.instrument_args.update(instrument_args)
self.instrument = instrument
def run(self, track):
assert isinstance(track, BeatTrack)
if self.instrument_type == 'SoundFontInstrument':
instr = instruments.SoundFontInstrument(**self.instrument_args)
elif self.instrument_type == 'SampleInstrument':
instr = instruments.SampleInstrument(**self.instrument_args)
else:
raise ValueError(self.instrument_type)
track.instrument = instr
track.instrument = self.instrument
track.instrument_node.update_pipeline()
commands.Command.register_command(SetBeatTrackInstrument)
@ -182,11 +171,7 @@ class BeatTrack(model.BeatTrack, MeasuredTrack):
if state is None:
if instrument is None:
self.instrument = instruments.SoundFontInstrument(
name="Default Drum",
path='/usr/share/sounds/sf2/FluidR3_GM.sf2',
bank=128,
preset=0)
self.instrument = 'sf2:/usr/share/sounds/sf2/FluidR3_GM.sf2?bank=128&preset=0'
else:
self.instrument = instrument

@ -1,88 +0,0 @@
#!/usr/bin/python3
from . import model
from . import state
from . import mutations
class Instrument(model.Instrument, state.StateBase):
def __init__(self, name=None, library_id=None, state=None):
super().__init__(state)
if state is None:
self.name = name
self.library_id = library_id
def __str__(self):
return self.name
@property
def track(self):
return self.parent
@property
def sheet(self):
return self.track.sheet
@property
def project(self):
return self.track.project
@property
def pipeline_node_id(self):
return '%s-instr' % self.id
state.StateBase.register_class(Instrument)
class SoundFontInstrument(model.SoundFontInstrument, Instrument):
def __init__(self, path=None, bank=None, preset=None, state=None, **kwargs):
super().__init__(state=state, **kwargs)
if state is None:
self.path = path
self.bank = bank
self.preset = preset
def __str__(self):
return '%s (%s, bank %d, preset %d)' % (
self.name, self.path, self.bank, self.preset)
def __eq__(self, other):
if not isinstance(other, SoundFontInstrument):
return False
return (self.path, self.bank, self.preset) == (other.path, other.bank, other.preset)
def add_to_pipeline(self):
self.sheet.handle_pipeline_mutation(
mutations.AddNode(
'fluidsynth', self.pipeline_node_id, self.name,
soundfont_path=self.path,
bank=self.bank,
preset=self.preset))
def remove_from_pipeline(self):
self.sheet.handle_pipeline_mutation(
mutations.RemoveNode(self.pipeline_node_id))
state.StateBase.register_class(SoundFontInstrument)
class SampleInstrument(model.SampleInstrument, Instrument):
def __init__(self, path=None, state=None, **kwargs):
super().__init__(state=state, **kwargs)
if state is None:
self.path = path
def __str__(self):
return '%s (%s)' % (self.name, self.path)
def add_to_pipeline(self):
self.sheet.handle_pipeline_mutation(
mutations.AddNode(
'sample_player', self.pipeline_node_id, self.name,
sample_path=self.path))
def remove_from_pipeline(self):
self.sheet.handle_pipeline_mutation(
mutations.RemoveNode(self.pipeline_node_id))
state.StateBase.register_class(SampleInstrument)

@ -12,21 +12,6 @@ from . import time_signature
from . import misc
class Instrument(core.ObjectBase):
name = core.Property(str)
library_id = core.Property(str, allow_none=True)
class SoundFontInstrument(Instrument):
path = core.Property(str)
bank = core.Property(int)
preset = core.Property(int)
class SampleInstrument(Instrument):
path = core.Property(str)
class Track(core.ObjectBase):
name = core.Property(str)
@ -153,7 +138,7 @@ class ScoreMeasure(Measure):
class ScoreTrack(MeasuredTrack):
instrument = core.ObjectProperty(cls=Instrument)
instrument = core.Property(str)
transpose_octaves = core.Property(int, default=0)
@ -175,7 +160,7 @@ class BeatMeasure(Measure):
class BeatTrack(MeasuredTrack):
instrument = core.ObjectProperty(cls=Instrument)
instrument = core.Property(str)
pitch = core.Property(pitch.Pitch)

@ -4,6 +4,7 @@ import io
import logging
from xml.etree import ElementTree
from noisicaa import instrument_db
from noisicaa import core
from noisicaa import node_db
@ -488,21 +489,11 @@ class InstrumentPipelineGraphNode(
connection.add_to_pipeline()
def add_to_pipeline(self):
instr = self.track.instrument
if isinstance(instr, model.SoundFontInstrument):
self.sheet.handle_pipeline_mutation(
mutations.AddNode(
'fluidsynth', self.pipeline_node_id, self.name,
soundfont_path=instr.path,
bank=instr.bank,
preset=instr.preset))
elif isinstance(instr, model.SampleInstrument):
self.sheet.handle_pipeline_mutation(
mutations.AddNode(
'sample_player', self.pipeline_node_id, self.name,
sample_path=instr.path))
node_cls, node_args = instrument_db.parse_uri(self.track.instrument)
self.sheet.handle_pipeline_mutation(
mutations.AddNode(
node_cls, self.pipeline_node_id, self.name,
**node_args))
self.set_initial_parameters()

@ -20,7 +20,6 @@ from .time import Duration
from . import model
from . import state
from . import commands
from . import instruments
from . import sheet
from . import misc
@ -232,18 +231,16 @@ class BaseProject(model.Project, state.RootMixin, state.StateBase):
for mref in s.property_track.measure_list:
mref.measure.bpm = 140
instr1 = instruments.SoundFontInstrument(
name="Flute",
path='/usr/share/sounds/sf2/FluidR3_GM.sf2', bank=0, preset=73)
track1 = ScoreTrack(
name="Track 1", instrument=instr1, num_measures=5)
name="Track 1",
instrument='sf2:/usr/share/sounds/sf2/FluidR3_GM.sf2?bank=0&preset=73',
num_measures=5)
s.add_track(s.master_group, 0, track1)
instr2 = instruments.SoundFontInstrument(
name="Yamaha Grand Piano",
path='/usr/share/sounds/sf2/FluidR3_GM.sf2', bank=0, preset=0)
track2 = ScoreTrack(
name="Track 2", instrument=instr2, num_measures=5)
name="Track 2",
instrument='sf2:/usr/share/sounds/sf2/FluidR3_GM.sf2?bank=0&preset=0',
num_measures=5)
s.add_track(s.master_group, 1, track2)
track1.measure_list[0].measure.notes.append(

@ -13,7 +13,6 @@ from .time import Duration
from . import model
from . import state
from . import commands
from . import instruments
from . import mutations
from . import pipeline_graph
from . import misc
@ -22,27 +21,17 @@ logger = logging.getLogger(__name__)
class SetInstrument(commands.Command):
instrument_type = core.Property(str)
instrument_args = core.DictProperty()
instrument = core.Property(str)
def __init__(self, instrument_type=None, instrument_args=None,
state=None):
def __init__(self, instrument=None, state=None):
super().__init__(state=state)
if state is None:
self.instrument_type = instrument_type
self.instrument_args.update(instrument_args)
self.instrument = instrument
def run(self, track):
assert isinstance(track, MeasuredTrack)
if self.instrument_type == 'SoundFontInstrument':
instr = instruments.SoundFontInstrument(**self.instrument_args)
elif self.instrument_type == 'SampleInstrument':
instr = instruments.SampleInstrument(**self.instrument_args)
else:
raise ValueError(self.instrument_type)
track.instrument = instr
track.instrument = self.instrument
track.instrument_node.update_pipeline()
commands.Command.register_command(SetInstrument)
@ -365,11 +354,7 @@ class ScoreTrack(model.ScoreTrack, MeasuredTrack):
if state is None:
if instrument is None:
self.instrument = instruments.SoundFontInstrument(
name="Default Piano",
path='/usr/share/sounds/sf2/FluidR3_GM.sf2',
bank=0,
preset=0)
self.instrument = 'sf2:/usr/share/sounds/sf2/FluidR3_GM.sf2?bank=0&preset=0'
else:
self.instrument = instrument

@ -6,7 +6,6 @@ from . import project
from . import sheet
from . import score_track
from . import track_group
from . import instruments
class SheetCommandTest(unittest.TestCase):
@ -56,9 +55,7 @@ class DeleteTrackTest(SheetCommandTest):
def test_track_with_instrument(self):
self.sheet.master_group.tracks.append(
score_track.ScoreTrack(name='Test'))
self.sheet.master_group.tracks[0].instrument = instruments.SoundFontInstrument(
name='Piano', path='/usr/share/sounds/sf2/FluidR3_GM.sf2',
bank=0, preset=0)
self.sheet.master_group.tracks[0].instrument = 'sf2:/usr/share/sounds/sf2/FluidR3_GM.sf2?bank=0&preset=0'
cmd = sheet.RemoveTrack(
track_id=self.sheet.master_group.tracks[0].id)

@ -9,12 +9,12 @@ from PyQt5 import QtCore
from PyQt5 import QtWidgets
from noisicaa import audioproc
from noisicaa import instrument_db
from noisicaa import node_db
from noisicaa import devices
from ..exceptions import RestartAppException, RestartAppCleanException
from ..constants import EXIT_EXCEPTION, EXIT_RESTART, EXIT_RESTART_CLEAN
from .editor_window import EditorWindow
from ..instr import library
from . import project_registry
from . import pipeline_perf_monitor
@ -79,6 +79,22 @@ class NodeDBClient(node_db.NodeDBClientMixin, NodeDBClientImpl):
pass
class InstrumentDBClientImpl(object):
def __init__(self, event_loop, server):
super().__init__()
self.event_loop = event_loop
self.server = server
async def setup(self):
pass
async def cleanup(self):
pass
class InstrumentDBClient(instrument_db.InstrumentDBClientMixin, InstrumentDBClientImpl):
pass
class BaseEditorApp(QtWidgets.QApplication):
def __init__(self, process, runtime_settings, settings=None):
super().__init__(['noisicaä'])
@ -104,6 +120,7 @@ class BaseEditorApp(QtWidgets.QApplication):
self.audioproc_client = None
self.audioproc_process = None
self.node_db = None
self.instrument_db = None
async def setup(self):
self.default_style = self.style().objectName()
@ -114,6 +131,7 @@ class BaseEditorApp(QtWidgets.QApplication):
self.setStyle(style)
await self.createNodeDB()
await self.createInstrumentDB()
self.project_registry = project_registry.ProjectRegistry(
self.process.event_loop, self.process.manager, self.node_db)
@ -140,6 +158,16 @@ class BaseEditorApp(QtWidgets.QApplication):
await self.audioproc_client.cleanup()
self.audioproc_client = None
if self.instrument_db is not None:
await self.instrument_db.disconnect(shutdown=True)
await self.instrument_db.cleanup()
self.instrument_db = None
if self.node_db is not None:
await self.node_db.disconnect(shutdown=True)
await self.node_db.cleanup()
self.node_db = None
if self.midi_hub is not None:
self.midi_hub.stop()
self.midi_hub = None
@ -173,6 +201,15 @@ class BaseEditorApp(QtWidgets.QApplication):
await self.node_db.setup()
await self.node_db.connect(node_db_address)
async def createInstrumentDB(self):
instrument_db_address = await self.process.manager.call(
'CREATE_INSTRUMENT_DB_PROCESS')
self.instrument_db = InstrumentDBClient(
self.process.event_loop, self.process.server)
await self.instrument_db.setup()
await self.instrument_db.connect(instrument_db_address)
def dumpSettings(self):
for key in self.settings.allKeys():
value = self.settings.value(key)
@ -239,9 +276,6 @@ class EditorApp(BaseEditorApp):
await super().setup()
logger.info("Creating InstrumentLibrary.")
self.instrument_library = library.InstrumentLibrary()
logger.info("Creating PipelinePerfMonitor.")
self.pipeline_perf_monitor = pipeline_perf_monitor.PipelinePerfMonitor(self)

@ -13,10 +13,10 @@ from PyQt5 import QtGui
from PyQt5 import QtWidgets
from noisicaa import audioproc
from noisicaa import instrument_db
from .piano import PianoWidget
from . import ui_base
from ..instr import library
logger = logging.getLogger(__name__)
@ -32,13 +32,14 @@ logger = logging.getLogger(__name__)
# - add/remove
class InstrumentListItem(QtWidgets.QListWidgetItem):
def __init__(self, parent, instrument):
def __init__(self, parent, description):
super().__init__(parent, 0)
self.instrument = instrument
self.description = description
self.setText(description.display_name)
class InstrumentLibraryDialog(ui_base.CommonMixin, QtWidgets.QDialog):
instrumentChanged = QtCore.pyqtSignal(library.Instrument)
instrumentChanged = QtCore.pyqtSignal(instrument_db.InstrumentDescription)
def __init__(self, parent=None, selectButton=False, **kwargs):
super().__init__(parent=parent, **kwargs)
@ -48,6 +49,7 @@ class InstrumentLibraryDialog(ui_base.CommonMixin, QtWidgets.QDialog):
self._pipeline_lock = asyncio.Lock()
self._pipeline_mixer_id = None
self._pipeline_instrument_id = None
self._pipeline_event_source_id = None
self._instrument = None
@ -97,18 +99,6 @@ class InstrumentLibraryDialog(ui_base.CommonMixin, QtWidgets.QDialog):
self.instrument_name = QtWidgets.QLineEdit(self, readOnly=True)
form_layout.addRow("Name", self.instrument_name)
self.instrument_type = QtWidgets.QLineEdit(self, readOnly=True)
form_layout.addRow("Type", self.instrument_type)
self.instrument_path = QtWidgets.QLineEdit(self, readOnly=True)
form_layout.addRow("Path", self.instrument_path)
self.instrument_collection = QtWidgets.QLineEdit(self, readOnly=True)
form_layout.addRow("Collection", self.instrument_collection)
self.instrument_location = QtWidgets.QLineEdit(self, readOnly=True)
form_layout.addRow("Location", self.instrument_location)
layout.addStretch(1)
self.piano = PianoWidget(self, self.app)
@ -178,11 +168,12 @@ class InstrumentLibraryDialog(ui_base.CommonMixin, QtWidgets.QDialog):
self._pipeline_mixer_id)
self._pipeline_mixer_id = None
async def addInstrumentToPipeline(self, node_type, **args):
async def addInstrumentToPipeline(self, uri):
assert self._pipeline_instrument_id is None
node_cls, node_args = instrument_db.parse_uri(uri)
self._pipeline_instrument_id = await self.audioproc_client.add_node(
node_type, **args)
node_cls, **node_args)
await self.audioproc_client.connect_ports(
self._pipeline_instrument_id, 'out',
self._pipeline_mixer_id, 'in')
@ -220,64 +211,36 @@ class InstrumentLibraryDialog(ui_base.CommonMixin, QtWidgets.QDialog):
def updateInstrumentList(self):
self.instruments_list.clear()
for idx, instr in enumerate(self.library.instruments):
item = InstrumentListItem(self.instruments_list, instr)
item.setText(instr.name)
self.instruments_list.addItem(item)
for description in self.app.instrument_db.instruments:
self.instruments_list.addItem(
InstrumentListItem(self.instruments_list, description))
def selectInstrument(self, instr_id):
def selectInstrument(self, uri):
for idx in range(self.instruments_list.count()):
item = self.instruments_list.item(idx)
if item.instrument.id == instr_id:
if item.description.uri == uri:
self.instruments_list.setCurrentRow(idx)
break
def onInstrumentItemSelected(self, item):
if item is None:
self.instrument_name.setText("")
self.instrument_collection.setText("")
self.instrument_type.setText("")
self.instrument_path.setText("")
self.instrument_location.setText("")
return
self.call_async(self.setCurrentInstrument(item.instrument))
self.call_async(self.setCurrentInstrument(item.description))
async def setCurrentInstrument(self, instr):
async def setCurrentInstrument(self, description):
await self.removeInstrumentFromPipeline()
self.instrument_name.setText(instr.name)
if instr.collection is not None:
self.instrument_collection.setText(instr.collection.name)
else:
self.instrument_collection.setText("")
if isinstance(instr, library.SoundFontInstrument):
self.instrument_type.setText("SoundFont")
self.instrument_path.setText(instr.path)
self.instrument_location.setText(
"bank %d, preset %d" % (instr.bank, instr.preset))
await self.addInstrumentToPipeline(
'fluidsynth',
soundfont_path=instr.path,
bank=instr.bank, preset=instr.preset)
elif isinstance(instr, library.SampleInstrument):
self.instrument_type.setText("Sample")
self.instrument_path.setText(instr.path)
self.instrument_location.setText("")
self.instrument_name.setText(description.display_name)
await self.addInstrumentToPipeline(
'sample_player',
sample_path=instr.path)
await self.addInstrumentToPipeline(description.uri)
self.piano.setVisible(True)
self.piano.setFocus(Qt.OtherFocusReason)
self._instrument = instr
self.instrumentChanged.emit(instr)
self._instrument = description
self.instrumentChanged.emit(description)
def onInstrumentSearchChanged(self, text):
for idx in range(self.instruments_list.count()):
@ -301,7 +264,6 @@ class InstrumentLibraryDialog(ui_base.CommonMixin, QtWidgets.QDialog):
if not path:
return
self.library.add_soundfont(path)
self.updateInstrumentList()
def keyPressEvent(self, event):

@ -7,10 +7,6 @@ from noisicaa.music import project_client
logger = logging.getLogger(__name__)
class SoundFontInstrument(
model.SoundFontInstrument, project_client.ObjectProxy): pass
class SampleInstrument(
model.SampleInstrument, project_client.ObjectProxy): pass
class MeasureReference(model.MeasureReference, project_client.ObjectProxy): pass
class Note(model.Note, project_client.ObjectProxy):
@ -84,8 +80,6 @@ class Project(model.Project, project_client.ObjectProxy):
cls_map = {
'SoundFontInstrument': SoundFontInstrument,
'SampleInstrument': SampleInstrument,
'MeasureReference': MeasureReference,
'Note': Note,
'ScoreMeasure': ScoreMeasure,

@ -144,7 +144,7 @@ class ScoreMeasureItemImpl(base_track_item.MeasureItemImpl):
# TODO: update when changed
text = self._instr_item = QtWidgets.QGraphicsSimpleTextItem(layer)
text.setText(
track.instrument.name if track.instrument is not None else "")
track.instrument if track.instrument is not None else "")
text.setPos(0, 20)