~hp/orbit.lv2

e05d46fb0254680bd39715db4892610bf3326742 — Hanspeter Portner 3 months ago edd9aad
Add orbit:midiClock and orbit:midiSync plugins

orbit:midiSync ist just a placeholder for now, needs to be implemented.
M meson.build => meson.build +6 -2
@@ 49,7 49,9 @@ dsp_srcs = [
  join_paths('src', 'orbit_quantum.c'),
  join_paths('src', 'orbit_subspace.c'),
  join_paths('src', 'orbit_monitor.c'),
  join_paths('src', 'orbit_timecapsule.c')
  join_paths('src', 'orbit_timecapsule.c'),
  join_paths('src', 'orbit_midi_sync.c'),
  join_paths('src', 'orbit_midi_clock.c')
]

c_args = [


@@ 103,7 105,9 @@ if build_tests
        'http://open-music-kontrollers.ch/lv2/orbit#quantum',
        'http://open-music-kontrollers.ch/lv2/orbit#subspace',
        'http://open-music-kontrollers.ch/lv2/orbit#monitor',
        'http://open-music-kontrollers.ch/lv2/orbit#timecapsule'])
        'http://open-music-kontrollers.ch/lv2/orbit#timecapsule',
        'http://open-music-kontrollers.ch/lv2/orbit#midiSync',
        'http://open-music-kontrollers.ch/lv2/orbit#midiClock'])
  endif

	if reuse.found() and not meson.is_cross_build()

M meson_options.txt => meson_options.txt +1 -1
@@ 9,4 9,4 @@ option('lv2libdir',
	type : 'string',
	value : 'lib/lv2')

option('version', type : 'string', value : '0.1.705')
option('version', type : 'string', value : '0.1.707')

M orbit.lv2/orbit.h => orbit.lv2/orbit.h +4 -0
@@ 35,6 35,8 @@
#define ORBIT_MONITOR_URI						ORBIT_URI"#monitor"
#define ORBIT_TIMECAPSULE_URI				ORBIT_URI"#timecapsule"
#define ORBIT_QUANTUM_URI						ORBIT_URI"#quantum"
#define ORBIT_MIDI_SYNC_URI					ORBIT_URI"#midiSync"
#define ORBIT_MIDI_CLOCK_URI				ORBIT_URI"#midiClock"

extern const LV2_Descriptor orbit_looper;
extern const LV2_Descriptor orbit_click;


@@ 44,5 46,7 @@ extern const LV2_Descriptor orbit_subspace;
extern const LV2_Descriptor orbit_monitor;
extern const LV2_Descriptor orbit_timecapsule;
extern const LV2_Descriptor orbit_quantum;
extern const LV2_Descriptor orbit_midi_sync;
extern const LV2_Descriptor orbit_midi_clock;

#endif // _ORBIT_LV2_H

M src/orbit.c => src/orbit.c +4 -0
@@ 26,6 26,10 @@ lv2_descriptor(uint32_t index)
			return &orbit_quantum;
		case 7:
			return &orbit_monitor;
		case 8:
			return &orbit_midi_sync;
		case 9:
			return &orbit_midi_clock;
		default:
			return NULL;
	}

A src/orbit_midi_clock.c => src/orbit_midi_clock.c +308 -0
@@ 0,0 1,308 @@
/*
 * SPDX-FileCopyrightText: Hanspeter Portner <dev@open-music-kontrollers.ch>
 * SPDX-License-Identifier: Artistic-2.0
 */

#include <stdio.h>
#include <stdlib.h>
#include <math.h>

#include <orbit.lv2/orbit.h>
#include <timely.lv2/timely.h>
#include <props.lv2/props.h>

#define MAX_NPROPS 1

typedef struct _plugstate_t plugstate_t;
typedef struct _plughandle_t plughandle_t;

struct _plugstate_t {
	int32_t song;
};

struct _plughandle_t {
	struct {
		LV2_URID midi_event;
	} urid;

	LV2_URID_Map *map;
	LV2_Atom_Forge forge;

	LV2_Log_Log *log;
	LV2_Log_Logger logger;

	timely_t timely;

	const LV2_Atom_Sequence *event_in;
	LV2_Atom_Sequence *event_out;

	plugstate_t state;
	plugstate_t stash;

	PROPS_T(props, MAX_NPROPS);
	
	bool rolling;
	LV2_Atom_Forge_Ref ref;
};

static const props_def_t defs [MAX_NPROPS] = {
	{
		.property = ORBIT_URI"#song",
		.offset = offsetof(plugstate_t, song),
		.type = LV2_ATOM__Int,
	}
};

static void
_midi(plughandle_t *handle, int64_t frames, const uint8_t *msg, size_t msg_len)
{
	LV2_Atom_Forge_Ref ref = handle->ref;

	if(ref)
	{
		ref = lv2_atom_forge_frame_time(&handle->forge, frames);
	}
	if(ref)
	{
		ref = lv2_atom_forge_atom(&handle->forge, msg_len, handle->urid.midi_event);
	}
	if(ref)
	{
		ref = lv2_atom_forge_raw(&handle->forge, msg, msg_len);
	}
	if(ref)
	{
		lv2_atom_forge_pad(&handle->forge, msg_len);
	}

	handle->ref = ref;
}

static void
_cb(timely_t *timely, int64_t frames, LV2_URID type, void *data)
{
	plughandle_t *handle = data;

	if(type == TIMELY_URI_SPEED(timely))
	{
		handle->rolling = TIMELY_SPEED(timely) > 0.f ? true : false;

		if(handle->rolling)
		{
			const uint64_t beats = TIMELY_BAR(timely) * TIMELY_BEATS_PER_BAR(timely)
				+ TIMELY_BAR_BEAT(timely);
			const uint16_t midi_beats = beats / 6; // 1 MIDI Beat spans 6 MIDI Clocks

			const uint8_t song_sel [] = {
				LV2_MIDI_MSG_SONG_SELECT,
				handle->state.song
			};

			_midi(handle, frames, song_sel, sizeof(song_sel));

			const uint8_t song_pos [] =  {
				LV2_MIDI_MSG_SONG_POS,
				midi_beats & 0x7f,
				midi_beats >> 7
			};

			_midi(handle, frames, song_pos, sizeof(song_pos));

			const uint8_t msg [] = {
  			LV2_MIDI_MSG_CONTINUE
			};

			_midi(handle, frames, msg, sizeof(msg));
		}
		else
		{
			const uint8_t msg [] = {
  			LV2_MIDI_MSG_STOP
			};

			_midi(handle, frames, msg, sizeof(msg));
		}
	}
	else if(type == TIMELY_URI_BAR_BEAT(timely))
	{
		if(handle->rolling)
		{
			const uint8_t msg [] = {
  			LV2_MIDI_MSG_CLOCK
			};

			_midi(handle, frames, msg, sizeof(msg));
		}
	}
}

static LV2_Handle
instantiate(const LV2_Descriptor* descriptor, double rate,
	const char *bundle_path UNUSED, const LV2_Feature *const *features)
{
	plughandle_t *handle = calloc(1, sizeof(plughandle_t));
	if(!handle)
	{
		return NULL;
	}
	mlock(handle, sizeof(plughandle_t));

	for(unsigned i=0; features[i]; i++)
	{
		if(!strcmp(features[i]->URI, LV2_URID__map))
		{
			handle->map = features[i]->data;
		}
		else if(!strcmp(features[i]->URI, LV2_LOG__log))
		{
			handle->log= features[i]->data;
		}
	}

	if(!handle->map)
	{
		fprintf(stderr,
			"%s: Host does not support urid:map\n", descriptor->URI);
		free(handle);
		return NULL;
	}

	if(handle->log)
	{
		lv2_log_logger_init(&handle->logger, handle->map, handle->log);
	}

	handle->urid.midi_event = handle->map->map(handle->map->handle, LV2_MIDI__MidiEvent);

	timely_mask_t mask = TIMELY_MASK_BAR_BEAT_WHOLE
		| TIMELY_MASK_SPEED;
	timely_init(&handle->timely, handle->map, rate, mask, _cb, handle);
	timely_set_multiplier(&handle->timely, 24.f);
	lv2_atom_forge_init(&handle->forge, handle->map);

	if(!props_init(&handle->props, descriptor->URI,
		defs, MAX_NPROPS, &handle->state, &handle->stash,
		handle->map, handle))
	{
		fprintf(stderr, "failed to initialize property structure\n");
		free(handle);
		return NULL;
	}

	return handle;
}

static void
connect_port(LV2_Handle instance, uint32_t port, void *data)
{
	plughandle_t *handle = (plughandle_t *)instance;

	switch(port)
	{
		case 0:
			handle->event_in = (const LV2_Atom_Sequence *)data;
			break;
		case 1:
			handle->event_out = (LV2_Atom_Sequence *)data;
			break;
		default:
			break;
	}
}

static void
run(LV2_Handle instance, uint32_t nsamples)
{
	plughandle_t *handle = instance;
	uint32_t last_t = 0;

	const uint32_t capacity = handle->event_out->atom.size;
	LV2_Atom_Forge_Frame frame;
	lv2_atom_forge_set_buffer(&handle->forge, (uint8_t *)handle->event_out, capacity);
	handle->ref = lv2_atom_forge_sequence_head(&handle->forge, &frame, 0);

	props_idle(&handle->props, &handle->forge, 0, &handle->ref);

	LV2_ATOM_SEQUENCE_FOREACH(handle->event_in, ev)
	{
		const LV2_Atom_Object *obj = (const LV2_Atom_Object *)&ev->body;
		if(!timely_advance(&handle->timely, obj, last_t, ev->time.frames))
		{
			props_advance(&handle->props, &handle->forge, ev->time.frames, obj, &handle->ref);
		}

		last_t = ev->time.frames;
	}

	timely_advance(&handle->timely, NULL, last_t, nsamples);

	if(handle->ref)
	{
		lv2_atom_forge_pop(&handle->forge, &frame);
	}
	else
	{
		lv2_atom_sequence_clear(handle->event_out);

		if(handle->log)
		{
			lv2_log_trace(&handle->logger, "forge buffer overflow\n");
		}
	}
}

static void
cleanup(LV2_Handle instance)
{
	plughandle_t *handle = instance;

	munlock(handle, sizeof(plughandle_t));
	free(handle);
}

static LV2_State_Status
_state_save(LV2_Handle instance, LV2_State_Store_Function store,
	LV2_State_Handle state, uint32_t flags,
	const LV2_Feature *const *features)
{
	plughandle_t *handle = instance;

	return props_save(&handle->props, store, state, flags, features);
}

static LV2_State_Status
_state_restore(LV2_Handle instance, LV2_State_Retrieve_Function retrieve,
	LV2_State_Handle state, uint32_t flags,
	const LV2_Feature *const *features)
{
	plughandle_t *handle = instance;

	return props_restore(&handle->props, retrieve, state, flags, features);
}

static const LV2_State_Interface state_iface = {
	.save = _state_save,
	.restore = _state_restore
};

static const void*
extension_data(const char* uri)
{
	if(!strcmp(uri, LV2_STATE__interface))
	{
		return &state_iface;
	}

	return NULL;
}

const LV2_Descriptor orbit_midi_clock = {
	.URI						= ORBIT_MIDI_CLOCK_URI,
	.instantiate		= instantiate,
	.connect_port		= connect_port,
	.activate				= NULL,
	.run						= run,
	.deactivate			= NULL,
	.cleanup				= cleanup,
	.extension_data	= extension_data
};

A src/orbit_midi_sync.c => src/orbit_midi_sync.c +260 -0
@@ 0,0 1,260 @@
/*
 * SPDX-FileCopyrightText: Hanspeter Portner <dev@open-music-kontrollers.ch>
 * SPDX-License-Identifier: Artistic-2.0
 */

#include <stdio.h>
#include <stdlib.h>
#include <math.h>

#include <orbit.lv2/orbit.h>
#include <timely.lv2/timely.h>
#include <props.lv2/props.h>

#define MAX_NPROPS 1

typedef struct _plugstate_t plugstate_t;
typedef struct _plughandle_t plughandle_t;

struct _plugstate_t {
	//FIXME
	int32_t mode;
};

struct _plughandle_t {
	struct {
		LV2_URID midi_event;
	} urid;

	LV2_URID_Map *map;
	LV2_Atom_Forge forge;

	LV2_Log_Log *log;
	LV2_Log_Logger logger;

	timely_t timely;

	const LV2_Atom_Sequence *event_in;
	LV2_Atom_Sequence *event_out;

	plugstate_t state;
	plugstate_t stash;

	PROPS_T(props, MAX_NPROPS);
	
	bool rolling;
	LV2_Atom_Forge_Ref ref;
};

static const props_def_t defs [MAX_NPROPS] = {
	{
		.property = ORBIT_URI"#quantum_mode",
		.offset = offsetof(plugstate_t, mode),
		.type = LV2_ATOM__Int,
	}
};

static void
_cb(timely_t *timely, int64_t frames UNUSED, LV2_URID type, void *data)
{
	plughandle_t *handle = data;

	if(type == TIMELY_URI_SPEED(timely))
	{
		handle->rolling = TIMELY_SPEED(timely) > 0.f ? true : false;

		if(handle->rolling)
		{
			//FIXME
		}
		else
		{
			//FIXME
		}
	}
	else if(type == TIMELY_URI_BAR_BEAT(timely))
	{
		if(handle->rolling)
		{
			//FIXME
		}
	}
	else if(type == TIMELY_URI_BAR(timely))
	{
		if(handle->rolling)
		{
			//FIXME
		}
	}
}

static LV2_Handle
instantiate(const LV2_Descriptor* descriptor, double rate,
	const char *bundle_path UNUSED, const LV2_Feature *const *features)
{
	plughandle_t *handle = calloc(1, sizeof(plughandle_t));
	if(!handle)
	{
		return NULL;
	}
	mlock(handle, sizeof(plughandle_t));

	for(unsigned i=0; features[i]; i++)
	{
		if(!strcmp(features[i]->URI, LV2_URID__map))
		{
			handle->map = features[i]->data;
		}
		else if(!strcmp(features[i]->URI, LV2_LOG__log))
		{
			handle->log= features[i]->data;
		}
	}

	if(!handle->map)
	{
		fprintf(stderr,
			"%s: Host does not support urid:map\n", descriptor->URI);
		free(handle);
		return NULL;
	}

	if(handle->log)
	{
		lv2_log_logger_init(&handle->logger, handle->map, handle->log);
	}

	handle->urid.midi_event = handle->map->map(handle->map->handle, LV2_MIDI__MidiEvent);

	timely_mask_t mask = TIMELY_MASK_BAR_BEAT_WHOLE
		| TIMELY_MASK_BAR_WHOLE
		| TIMELY_MASK_SPEED;
	timely_init(&handle->timely, handle->map, rate, mask, _cb, handle);
	lv2_atom_forge_init(&handle->forge, handle->map);

	if(!props_init(&handle->props, descriptor->URI,
		defs, MAX_NPROPS, &handle->state, &handle->stash,
		handle->map, handle))
	{
		fprintf(stderr, "failed to initialize property structure\n");
		free(handle);
		return NULL;
	}

	return handle;
}

static void
connect_port(LV2_Handle instance, uint32_t port, void *data)
{
	plughandle_t *handle = (plughandle_t *)instance;

	switch(port)
	{
		case 0:
			handle->event_in = (const LV2_Atom_Sequence *)data;
			break;
		case 1:
			handle->event_out = (LV2_Atom_Sequence *)data;
			break;
		default:
			break;
	}
}

static void
run(LV2_Handle instance, uint32_t nsamples)
{
	plughandle_t *handle = instance;
	uint32_t last_t = 0;

	const uint32_t capacity = handle->event_out->atom.size;
	LV2_Atom_Forge_Frame frame;
	lv2_atom_forge_set_buffer(&handle->forge, (uint8_t *)handle->event_out, capacity);
	handle->ref = lv2_atom_forge_sequence_head(&handle->forge, &frame, 0);

	props_idle(&handle->props, &handle->forge, 0, &handle->ref);

	LV2_ATOM_SEQUENCE_FOREACH(handle->event_in, ev)
	{
		const LV2_Atom_Object *obj = (const LV2_Atom_Object *)&ev->body;
		if(!timely_advance(&handle->timely, obj, last_t, ev->time.frames))
		{
			props_advance(&handle->props, &handle->forge, ev->time.frames, obj, &handle->ref);
		}

		last_t = ev->time.frames;
	}

	timely_advance(&handle->timely, NULL, last_t, nsamples);

	if(handle->ref)
	{
		lv2_atom_forge_pop(&handle->forge, &frame);
	}
	else
	{
		lv2_atom_sequence_clear(handle->event_out);

		if(handle->log)
		{
			lv2_log_trace(&handle->logger, "forge buffer overflow\n");
		}
	}
}

static void
cleanup(LV2_Handle instance)
{
	plughandle_t *handle = instance;

	munlock(handle, sizeof(plughandle_t));
	free(handle);
}

static LV2_State_Status
_state_save(LV2_Handle instance, LV2_State_Store_Function store,
	LV2_State_Handle state, uint32_t flags,
	const LV2_Feature *const *features)
{
	plughandle_t *handle = instance;

	return props_save(&handle->props, store, state, flags, features);
}

static LV2_State_Status
_state_restore(LV2_Handle instance, LV2_State_Retrieve_Function retrieve,
	LV2_State_Handle state, uint32_t flags,
	const LV2_Feature *const *features)
{
	plughandle_t *handle = instance;

	return props_restore(&handle->props, retrieve, state, flags, features);
}

static const LV2_State_Interface state_iface = {
	.save = _state_save,
	.restore = _state_restore
};

static const void*
extension_data(const char* uri)
{
	if(!strcmp(uri, LV2_STATE__interface))
	{
		return &state_iface;
	}

	return NULL;
}

const LV2_Descriptor orbit_midi_sync = {
	.URI						= ORBIT_MIDI_SYNC_URI,
	.instantiate		= instantiate,
	.connect_port		= connect_port,
	.activate				= NULL,
	.run						= run,
	.deactivate			= NULL,
	.cleanup				= cleanup,
	.extension_data	= extension_data
};

M ttl/manifest.ttl.in => ttl/manifest.ttl.in +16 -0
@@ 70,3 70,19 @@ orbit:quantum
	lv2:microVersion @MICRO_VERSION@ ;
	lv2:binary <orbit@MODULE_SUFFIX@> ;
	rdfs:seeAlso <orbit.ttl> .

# Orbit MIDI Sync
orbit:midiSync
	a lv2:Plugin ;
	lv2:minorVersion @MINOR_VERSION@ ;
	lv2:microVersion @MICRO_VERSION@ ;
	lv2:binary <orbit@MODULE_SUFFIX@> ;
	rdfs:seeAlso <orbit.ttl> .

# Orbit MIDI Clock
orbit:midiClock
	a lv2:Plugin ;
	lv2:minorVersion @MINOR_VERSION@ ;
	lv2:microVersion @MICRO_VERSION@ ;
	lv2:binary <orbit@MODULE_SUFFIX@> ;
	rdfs:seeAlso <orbit.ttl> .

M ttl/orbit.ttl => ttl/orbit.ttl +91 -0
@@ 805,3 805,94 @@ orbit:quantum
	state:state [
		orbit:quantum_mode  2 ;
	] .

orbit:midiSync
	a lv2:Plugin ,
		lv2:ConverterPlugin ;
	doap:name "Orbit MIDI Sync" ;
	doap:license <https://spdx.org/licenses/Artistic-2.0> ;
	lv2:project proj:orbit ;
	lv2:requiredFeature urid:map, state:loadDefaultState ;
	lv2:optionalFeature lv2:isLive, lv2:hardRTCapable, state:threadSafeRestore, log:log ;
	lv2:extensionData state:interface ;

	lv2:port [
		# sink event port
	  a lv2:InputPort ,
			atom:AtomPort ;
		atom:bufferType atom:Sequence ;
		atom:supports midi:MidiEvent ,
			patch:Message ;
		lv2:index 0 ;
		lv2:symbol "event_in" ;
		lv2:name "Event Input" ;
		lv2:designation lv2:control ;
	] , [
		# source event port
	  a lv2:OutputPort ,
			atom:AtomPort ;
		atom:bufferType atom:Sequence ;
		atom:supports midi:MidiEvent ,
			time:Position ,
			patch:Message ;
		lv2:index 1 ;
		lv2:symbol "event_out" ;
		lv2:name "Event Output" ;
		lv2:designation lv2:control ;
	] ;

	patch:writable
		orbit:quantum_mode ; # FIXME

	state:state [
		orbit:quantum_mode  2 ; # FIXME
	] .

orbit:song
	a lv2:Parameter ;
	rdfs:label "Song" ;
	rdfs:comment "select song" ;
	rdfs:range atom:Int ;
	lv2:minimum 0 ;
	lv2:maximum 127 .

orbit:midiClock
	a lv2:Plugin ,
		lv2:ConverterPlugin ;
	doap:name "Orbit MIDI Clock" ;
	doap:license <https://spdx.org/licenses/Artistic-2.0> ;
	lv2:project proj:orbit ;
	lv2:requiredFeature urid:map, state:loadDefaultState ;
	lv2:optionalFeature lv2:isLive, lv2:hardRTCapable, state:threadSafeRestore, log:log ;
	lv2:extensionData state:interface ;

	lv2:port [
		# sink event port
	  a lv2:InputPort ,
			atom:AtomPort ;
		atom:bufferType atom:Sequence ;
		atom:supports time:Position ,
			patch:Message ;
		lv2:index 0 ;
		lv2:symbol "event_in" ;
		lv2:name "Event Input" ;
		lv2:designation lv2:control ;
	] , [
		# source event port
	  a lv2:OutputPort ,
			atom:AtomPort ;
		atom:bufferType atom:Sequence ;
		atom:supports midi:MidiEvent ,
			patch:Message ;
		lv2:index 1 ;
		lv2:symbol "event_out" ;
		lv2:name "Event Output" ;
		lv2:designation lv2:control ;
	] ;

	patch:writable
		orbit:song ;

	state:state [
		orbit:song 0 ;
	] .