From 1b5bec6f53d507ec1cca34c78f3f1e1cf18697ae Mon Sep 17 00:00:00 2001 From: Nils <> Date: Thu, 16 Dec 2021 18:39:04 +0100 Subject: [PATCH] cbox update and out support in the template --- template/calfbox/cleanpythonbuild.sh | 2 +- template/calfbox/jackio.c | 8 -- .../calfbox/sampler_api_load_stress_test.py | 110 ++++++++++++++++++ .../send_pattern_to_midi_out_example.py | 39 +++++++ template/engine/api.py | 3 + template/engine/input_midi.py | 10 +- 6 files changed, 162 insertions(+), 10 deletions(-) create mode 100755 template/calfbox/sampler_api_load_stress_test.py create mode 100644 template/calfbox/send_pattern_to_midi_out_example.py diff --git a/template/calfbox/cleanpythonbuild.sh b/template/calfbox/cleanpythonbuild.sh index de408d9..602a0f4 100755 --- a/template/calfbox/cleanpythonbuild.sh +++ b/template/calfbox/cleanpythonbuild.sh @@ -5,7 +5,7 @@ rm build -rf set -e sh autogen.sh ./configure --prefix=/usr --without-python -make +make CFLAGS="-O0 -g" python3 setup.py build sudo python3 setup.py install sudo make install diff --git a/template/calfbox/jackio.c b/template/calfbox/jackio.c index 4643b9a..47dc64a 100644 --- a/template/calfbox/jackio.c +++ b/template/calfbox/jackio.c @@ -245,18 +245,10 @@ static int process_cb(jack_nframes_t nframes, void *arg) cbox_midi_merger_render(&midiout->hdr.merger); if (midiout->hdr.buffer.count) { - uint8_t tmp_data[4]; for (uint32_t i = 0; i < midiout->hdr.buffer.count; i++) { const struct cbox_midi_event *event = cbox_midi_buffer_get_event(&midiout->hdr.buffer, i); const uint8_t *pdata = cbox_midi_event_get_data(event); - if ((pdata[0] & 0xF0) == 0x90 && !pdata[2] && event->size == 3) - { - tmp_data[0] = pdata[0] & ~0x10; - tmp_data[1] = pdata[1]; - tmp_data[2] = pdata[2]; - pdata = tmp_data; - } if (jack_midi_event_write(pbuf, event->time, pdata, event->size)) { g_warning("MIDI buffer overflow on JACK output port '%s'", midiout->hdr.name); diff --git a/template/calfbox/sampler_api_load_stress_test.py b/template/calfbox/sampler_api_load_stress_test.py new file mode 100755 index 0000000..01e4755 --- /dev/null +++ b/template/calfbox/sampler_api_load_stress_test.py @@ -0,0 +1,110 @@ +#! /usr/bin/env python3 +# -*- coding: utf-8 -*- + +NUMBER_OF_INSTRUMENTS = 240 +""" +2021-11-13 Benchmark: +NumberOfInstruments,StartInSeconds,QuitInSeconds +30, 5, 3 +60, 10, 6 +120, 21, 12 +240, 42, 25 + +Conclusion: Linear time. +""" + +from calfbox import cbox + +import atexit +from datetime import datetime + +#Capture Ctlr+C / SIGINT and let @atexit handle the rest. +import signal +import sys +def signal_handler(sig, frame): + sys.exit(0) #atexit will trigger +signal.signal(signal.SIGINT, signal_handler) + + +def cmd_dumper(cmd, fb, args): + #print ("%s(%s)" % (cmd, ",".join(list(map(repr,args))))) + pass + +def stopSession(): + """This got registered with atexit in the nsm new or open callback above. + will handle all python exceptions, but not segfaults of C modules. """ + print() + print("Starting Quit through @atexit, stopSession") + starttime = datetime.now() + #Don't do that. We are just a client. + #cbox.Transport.stop() + #print("@atexit: Calfbox Transport stopped ") + cbox.stop_audio() + print("@atexit: Calfbox Audio stopped ") + cbox.shutdown_engine() + print("@atexit: Calfbox Engine shutdown ") + endtime = datetime.now() - starttime + print (f"Shutdown took {endtime.seconds} seconds for {NUMBER_OF_INSTRUMENTS} instruments") + +cbox.init_engine() +cbox.start_audio(cmd_dumper) +atexit.register(stopSession) #this will handle all python exceptions, but not segfaults of C modules. + +scenes = {} +jackAudioOutLefts = {} +jackAudioOutRights = {} +outputMergerRouters = {} +routerToGlobalSummingStereoMixers = {} +lmixUuid = cbox.JackIO.create_audio_output('left_mix', "#1") #add "#1" as second parameter for auto-connection to system out 1 +rmixUuid = cbox.JackIO.create_audio_output('right_mix', "#2") #add "#2" as second parameter for auto-connection to system out 2 +cboxMidiPortUids = {} +sfzSamplerLayers = {} +instrumentLayers = {}\ + + +print (f"Creating {NUMBER_OF_INSTRUMENTS} instruments") +starttime = datetime.now() + +for i in range(NUMBER_OF_INSTRUMENTS): + scenes[i] = cbox.Document.get_engine().new_scene() + scenes[i].clear() + + #instrumentLayer = scenes[i].status().layers[0].get_instrument() + sfzSamplerLayers[i] = scenes[i].add_new_instrument_layer(str(i), "sampler") #"sampler" is the cbox sfz engine + scenes[i].status().layers[0].get_instrument().engine.load_patch_from_string(0, "", "", "") #fill with null instruments + + jackAudioOutLefts[i] = cbox.JackIO.create_audio_output(str(i) +"_L") + jackAudioOutRights[i] = cbox.JackIO.create_audio_output(str(i) +"_R") + + outputMergerRouters[i] = cbox.JackIO.create_audio_output_router(jackAudioOutLefts[i], jackAudioOutRights[i]) + outputMergerRouters[i].set_gain(-3.0) + #instrumentLayer = sfzSamplerLayers[i].get_instrument() + instrumentLayers[i] = scenes[i].status().layers[0].get_instrument() + instrumentLayers[i].get_output_slot(0).rec_wet.attach(outputMergerRouters[i]) #output_slot is 0 based and means a pair. Most sfz instrument have only one stereo pair. #TODO: And what if not? + + routerToGlobalSummingStereoMixers[i] = cbox.JackIO.create_audio_output_router(lmixUuid, rmixUuid) + routerToGlobalSummingStereoMixers[i].set_gain(-3.0) + instrument = sfzSamplerLayers[i].get_instrument() + instrument.get_output_slot(0).rec_wet.attach(routerToGlobalSummingStereoMixers[i]) + + #Create Midi Input Port + cboxMidiPortUids[i] = cbox.JackIO.create_midi_input(str(i) + "midi_in") + cbox.JackIO.set_appsink_for_midi_input(cboxMidiPortUids[i], True) #This sounds like a program wide sink, but it is needed for every port. + cbox.JackIO.route_midi_input(cboxMidiPortUids[i], scenes[i].uuid) #Route midi input to the scene. Without this we have no sound, but the python processor would still work. + + #Actually load the instrument + programNumber = 0 + #program = instrumentLayers[i].engine.load_patch_from_tar(programNumber, "bug.tar", f'Saw{1}.sfz', f'Saw{i+1}') #tar_name, sfz_name, display_name + program = instrumentLayers[i].engine.load_patch_from_string(programNumber, ".", "", " sample=*saw") #fill with null instruments + print (f"[{i}]", program.status()) + instrumentLayers[i].engine.set_patch(1, programNumber) #1 is the channel, counting from 1. #TODO: we want this to be on all channels. + +endtime = datetime.now() - starttime +print (f"Creation took {endtime.seconds} seconds for {NUMBER_OF_INSTRUMENTS} instruments") + +print() +print("Press Ctrl+C for a controlled shutdown.") +print() + +while True: + cbox.call_on_idle(cmd_dumper) diff --git a/template/calfbox/send_pattern_to_midi_out_example.py b/template/calfbox/send_pattern_to_midi_out_example.py new file mode 100644 index 0000000..d6412f0 --- /dev/null +++ b/template/calfbox/send_pattern_to_midi_out_example.py @@ -0,0 +1,39 @@ +#! /usr/bin/env python3 +# -*- coding: utf-8 -*- + +from calfbox import cbox + +def cmd_dumper(cmd, fb, args): + print ("%s(%s)" % (cmd, ",".join(list(map(repr,args))))) + +cbox.init_engine() +cbox.start_audio(cmd_dumper) + +outportname = "CboxSendPattern" +cboxMidiOutUuid = cbox.JackIO.create_midi_output(outportname) #Add a named midi out port +cbox.JackIO.rename_midi_output(cboxMidiOutUuid, outportname) #For good measure. +outputScene = cbox.Document.get_engine().new_scene() #Create a new scene that will play the pattern. The pattern is not saved in the scene, it is not a track or so. +outputScene.clear() #For good measure. +outputScene.add_new_midi_layer(cboxMidiOutUuid) #Connect the scene to our midi output port. Without this there will be no midi out. + + +# Send 8 pitches 0x90 with velocity 0 +# Create a binary blob that contains the MIDI events +pblob = bytes() +for pitch in range(0,8): + # note on + pblob += cbox.Pattern.serialize_event(1, 0x90, pitch, 0) #tick in pattern, midi, pitch, velocity +# Create a new pattern object using events from the blob +allNoteOnZeroPattern = cbox.Document.get_song().pattern_from_blob(pblob, 0) #0 ticks. + + +print ("\nThis example sends midi events from a pattern without any tracks. Rolling transport or not doesn't matter.") +print("Ready!") +counter = 0 #To add delay +while True: + cbox.call_on_idle(cmd_dumper) + if counter > 10**5 * 4 : + print ("Send pattern") + outputScene.play_pattern(allNoteOnZeroPattern, 150.0) #150 tempo + counter = 0 + counter += 1 diff --git a/template/engine/api.py b/template/engine/api.py index df252a5..6c976d6 100644 --- a/template/engine/api.py +++ b/template/engine/api.py @@ -381,6 +381,9 @@ def playPause(): cbox.Transport.play() #It takes a few ms for playbackStatus to catch up. If you call it here again it will not be updated. +def stop(): + cbox.Transport.stop() + def getPlaybackTicks()->int: return cbox.Transport.status().pos_ppqn diff --git a/template/engine/input_midi.py b/template/engine/input_midi.py index fb46a39..837e31c 100644 --- a/template/engine/input_midi.py +++ b/template/engine/input_midi.py @@ -101,6 +101,14 @@ class MidiInput(object): raise ValueError("Channels are from 1 to 16 (inclusive). You sent " + str(channel)) self.realtimeMidiThroughLayer.set_out_channel(channel) + def connectToHardware(self, portPattern:str): + if not portPattern: + portPattern = ".*" + + hardwareMidiPorts = set(cbox.JackIO.get_ports(portPattern, cbox.JackIO.MIDI_TYPE, cbox.JackIO.PORT_IS_SOURCE | cbox.JackIO.PORT_IS_PHYSICAL)) + for hp in hardwareMidiPorts: + cbox.JackIO.port_connect(hp, cbox.JackIO.status().client_name + ":" + self.portName) + class MidiProcessor(object): """ @@ -245,7 +253,7 @@ class MidiProcessor(object): def notePrinter(self, state:bool): if state: def _printer(timestamp, channel, note, velocity): - print(f"[{timestamp}] Chan: {channel} Note: {pitch.midi_notenames_english[note]}: Vel: {velocity}") + print(f"[{timestamp}] Chan: {channel} Note: {note} -> {pitch.midi_notenames_english[note]}: Vel: {velocity}") self.callbacks3[(MidiProcessor.SIMPLE_EVENT, MidiProcessor.M_NOTE_ON)] = _printer else: try: