diff --git a/Makefile b/Makefile index 78b52ce..1b8854d 100644 --- a/Makefile +++ b/Makefile @@ -48,26 +48,42 @@ endif #CWDAEMON_INCLUDE=CWDAEMON -#ifeq ($(CWDAEMON_INCLUDE),CWDAEMON) -#CWDAEMON_OPTIONS=-D CWDAEMON -#CWDAEMON_LIBS=-lcw -#CWDAEMON_SOURCES= \ -#cwdaemon.c -#CWDAEMON_HEADERS= \ -#cwdaemon.h -#CWDAEMON_OBJS= \ -#cwdaemon.o -#endif - - -OPTIONS=-Wno-deprecated-declarations $(AUDIO_OPTIONS) -D GIT_DATE='"$(GIT_DATE)"' -D GIT_VERSION='"$(GIT_VERSION)"' $(SOAPYSDR_OPTIONS) \ - $(CWDAEMON_OPTIONS) $(OPENGL_OPTIONS) -O3 -g +ifeq ($(CWDAEMON_INCLUDE),CWDAEMON) +CWDAEMON_OPTIONS=-D CWDAEMON +CWDAEMON_LIBS=-lcw +CWDAEMON_SOURCES= \ +cwdaemon.c +CWDAEMON_HEADERS= \ +cwdaemon.h +CWDAEMON_OBJS= \ +cwdaemon.o +endif + +# MIDI code from piHPSDR written by Christoph van Wullen, DL1YCF. +MIDI_INCLUDE=MIDI + +ifeq ($(MIDI_INCLUDE),MIDI) +MIDI_OPTIONS=-D MIDI +MIDI_SOURCES= alsa_midi.c midi2.c midi3.c +MIDI_HEADERS= midi.h +MIDI_OBJS= alsa_midi.o midi2.o midi3.o +MIDI_LIBS= -lasound +endif + +CFLAGS= -g -Wno-deprecated-declarations -O3 +OPTIONS= $(MIDI_OPTIONS) $(AUDIO_OPTIONS) $(SOAPYSDR_OPTIONS) \ + $(CWDAEMON_OPTIONS) $(OPENGL_OPTIONS) \ + -D GIT_DATE='"$(GIT_DATE)"' -D GIT_VERSION='"$(GIT_VERSION)"' #OPTIONS=-g -Wno-deprecated-declarations $(AUDIO_OPTIONS) -D GIT_DATE='"$(GIT_DATE)"' -D GIT_VERSION='"$(GIT_VERSION)"' -O3 -D FT8_MARKER -LIBS=-lrt -lm -lpthread -lwdsp +LIBS=-lrt -lm -lpthread -lwdsp $(GTKLIBS) $(AUDIO_LIBS) $(SOAPYSDR_LIBS) $(CWDAEMON_LIBS) $(OPENGL_LIBS) $(MIDI_LIBS) + INCLUDES=$(GTKINCLUDES) $(OPGL_INCLUDES) -COMPILE=$(CC) $(OPTIONS) $(INCLUDES) +COMPILE=$(CC) $(CFLAGS) $(OPTIONS) $(INCLUDES) + +.c.o: + $(COMPILE) -c -o $@ $< PROGRAM=linhpsdr @@ -219,13 +235,17 @@ error_handler.o\ radio_info.o\ bpsk.o -all: prebuild $(PROGRAM) $(HEADERS) $(SOURCES) $(SOAPYSDR_SOURCES) $(CWDAEMON_SOURCES) + +$(PROGRAM): $(OBJS) $(SOAPYSDR_OBJS) $(CWDAEMON_OBJS) $(MIDI_OBJS) + $(LINK) -o $(PROGRAM) $(OBJS) $(SOAPYSDR_OBJS) $(CWDAEMON_OBJS) $(MIDI_OBJS) $(LIBS) + + +all: prebuild $(PROGRAM) $(HEADERS) $(MIDI_HEADERS) $(SOURCES) $(SOAPYSDR_SOURCES) \ + $(CWDAEMON_SOURCES) $(MIDI_SOURCES) prebuild: rm -f version.o -$(PROGRAM): $(OBJS) $(SOAPYSDR_OBJS) $(CWDAEMON_OBJS) - $(LINK) -o $(PROGRAM) $(OBJS) $(SOAPYSDR_OBJS) $(CWDAEMON_OBJS) $(GTKLIBS) $(LIBS) $(AUDIO_LIBS) $(SOAPYSDR_LIBS) $(CWDAEMON_LIBS) $(OPENGL_LIBS) .c.o: $(COMPILE) -c -o $@ $< diff --git a/alsa_midi.c b/alsa_midi.c new file mode 100644 index 0000000..89dc8e5 --- /dev/null +++ b/alsa_midi.c @@ -0,0 +1,244 @@ +/* + * MIDI support for pihpsdr + * (C) Christoph van Wullen, DL1YCF. + * + * This is the "Layer-1" for ALSA-MIDI (Linux) + * For further comments see file mac_midi.c + */ + +/* + * ALSA: MIDI devices are sub-devices to sound cards. + * Therefore we have to loop through the sound cards + * and then, for each sound card, through the + * sub-devices until we have found "our" MIDI + * input device. + * + * The procedure how to find and talk with + * a MIDI device is taken from the sample + * program amidi.c in alsautils. + */ + +#include "midi.h" + +#ifndef __APPLE__ + +#include +#include + +static pthread_t midi_thread_id; +static void* midi_thread(void *); + +static char portname[64]; + +static enum { + STATE_SKIP, // skip bytes + STATE_ARG1, // one arg byte to come + STATE_ARG2, // two arg bytes to come +} state=STATE_SKIP; + +static enum { + CMD_NOTEON, + CMD_NOTEOFF, + CMD_CTRL, + CMD_PITCH, +} command; + +static void *midi_thread(void *arg) { + int ret; + snd_rawmidi_t *input; + int npfds; + struct pollfd *pfds; + unsigned char buf[32]; + unsigned char byte; + unsigned short revents; + int i; + int chan,arg1,arg2; + + if ((ret = snd_rawmidi_open(&input, NULL, portname, SND_RAWMIDI_NONBLOCK)) < 0) { + fprintf(stderr,"cannot open port \"%s\": %s\n", portname, snd_strerror(ret)); + return NULL; + } + snd_rawmidi_read(input, NULL, 0); /* trigger reading */ + + npfds = snd_rawmidi_poll_descriptors_count(input); + pfds = alloca(npfds * sizeof(struct pollfd)); + snd_rawmidi_poll_descriptors(input, pfds, npfds); + for (;;) { + ret = poll(pfds, npfds, 250); + if (ret < 0) { + fprintf(stderr,"poll failed: %s\n", strerror(errno)); + // Do not give up, but also do not fire too rapidly + usleep(250000); + } + if (ret <= 0) continue; // nothing arrived, do next poll() + if ((ret = snd_rawmidi_poll_descriptors_revents(input, pfds, npfds, &revents)) < 0) { + fprintf(stderr,"cannot get poll events: %s\n", snd_strerror(errno)); + continue; + } + if (revents & (POLLERR | POLLHUP)) continue; + if (!(revents & POLLIN)) continue; + // something has arrived + ret = snd_rawmidi_read(input, buf, 64); + if (ret == 0) continue; + if (ret < 0) { + fprintf(stderr,"cannot read from port \"%s\": %s\n", portname, snd_strerror(ret)); + continue; + } + // process bytes in buffer. Since they no not necessarily form complete messages + // we need a state machine here. + for (i=0; i< ret; i++) { + byte=buf[i]; + switch (state) { + case STATE_SKIP: + chan=byte & 0x0F; + switch (byte & 0xF0) { + case 0x80: // Note-OFF command + command=CMD_NOTEOFF; + state=STATE_ARG2; + break; + case 0x90: // Note-ON command + command=CMD_NOTEON; + state=STATE_ARG2; + break; + case 0xB0: // Controller Change + command=CMD_CTRL; + state=STATE_ARG2; + break; + case 0xE0: // Pitch Bend + command=CMD_PITCH; + state=STATE_ARG2; + break; + case 0xA0: // Polyphonic Pressure + case 0xC0: // Program change + case 0xD0: // Channel pressure + case 0xF0: // System Message: continue waiting for bit7 set + default: // Remain in STATE_SKIP until bit7 is set + break; + } + break; + case STATE_ARG2: + arg1=byte; + state=STATE_ARG1; + break; + case STATE_ARG1: + arg2=byte; + // We have a command! + switch (command) { + case CMD_NOTEON: + // Hercules MIDI controllers generate NoteOn + // messages with velocity == 0 when releasing + // a push-button + if (arg2 == 0) { + NewMidiEvent(MIDI_NOTE, chan, arg1, 0); + } else { + NewMidiEvent(MIDI_NOTE, chan, arg1, 1); + } + break; + case CMD_NOTEOFF: + NewMidiEvent(MIDI_NOTE, chan, arg1, 0); + break; + case CMD_CTRL: + NewMidiEvent(MIDI_CTRL, chan, arg1, arg2); + break; + case CMD_PITCH: + NewMidiEvent(MIDI_PITCH, chan, 0, arg1+128*arg2); + break; + } + state=STATE_SKIP; + break; + } + } + } +} + +void register_midi_device(char *myname) { + + int mylen=strlen(myname); + snd_ctl_t *ctl; + snd_rawmidi_info_t *info; + int card, device, subs, sub, ret; + const char *devnam, *subnam; + int found=0; + char name[64]; + + card=-1; + if ((ret = snd_card_next(&card)) < 0) { + fprintf(stderr,"cannot determine card number: %s\n", snd_strerror(ret)); + return; + } + while (card >= 0) { + fprintf(stderr,"Found Sound Card=%d\n",card); + sprintf(name,"hw:%d", card); + if ((ret = snd_ctl_open(&ctl, name, 0)) < 0) { + fprintf(stderr,"cannot open control for card %d: %s\n", card, snd_strerror(ret)); + return; + } + device = -1; + // loop through devices of the card + for (;;) { + if ((ret = snd_ctl_rawmidi_next_device(ctl, &device)) < 0) { + fprintf(stderr,"cannot determine device number: %s\n", snd_strerror(ret)); + break; + } + if (device < 0) break; + fprintf(stderr,"Found Device=%d on Card=%d\n", device, card); + // found sub-device + snd_rawmidi_info_alloca(&info); + snd_rawmidi_info_set_device(info, device); + snd_rawmidi_info_set_stream(info, SND_RAWMIDI_STREAM_INPUT); + ret = snd_ctl_rawmidi_info(ctl, info); + if (ret >= 0) { + subs = snd_rawmidi_info_get_subdevices_count(info); + } else { + subs = 0; + } + fprintf(stderr,"Number of MIDI input devices: %d\n", subs); + if (!subs) break; + // subs: number of sub-devices to device on card + for (sub = 0; sub < subs; ++sub) { + snd_rawmidi_info_set_stream(info, SND_RAWMIDI_STREAM_INPUT); + snd_rawmidi_info_set_subdevice(info, sub); + ret = snd_ctl_rawmidi_info(ctl, info); + if (ret < 0) { + fprintf(stderr,"cannot get rawmidi information %d:%d:%d: %s\n", + card, device, sub, snd_strerror(ret)); + break; + } + if (found) break; + devnam = snd_rawmidi_info_get_name(info); + subnam = snd_rawmidi_info_get_subdevice_name(info); + // If there is only one sub-device and it has no name, we use + // devnam for comparison and make a portname of form "hw:x,y", + // else we use subnam for comparison and make a portname of form "hw:x,y,z". + if (sub == 0 && subnam[0] == '\0') { + sprintf(portname,"hw:%d,%d", card, device); + } else { + sprintf(portname,"hw:%d,%d,%d", card, device, sub); + devnam=subnam; + } + if (!strncmp(myname, devnam, mylen)) { + found=1; + fprintf(stderr,"MIDI device %s selected (PortName=%s)\n", devnam, portname); + } else { + fprintf(stderr,"MIDI device found BUT NOT SELECTED: %s\n", devnam); + } + } + } + snd_ctl_close(ctl); + // next card + if ((ret = snd_card_next(&card)) < 0) { + fprintf(stderr,"cannot determine card number: %s\n", snd_strerror(ret)); + break; + } + } + if (!found) { + fprintf(stderr,"MIDI device %s NOT FOUND!\n", myname); + return; + } + // Found our MIDI input device. Spawn off a thread reading data + ret = pthread_create(&midi_thread_id, NULL, midi_thread, NULL); + if (ret < 0) { + fprintf(stderr,"Failed to create MIDI read thread\n"); + } +} +#endif diff --git a/cwdaemon.c b/cwdaemon.c index ecb962d..33d639d 100644 --- a/cwdaemon.c +++ b/cwdaemon.c @@ -95,7 +95,7 @@ #define CWDAEMON_MORSE_WEIGHTING_MAX 50 #define CWDAEMON_NETWORK_PORT_DEFAULT 6789 -#define CWDAEMON_AUDIO_SYSTEM_DEFAULT CW_AUDIO_NULL /* Console buzzer, from libcw.h. */ +#define CWDAEMON_AUDIO_SYSTEM_DEFAULT CW_AUDIO_PA /* Console buzzer, from libcw.h. */ #define CWDAEMON_VERBOSITY_DEFAULT CWDAEMON_VERBOSITY_W /* Threshold of verbosity of debug strings. */ #define CWDAEMON_USECS_PER_MSEC 1000 /* Just to avoid magic numbers. */ @@ -960,7 +960,7 @@ void cwdaemon_handle_escaped_request(char *request) cwdaemon_close_libcw_output(); if (cwdaemon_open_libcw_output(CW_AUDIO_NULL)) { printf("fall back to \"Null\" sound system"); - current_audio_system = CW_AUDIO_NULL; + current_audio_system = CW_AUDIO_PA; has_audio_output = true; } else { printf( diff --git a/cwdaemon.h b/cwdaemon.h index fb34830..68f5636 100644 --- a/cwdaemon.h +++ b/cwdaemon.h @@ -35,6 +35,7 @@ #define MAX_DEVICE 20 #include +#include typedef struct cwdev_s { int (*init) (struct cwdev_s *, int fd); @@ -64,8 +65,10 @@ enum cwdaemon_verbosity { extern gpointer cwdaemon_thread(gpointer data); GMutex cwdaemon_mutex; + bool keytx; -int cwdaemon_run; +bool keysidetone; + void cwdaemon_stop(void); void cwdaemon_close_socket(void); diff --git a/midi.h b/midi.h new file mode 100644 index 0000000..fe0c82e --- /dev/null +++ b/midi.h @@ -0,0 +1,234 @@ +/* + * MIDI support for pihpsdr + * + * (C) Christoph van Wullen, DL1YCF. + * + * Midi support works in three layers + * + * Layer-1: hardware specific + * -------------------------- + * + * Layer1 either implements a callback function (if the operating system + * supports MIDI) or a separate thread polling MIDI data. Whenever a + * MIDI command arrives, such as Note on/off or Midi-Controller value + * changed, it calls Layer 2. + * + * Layer-2: MIDI device specific + * ----------------------------- + * + * Layer2 translates MIDI commands into pihpsdr actions. This is done with + * a table-driven algorithm, such that the same translator can be used for + * any MIDI device provided the tables have been set up correctly. + * It seems overly complicated to create a user interface for setting up + * these tables, instead a standard text file describing the MIDI device + * is read and the tables are set up. + * Layer-2 has SDR applications in mind, but is not necessarily specific + * to pihpsr. It calls the Layer-3 function. + * + * Layer-3: pihpsdr specific + * ------------------------- + * + * Layer 3, finally, implements all the "actions" we can make, such as TUNE + * or VFO. This Layer calls pihpsdr functions. + * + * One word to MIDI channels. Usually, a MIDI device can be configured to use + * a specific channel, such that different keyboards use different channels. + * The Layer-2 tables can either specify that the MIDI command has to come from + * a specific channel, or can specify that the action will be taken not matter which + * channel the MIDI message comes from. The latter case should be the default, but + * if we want to connect more than one MIDI device, we need to speficy the channel. + * + * In principle this supports more than one MIDI device, but in this case they + * must generate MIDI events on different channels + */ + +// +// MIDIaction encodes the "action" to be taken in Layer3 +// (sorted alphabetically by the keyword) +// +enum MIDIaction { + ACTION_NONE=0, // NONE: No-Op (unassigned key) + VFO_A2B, // A2B: VFO A -> B + MIDI_AF_GAIN, // AFGAIN: AF gain + AGCATTACK, // AGCATTACK: AGC ATTACK (cycle fast/med/slow etc.) + MIDI_AGC, // AGCVAL: AGC level + ANF, // ANF: toggel ANF on/off + ATT, // ATT: Step attenuator or Programmable attenuator + VFO_B2A, // B2A: VFO B -> A + BAND_DOWN, // BANDDOWN: cycle through bands downwards + BAND_UP, // BANDUP: cycle through bands upwards + COMPRESS, // COMPRESS: TX compressor value + MIDI_CTUN, // CTUN: toggle CTUN on/off + VFO, // CURRVFO: change VFO frequency + CWKEY, // CWL: Left paddle pressed (use with ONOFF) + CWR, // CWR: Right paddle pressed (use with ONOFF) + CWSPEED, // CWSPEED: Set speed of (iambic) CW keyer + DIV_COARSEGAIN, // DIVCOARSEGAIN: change DIVERSITY gain in large increments + DIV_COARSEPHASE, // DIVPHASE: change DIVERSITY phase in large increments + DIV_FINEGAIN, // DIVFINEGAIN: change DIVERSITY gain in small increments + DIV_FINEPHASE, // DIVFINEPHASE: change DIVERSITY phase in small increments + DIV_GAIN, // DIVGAIN: change DIVERSITY gain in medium increments + DIV_PHASE, // DIVPHASE: change DIVERSITY phase in medium increments + DIV_TOGGLE, // DIVTOGGLE: DIVERSITY on/off + MIDI_DUP, // DUP: toggle duplex on/off + FILTER_DOWN, // FILTERDOWN: cycle through filters downwards + FILTER_UP, // FILTERUP: cycle through filters upwards + MIDI_LOCK, // LOCK: lock VFOs, disable frequency changes + MIC_VOLUME, // MICGAIN: MIC gain + MODE_DOWN, // MODEDOWN: cycle through modes downwards + MODE_UP, // MODEUP: cycle through modes upwards + MIDI_MOX, // MOX: toggle "mox" state + MIDI_MUTE, // MUTE: toggle mute on/off + MIDI_NB, // NOISEBLANKER: cycle through NoiseBlanker states (none, NB, NB2) + MIDI_NR, // NOISEREDUCTION: cycle through NoiseReduction states (none, NR, NR2) + MIDI_PAN, // PAN: change panning of panadater/waterfall when zoomed + PAN_HIGH, // PANHIGH: "high" value of current panadapter + PAN_LOW, // PANLOW: "low" value of current panadapter + PRE, // PREAMP: preamp on/off + MIDI_PS, // PURESIGNAL: toggle PURESIGNAL on/off + MIDI_RF_GAIN, // RFGAIN: receiver RF gain + TX_DRIVE, // RFPOWER: adjust TX RF output power + MIDI_RIT_CLEAR, // RITCLEAR: clear RIT and XIT value + RIT_STEP, // RITSTEP: cycle through RIT/XIT step size values + RIT_TOGGLE, // RITTOGGLE: toggle RIT on/off + RIT_VAL, // RITVAL: change RIT value + MIDI_SAT, // SAT: cycle through SAT modes off/SAT/RSAT + SNB, // SNB: toggle SNB on/off + MIDI_SPLIT, // SPLIT: Split on/off + SWAP_RX, // SWAPRX: swap active receiver (if there are two receivers) + SWAP_VFO, // SWAPVFO: swap VFO A/B frequency + MIDI_TUNE, // TUNE: toggle "tune" state + VFOA, // VFOA: change VFO-A frequency + VFOB, // VFOB: change VFO-B frequency + VFO_STEP_UP, // VFOSTEPUP: cycle through vfo steps upwards; + VFO_STEP_DOWN, // VFOSTEPDOWN: cycle through vfo steps downwards; + VOX, // VOX: toggle VOX on/off + VOXLEVEL, // VOXLEVEL: adjust VOX threshold + MIDI_XIT_CLEAR, // XITCLEAR: clear XIT value + XIT_VAL, // XITVAL: change XIT value + MIDI_ZOOM, // ZOOM: change zoom factor + ZOOM_UP, // ZOOMUP: change zoom factor + ZOOM_DOWN, // ZOOMDOWN: change zoom factor +}; + +// +// MIDItype encodes the type of MIDI control. This info +// is passed from Layer-2 to Layer-3 +// +// MIDI_KEY has no parameters and indicates that some +// button has been pressed. +// +// MIDI_KNOB has a "value" parameter (between 0 and 100) +// and indicates that some knob has been set to a specific +// position. +// +// MIDI_WHEEL has a "direction" parameter and indicates that +// some knob has been turned left/down or right/ip. The value +// can be +// +// -100 very fast going down +// -10 fast going down +// -1 going down +// 1 going up +// 10 fast going up +// 100 very fast going up +// + +enum MIDItype { + TYPE_NONE=0, + MIDI_KEY, // Button (press event) + MIDI_KNOB, // Knob (value between 0 and 100) + MIDI_WHEEL // Wheel (direction and speed) +}; + +// +// MIDIevent encodes the actual MIDI event "seen" in Layer-1 and +// passed to Layer-2. MIDI_NOTE events end up as MIDI_KEY and +// MIDI_PITCH as MIDI_KNOB, while MIDI_CTRL can end up both as +// MIDI_KNOB or MIDI_WHEEL, depending on the device description. +// +enum MIDIevent { + EVENT_NONE=0, + MIDI_NOTE, + MIDI_CTRL, + MIDI_PITCH +}; + +// +// Data structure for Layer-2 +// + +// +// There is linked list of all specified MIDI events for a given "Note" value, +// which contains the defined actions for all MIDI_NOTE and MIDI_CTRL events +// with that given note and for all channels +// Note on wheel delay: +// If using a wheel for cycling through a menu, it is difficult to "hit" the correct +// menu item if wheel events are generated at a very high rate. Therefore we can define +// a delay: once a wheel event is reported upstream, any such events are suppressed during +// the delay. +// +// Note that with a MIDI KEY, you can choose that an action is +// generated only for a NOTE_ON event or both for NOTE_ON and +// NOTE_OFF. In the first case, if the key is associated to MOX, +// then MOX is toggled each time the key is pressed. This behaves +// very much like point-and-clicking the MOX buttion in the GUI. +// +// If an action is generated both on NOTE_ON and NOTE_OFF, +// then MOX is engaged when pressing the key and disengaged +// when releasing it. For MOX this makes little send but you +// might want to configure the TUNE button this way. +// The latter behaviour is triggered when the line assigning the key +// or "NOTE OFF". The table speficying the behaviour of layer-2 thus +// contains the key word "ONOFF". This is stored in the field "onoff" +// in struct desc. + +struct desc { + int channel; // -1 for ANY channel + enum MIDIevent event; // type of event (NOTE on/off, Controller change, Pitch value) + int onoff; // 1: generate upstream event both for Note-on and Note-off + enum MIDItype type; // Key, Knob, or Wheel + int vfl1,vfl2; // Wheel only: range of controller values for "very fast left" + int fl1,fl2; // Wheel only: range of controller values for "fast left" + int lft1,lft2; // Wheel only: range of controller values for "slow left" + int vfr1,vfr2; // Wheel only: range of controller values for "very fast right" + int fr1,fr2; // Wheel only: range of controller values for "fast right" + int rgt1,rgt2; // Wheel only: range of controller values for "slow right" + int delay; // Wheel only: delay (msec) before next message is given upstream + enum MIDIaction action; // SDR "action" to generate + struct desc *next; // Next defined action for a controller/key with that note value (NULL for end of list) +}; + +struct { + struct desc *desc[128]; // description for Note On/Off and ControllerChange + struct desc *pitch; // description for PitchChanges +} MidiCommandsTable; + +// +// Layer-1 entry point, called once for all the MIDI devices +// that have been defined. This is called upon startup by +// Layer-2 through the function MIDIstartup. +// +void register_midi_device(char *name); + +// +// Layer-2 entry point (called by Layer1) +// +// When Layer-1 has received a MIDI message, it calls +// NewMidiEvent. +// +// MIDIstartup looks for files containing descriptions for MIDI +// devices and calls the Layer-1 function register_midi_device +// for each device description that was successfully read. + +void NewMidiEvent(enum MIDIevent event, int channel, int note, int val); +int MIDIstartup(); + +// +// Layer-3 entry point (called by Layer2). In Layer-3, all the pihpsdr +// actions (such as changing the VFO frequency) are performed. +// The implementation of DoTheMIDI is tightly bound to pihpsr and contains +// tons of invocations of g_idle_add with routines from ext.c +// + +void DoTheMidi(enum MIDIaction code, enum MIDItype type, int val); diff --git a/midi2.c b/midi2.c new file mode 100644 index 0000000..6d7c6ea --- /dev/null +++ b/midi2.c @@ -0,0 +1,371 @@ +/* + * Layer-2 of MIDI support + * + * (C) Christoph van Wullen, DL1YCF + * + * Using the data in MIDICommandsTable, this subroutine translates the low-level + * MIDI events into MIDI actions in the SDR console. + */ +#include + +#include +#include +#include +#include +#include "midi.h" + +void NewMidiEvent(enum MIDIevent event, int channel, int note, int val) { + + struct desc *desc; + int new; + static enum MIDIaction last_wheel_action=ACTION_NONE ; + static struct timespec tp, last_wheel_tp={0,0}; + long delta; + +//fprintf(stderr,"MIDI:EVENT=%d CHAN=%d NOTE=%d VAL=%d\n",event,channel,note,val); + if (event == MIDI_PITCH) { + desc=MidiCommandsTable.pitch; + } else { + desc=MidiCommandsTable.desc[note]; + } +//fprintf(stderr,"MIDI:init DESC=%p\n",desc); + while (desc) { +//fprintf(stderr,"DESC=%p next=%p CHAN=%d EVENT=%d\n", desc,desc->next,desc->channel,desc->event); + if ((desc->channel == channel || desc->channel == -1) && (desc->event == event)) { + // Found matching entry + switch (desc->event) { + case MIDI_NOTE: + if ((val == 1 || (desc->onoff == 1)) && desc->type == MIDI_KEY) { + DoTheMidi(desc->action, desc->type, val); + } + break; + case MIDI_CTRL: + if (desc->type == MIDI_KNOB) { + // normalize value to range 0 - 100 + new = (val*100)/127; + DoTheMidi(desc->action, desc->type, new); + } else if (desc->type == MIDI_WHEEL) { + if (desc->delay > 0 && last_wheel_action == desc->action) { + clock_gettime(CLOCK_MONOTONIC, &tp); + delta=1000*(tp.tv_sec - last_wheel_tp.tv_sec); + delta += (tp.tv_nsec - last_wheel_tp.tv_nsec)/1000000; + if (delta < desc->delay) break; + last_wheel_tp = tp; + } + // translate value to direction + new=0; + if ((val >= desc->vfl1) && (val <= desc->vfl2)) new=-100; + if ((val >= desc-> fl1) && (val <= desc-> fl2)) new=-10; + if ((val >= desc->lft1) && (val <= desc->lft2)) new=-1; + if ((val >= desc->rgt1) && (val <= desc->rgt2)) new= 1; + if ((val >= desc-> fr1) && (val <= desc-> fr2)) new= 10; + if ((val >= desc->vfr1) && (val <= desc->vfr2)) new= 100; +// fprintf(stderr,"WHEEL: val=%d new=%d thrs=%d/%d, %d/%d, %d/%d, %d/%d, %d/%d, %d/%d\n", +// val, new, desc->vfl1, desc->vfl2, desc->fl1, desc->fl2, desc->lft1, desc->lft2, +// desc->rgt1, desc->rgt2, desc->fr1, desc->fr2, desc->vfr1, desc->vfr2); + if (new != 0) DoTheMidi(desc->action, desc->type, new); + last_wheel_action=desc->action; + } + break; + case MIDI_PITCH: + if (desc->type == MIDI_KNOB) { + // normalize value to 0 - 100 + new = (val*100)/16383; + DoTheMidi(desc->action, desc->type, new); + } + break; + case EVENT_NONE: + break; + } + break; + } else { + desc=desc->next; + } + } + if (!desc) { + // Nothing found. This is nothing to worry about, but log the key to stderr + if (event == MIDI_PITCH) fprintf(stderr, "Unassigned PitchBend Value=%d\n", val); + if (event == MIDI_NOTE ) fprintf(stderr, "Unassigned Key Note=%d Val=%d\n", note, val); + if (event == MIDI_CTRL ) fprintf(stderr, "Unassigned Controller Ctl=%d Val=%d\n", note, val); + } +} + +/* + * This data structre connects names as used in the midi.props file with + * our MIDIaction enum values. + * Take care that no key word is contained in another one! + * Example: use "CURRVFO" not "VFO" otherwise there is possibly + * a match for "VFO" when the key word is "VFOA". + */ + +static struct { + enum MIDIaction action; + const char *str; +} ActionTable[] = { + { VFO_A2B, "A2B"}, + { MIDI_AF_GAIN, "AFGAIN"}, + { AGCATTACK, "AGCATTACK"}, + { MIDI_AGC, "AGCVAL"}, + { ANF, "ANF"}, + { ATT, "ATT"}, + { VFO_B2A, "B2A"}, + { BAND_DOWN, "BANDDOWN"}, + { BAND_UP, "BANDUP"}, + { COMPRESS, "COMPRESS"}, + { MIDI_CTUN, "CTUN"}, + { VFO, "CURRVFO"}, + { CWKEY, "CWL"}, + { CWR, "CWR"}, + { CWSPEED, "CWSPEED"}, + { DIV_COARSEGAIN, "DIVCOARSEGAIN"}, + { DIV_COARSEPHASE, "DIVCOARSEPHASE"}, + { DIV_FINEGAIN, "DIVFINEGAIN"}, + { DIV_FINEPHASE, "DIVFINEPHASE"}, + { DIV_GAIN, "DIVGAIN"}, + { DIV_PHASE, "DIVPHASE"}, + { DIV_TOGGLE, "DIVTOGGLE"}, + { MIDI_DUP, "DUP"}, + { FILTER_DOWN, "FILTERDOWN"}, + { FILTER_UP, "FILTERUP"}, + { MIDI_LOCK, "LOCK"}, + { MIC_VOLUME, "MICGAIN"}, + { MODE_DOWN, "MODEDOWN"}, + { MODE_UP, "MODEUP"}, + { MIDI_MOX, "MOX"}, + { MIDI_MUTE, "MUTE"}, + { MIDI_NB, "NOISEBLANKER"}, + { MIDI_NR, "NOISEREDUCTION"}, + { MIDI_PAN, "PAN"}, + { PAN_HIGH, "PANHIGH"}, + { PAN_LOW, "PANLOW"}, + { PRE, "PREAMP"}, + { MIDI_PS, "PURESIGNAL"}, + { MIDI_RF_GAIN, "RFGAIN"}, + { TX_DRIVE, "RFPOWER"}, + { MIDI_RIT_CLEAR, "RITCLEAR"}, + { RIT_STEP, "RITSTEP"}, + { RIT_TOGGLE, "RITTOGGLE"}, + { RIT_VAL, "RITVAL"}, + { MIDI_SAT, "SAT"}, + { SNB, "SNB"}, + { MIDI_SPLIT, "SPLIT"}, + { SWAP_RX, "SWAPRX"}, + { SWAP_VFO, "SWAPVFO"}, + { MIDI_TUNE, "TUNE"}, + { VFOA, "VFOA"}, + { VFOB, "VFOB"}, + { VFO_STEP_UP, "VFOSTEPUP"}, + { VFO_STEP_DOWN, "VFOSTEPDOWN"}, + { VOX, "VOX"}, + { VOXLEVEL, "VOXLEVEL"}, + { MIDI_XIT_CLEAR, "XITCLEAR"}, + { XIT_VAL, "XITVAL"}, + { MIDI_ZOOM, "ZOOM"}, + { ZOOM_UP, "ZOOMUP"}, + { ZOOM_DOWN, "ZOOMDOWN"}, + { ACTION_NONE, "NONE"} +}; + +/* + * Translation from keyword in midi.props file to MIDIaction + */ + +static enum MIDIaction keyword2action(char *s) { + int i=0; + + for (i=0; i< (sizeof(ActionTable) / sizeof(ActionTable[0])); i++) { + if (!strcmp(s, ActionTable[i].str)) return ActionTable[i].action; + } + fprintf(stderr,"MIDI: action keyword %s NOT FOUND.\n", s); + return ACTION_NONE; +} + +/* + * Here we read in a MIDI description file "midi.def" and fill the MidiCommandsTable + * data structure + */ + +int MIDIstartup() { + FILE *fpin; + char zeile[255]; + char *cp,*cq; + int key; + enum MIDIaction action; + int chan; + int t1,t2,t3,t4,t5,t6,t7,t8,t9,t10,t11,t12; + int onoff, delay; + struct desc *desc,*dp; + enum MIDItype type; + enum MIDIevent event; + int i; + char c; + + for (i=0; i<128; i++) MidiCommandsTable.desc[i]=NULL; + MidiCommandsTable.pitch=NULL; + + char filename[128]; + sprintf(filename,"%s/.local/share/linhpsdr/midi.props", g_get_home_dir()); + fpin=fopen(filename, "r"); + if (!fpin) return -1; + for (;;) { + if (fgets(zeile, 255, fpin) == NULL) break; + + // ignore comments + cp=index(zeile,'#'); + if (cp == zeile) continue; // comment line + if (cp) *cp=0; // ignore trailing comment + + // change newline, comma, slash etc. to blanks + cp=zeile; + while ((c=*cp)) { + switch (c) { + case '\n': + case '\r': + case '\t': + case ',': + case '/': + *cp=' '; + break; + } + cp++; + } +fprintf(stderr,"\nMIDI:INP:%s\n",zeile); + + if ((cp = strstr(zeile, "DEVICE="))) { + // Delete comments and trailing blanks + cq=cp+7; + while (*cq != 0 && *cq != '#') cq++; + *cq--=0; + while (cq > cp+7 && (*cq == ' ' || *cq == '\t')) cq--; + *(cq+1)=0; +//fprintf(stderr,"MIDI:REG:>>>%s<<<\n",cp+7); + register_midi_device(cp+7); + continue; // nothing more in this line + } + chan=-1; // default: any channel + t1=t3=t5=t7= t9=t11=128; // range that never occurs + t2=t4=t6=t8=t10=t12=-1; // range that never occurs + onoff=0; + event=EVENT_NONE; + type=TYPE_NONE; + key=0; + delay=0; + + // + // The KEY=, CTRL=, and PITCH= cases are mutually exclusive + // If more than one keyword is in the line, PITCH wins over CTRL + // wins over KEY. + // + if ((cp = strstr(zeile, "KEY="))) { + sscanf(cp+4, "%d", &key); + event=MIDI_NOTE; + type=MIDI_KEY; +//fprintf(stderr,"MIDI:KEY:%d\n", key); + } + if ((cp = strstr(zeile, "CTRL="))) { + sscanf(cp+5, "%d", &key); + event=MIDI_CTRL; + type=MIDI_KNOB; +//fprintf(stderr,"MIDI:CTL:%d\n", key); + } + if ((cp = strstr(zeile, "PITCH "))) { + event=MIDI_PITCH; + type=MIDI_KNOB; +//fprintf(stderr,"MIDI:PITCH\n"); + } + // + // If event is still undefined, skip line + // + if (event == EVENT_NONE) { +//fprintf(stderr,"MIDI:ERR:NO_EVENT\n"); + continue; + } + + // + // beware of illegal key values + // + if (key < 0 ) key=0; + if (key > 127) key=127; + + if ((cp = strstr(zeile, "CHAN="))) { + sscanf(cp+5, "%d", &chan); + chan--; + if (chan<0 || chan>15) chan=-1; +//fprintf(stderr,"MIDI:CHA:%d\n",chan); + } + if ((cp = strstr(zeile, "WHEEL")) && (type == MIDI_KNOB)) { + // change type from MIDI_KNOB to MIDI_WHEEL + type=MIDI_WHEEL; +//fprintf(stderr,"MIDI:WHEEL\n"); + } + if ((cp = strstr(zeile, "ONOFF"))) { + onoff=1; +fprintf(stderr,"MIDI:ONOFF\n"); + } + if ((cp = strstr(zeile, "DELAY="))) { + sscanf(cp+6, "%d", &delay); +//fprintf(stderr,"MIDI:DELAY:%d\n",delay); + } + if ((cp = strstr(zeile, "THR="))) { + sscanf(cp+4, "%d %d %d %d %d %d %d %d %d %d %d %d", + &t1,&t2,&t3,&t4,&t5,&t6,&t7,&t8,&t9,&t10,&t11,&t12); +//fprintf(stderr,"MIDI:THR:%d/%d, %d/%d, %d/%d, %d/%d, %d/%d, %d/%d\n",t1,t2,t3,t4,t5,t6,t7,t8,t9,t10,t11,t12); + } + if ((cp = strstr(zeile, "ACTION="))) { + // cut zeile at the first blank character following + cq=cp+7; + while (*cq != 0 && *cq != '\n' && *cq != ' ' && *cq != '\t') cq++; + *cq=0; + action=keyword2action(cp+7); +//fprintf(stderr,"MIDI:ACTION:%s (%d)\n",cp+7, action); + } + // + // All data for a descriptor has been read. Construct it! + // + desc = (struct desc *) malloc(sizeof(struct desc)); + desc->next = NULL; + desc->action = action; + desc->type = type; + desc->event = event; + desc->onoff = onoff; + desc->delay = delay; + desc->vfl1 = t1; + desc->vfl2 = t2; + desc->fl1 = t3; + desc->fl2 = t4; + desc->lft1 = t5; + desc->lft2 = t6; + desc->rgt1 = t7; + desc->rgt2 = t8; + desc->fr1 = t9; + desc->fr2 = t10; + desc->vfr1 = t11; + desc->vfr2 = t12; + desc->channel = chan; + // + // insert descriptor into linked list. + // We have a linked list for each key value to speed up searches + // + if (event == MIDI_PITCH) { +fprintf(stderr,"MIDI:TAB:Insert desc=%p in PITCH table\n",desc); + dp = MidiCommandsTable.pitch; + if (dp == NULL) { + MidiCommandsTable.pitch = desc; + } else { + while (dp->next != NULL) dp=dp->next; + dp->next=desc; + } + } + if (event == MIDI_KEY || event == MIDI_CTRL) { +fprintf(stderr,"MIDI:TAB:Insert desc=%p in CMDS[%d] table\n",desc,key); + dp = MidiCommandsTable.desc[key]; + if (dp == NULL) { + MidiCommandsTable.desc[key]=desc; + } else { + while (dp->next != NULL) dp=dp->next; + dp->next=desc; + } + } + } + return 0; +} diff --git a/midi3.c b/midi3.c new file mode 100644 index 0000000..3c05068 --- /dev/null +++ b/midi3.c @@ -0,0 +1,914 @@ +/* + * Layer-3 of MIDI support + * + * (C) Christoph van Wullen, DL1YCF + * + * + * In most cases, a certain action only makes sense for a specific + * type. For example, changing the VFO frequency will only be implemeted + * for MIDI_WHEEL, and TUNE off/on only with MIDI_KNOB. + * + * However, changing the volume makes sense both with MIDI_KNOB and MIDI_WHEEL. + */ +#include + +// All the following needed to access radio setting? +// TODO: look more closely if anything can be removed from the list +#include "band.h" +#include "channel.h" +#include "discovered.h" +#include "mode.h" +#include "filter.h" +#include "receiver.h" +#include "transmitter.h" +#include "wideband.h" +#include "adc.h" +#include "dac.h" +#include "radio.h" +#include "main.h" +#include "protocol1.h" +#include "audio.h" +#include "signal.h" +#include "vfo.h" +#include "transmitter.h" + +#include "midi.h" +#ifdef CWDAEMON +#include "cwdaemon.h" +#include +#endif + + +void DoTheMidi(enum MIDIaction action, enum MIDItype type, int val) { + + int new; + double dnew; + double *dp; + int *ip; + + // + // Handle cases in alphabetical order of the key words in midi.props + // + switch (action) { + + case CWR: // CW straight key + #ifdef CWDAEMON + // CWdaemon must be running to produce the sidetone + if (radio->cwdaemon_running == TRUE) { + if (val) { + keysidetone = 1; + g_mutex_lock(&cwdaemon_mutex); + keytx = true; + g_mutex_unlock(&cwdaemon_mutex); + } + else { + keysidetone = 0; + g_mutex_lock(&cwdaemon_mutex); + keytx = false; + g_mutex_unlock(&cwdaemon_mutex); + } + cw_notify_straight_key_event(keysidetone); + } + #endif + break; + // TODO: add dit dah and use unixcw built in iambic keyer + /* + /////////////////////////////////////////////////////////// "A2B" + case VFO_A2B: // only key supported + if (type == MIDI_KEY) { + g_idle_add(ext_vfo_a_to_b, NULL); + } + break; + /////////////////////////////////////////////////////////// "AFGAIN" + case MIDI_AF_GAIN: // knob or wheel supported + switch (type) { + case MIDI_KNOB: + active_receiver->volume = 0.01*val; + break; + case MIDI_WHEEL: + dnew=active_receiver->volume += 0.01*val; + if (dnew < 0.0) dnew=0.0; if (dnew > 1.0) dnew=1.0; + active_receiver->volume = dnew; + break; + default: + // do not change volume + // we should not come here anyway + break; + } + g_idle_add(ext_update_af_gain, NULL); + break; + /////////////////////////////////////////////////////////// "AGCATTACK" + case AGCATTACK: // only key supported + // cycle through fast/med/slow AGC attack + if (type == MIDI_KEY) { + new=active_receiver->agc + 1; + if (new > AGC_FAST) new=0; + active_receiver->agc=new; + g_idle_add(ext_vfo_update, NULL); + } + break; + /////////////////////////////////////////////////////////// "AGCVAL" + case MIDI_AGC: // knob or wheel supported + switch (type) { + case MIDI_KNOB: + dnew = -20.0 + 1.4*val; + break; + case MIDI_WHEEL: + dnew=active_receiver->agc_gain + val; + if (dnew < -20.0) dnew=-20.0; if (dnew > 120.0) dnew=120.0; + break; + default: + // do not change value + // we should not come here anyway + dnew=active_receiver->agc_gain; + break; + } + dp=malloc(sizeof(double)); + *dp=dnew; + g_idle_add(ext_set_agc_gain, (gpointer) dp); + break; + /////////////////////////////////////////////////////////// "ANF" + case ANF: // only key supported + if (type == MIDI_KEY) { + g_idle_add(ext_anf_update, NULL); + } + break; + /////////////////////////////////////////////////////////// "ATT" + case ATT: // Key for ALEX attenuator, wheel or knob for slider + switch(type) { + case MIDI_KEY: + if (filter_board == ALEX && active_receiver->adc == 0) { + new=active_receiver->alex_attenuation + 1; + if (new > 3) new=0; + g_idle_add(ext_set_alex_attenuation, GINT_TO_POINTER(new)); + g_idle_add(ext_update_att_preamp, NULL); + } + break; + case MIDI_WHEEL: + new=adc_attenuation[active_receiver->adc] + val; + dp=malloc(sizeof(double)); + *dp=(double) new; + if(have_rx_gain) { + if(*dp<-12.0) { + *dp=-12.0; + } else if(*dp>48.0) { + *dp=48.0; + } + } else { + if(*dp<0.0) { + *dp=0.0; + } else if (*dp>31.0) { + *dp=31.0; + } + } + g_idle_add(ext_set_attenuation_value,(gpointer) dp); + break; + case MIDI_KNOB: + dp=malloc(sizeof(double)); + if (have_rx_gain) { + *dp=-12.0 + 0.6*(double) val; + } else { + *dp = 0.31 * (double) val; + } + g_idle_add(ext_set_attenuation_value,(gpointer) dp); + break; + default: + // do nothing + // we should not come here anyway + break; + } + break; + /////////////////////////////////////////////////////////// "B2A" + case VFO_B2A: // only key supported + if (type == MIDI_KEY) { + g_idle_add(ext_vfo_b_to_a, NULL); + } + break; + /////////////////////////////////////////////////////////// "BANDDOWN" + /////////////////////////////////////////////////////////// "BANDUP" + case BAND_DOWN: + case BAND_UP: + switch (type) { + case MIDI_KEY: + new=(action == BAND_UP) ? 1 : -1; + break; + case MIDI_WHEEL: + new=val > 0 ? 1 : -1; + break; + case MIDI_KNOB: + // cycle through the bands + new = ((BANDS-1) * val) / 100 - vfo[active_receiver->id].band; + break; + default: + // do not change + // we should not come here anyway + new=0; + break; + } + // + // If the band has not changed, do nothing. Otherwise + // vfo.c will loop through the band stacks + // + if (new != 0) { + new+=vfo[active_receiver->id].band; + if (new >= BANDS) new=0; + if (new < 0) new=BANDS-1; + g_idle_add(ext_vfo_band_changed, GINT_TO_POINTER(new)); + } + break; + /////////////////////////////////////////////////////////// "COMPRESS" + case COMPRESS: // wheel or knob + switch (type) { + case MIDI_WHEEL: + dnew=transmitter->compressor_level + val; + if (dnew > 20.0) dnew=20.0; + if (dnew < 0 ) dnew=0; + break; + case MIDI_KNOB: + dnew=(20.0*val)/100.0; + break; + default: + // do not change + // we should not come here anyway + dnew=transmitter->compressor_level; + break; + } + transmitter->compressor_level=dnew; + // automatically engange compressor if level > 0.5 + if (dnew < 0.5) transmitter->compressor=0; + if (dnew > 0.5) transmitter->compressor=1; + g_idle_add(ext_set_compression, NULL); + break; + /////////////////////////////////////////////////////////// "CTUN" + case MIDI_CTUN: // only key supported + // toggle CTUN + if (type == MIDI_KEY) { + g_idle_add(ext_ctun_update, NULL); + } + break; + /////////////////////////////////////////////////////////// "CURRVFO" + case VFO: // only wheel supported + if (type == MIDI_WHEEL && !locked) { + g_idle_add(ext_vfo_step, GINT_TO_POINTER(val)); + } + break; + /////////////////////////////////////////////////////////// "CWL" + /////////////////////////////////////////////////////////// "CWR" + case CWL: // only key + case CWR: // only key +#ifdef LOCALCW + if (type == MIDI_KEY) { + new=(action == CWL); + keyer_event(new,val); + } +#endif + break; + /////////////////////////////////////////////////////////// "CWSPEED" + case CWSPEED: // knob or wheel + switch (type) { + case MIDI_KNOB: + // speed between 5 and 35 wpm + new= (int) (5.0 + (double) val * 0.3); + break; + case MIDI_WHEEL: + // here we allow from 1 to 60 wpm + new = cw_keyer_speed + val; + if (new < 1) new=1; + if (new > 60) new=60; + break; + default: + // do not change + // we should not come here anyway + new = cw_keyer_speed; + break; + } + cw_keyer_speed=new; +#ifdef LOCALCW + keyer_update(); +#endif + g_idle_add(ext_vfo_update, NULL); + break; + /////////////////////////////////////////////////////////// "DIVCOARSEGAIN" + case DIV_COARSEGAIN: // knob or wheel supported + case DIV_FINEGAIN: // knob or wheel supported + case DIV_GAIN: // knob or wheel supported + switch (type) { + case MIDI_KNOB: + if (action == DIV_COARSEGAIN || action == DIV_GAIN) { + // -25 to +25 dB in steps of 0.5 dB + dnew = 10.0*(-25.0 + 0.5*val - div_gain); + } else { + // round gain to a multiple of 0.5 dB and apply a +/- 0.5 dB update + new = (int) (2*div_gain + 1.0) / 2; + dnew = 10.0*((double) new + 0.01*val - 0.5 - div_gain); + } + break; + case MIDI_WHEEL: + // coarse: increaments in steps of 0.25 dB, medium: steps of 0.1 dB fine: in steps of 0.01 dB + if (action == DIV_GAIN) { + dnew = val*0.5; + } else if (action == DIV_COARSEGAIN) { + dnew = val*2.5; + } else { + dnew = val * 0.1; + } + break; + default: + // do not change + // we should not come here anyway + dnew = 0.0; + break; + } + // dnew is the delta times 10 + dp=malloc(sizeof(double)); + *dp=dnew; + g_idle_add(ext_diversity_change_gain, dp); + break; + /////////////////////////////////////////////////////////// "DIVPHASE" + case DIV_COARSEPHASE: // knob or wheel supported + case DIV_FINEPHASE: // knob or wheel supported + case DIV_PHASE: // knob or wheel supported + switch (type) { + case MIDI_KNOB: + // coarse: change phase from -180 to 180 + // fine: change from -5 to 5 + if (action == DIV_COARSEPHASE || action == DIV_PHASE) { + // coarse: change phase from -180 to 180 in steps of 3.6 deg + dnew = (-180.0 + 3.6*val - div_phase); + } else { + // fine: round to multiple of 5 deg and apply a +/- 5 deg update + new = 5 * ((int) (div_phase+0.5) / 5); + dnew = (double) new + 0.1*val -5.0 -div_phase; + } + break; + case MIDI_WHEEL: + if (action == DIV_PHASE) { + dnew = val*0.5; + } else if (action == DIV_COARSEPHASE) { + dnew = val*2.5; + } else if (action == DIV_FINEPHASE) { + dnew = 0.1*val; + } + break; + default: + // do not change + // we should not come here anyway + dnew = 0.0; + break; + } + // dnew is the delta + dp=malloc(sizeof(double)); + *dp=dnew; + g_idle_add(ext_diversity_change_phase, dp); + break; + /////////////////////////////////////////////////////////// "DIVTOGGLE" + case DIV_TOGGLE: // only key supported + if (type == MIDI_KEY) { + // enable/disable DIVERSITY + diversity_enabled = diversity_enabled ? 0 : 1; + g_idle_add(ext_vfo_update, NULL); + } + break; + /////////////////////////////////////////////////////////// "DUP" + case MIDI_DUP: + if (can_transmit && !isTransmitting()) { + duplex=duplex==1?0:1; + g_idle_add(ext_set_duplex, NULL); + } + break; + /////////////////////////////////////////////////////////// "FILTERDOWN" + /////////////////////////////////////////////////////////// "FILTERUP" + case FILTER_DOWN: + case FILTER_UP: + // + // In filter.c, the filters are sorted such that the widest one comes first + // Therefore let FILTER_UP move down. + // + switch (type) { + case MIDI_KEY: + new=(action == FILTER_UP) ? -1 : 1; + break; + case MIDI_WHEEL: + new=val > 0 ? -1 : 1; + break; + case MIDI_KNOB: + // cycle through all the filters: val=100 maps to filter #0 + new = ((FILTERS-1) * (val-100)) / 100 - vfo[active_receiver->id].filter; + break; + default: + // do not change filter setting + // we should not come here anyway + new=0; + break; + } + if (new != 0) { + new+=vfo[active_receiver->id].filter; + if (new >= FILTERS) new=0; + if (new <0) new=FILTERS-1; + g_idle_add(ext_vfo_filter_changed, GINT_TO_POINTER(new)); + } + break; + /////////////////////////////////////////////////////////// "LOCK" + case MIDI_LOCK: // only key supported + if (type == MIDI_KEY) { + locked=!locked; + g_idle_add(ext_vfo_update, NULL); + } + break; + /////////////////////////////////////////////////////////// "MICGAIN" + case MIC_VOLUME: // knob or wheel supported + // TODO: possibly adjust linein value if that is effective + switch (type) { + case MIDI_KNOB: + dnew=-10.0 + 0.6*val; + break; + case MIDI_WHEEL: + dnew = mic_gain + val; + if (dnew < -10.0) dnew=-10.0; if (dnew > 50.0) dnew=50.0; + break; + default: + // do not change mic gain + // we should not come here anyway + dnew = mic_gain; + break; + } + dp=malloc(sizeof(double)); + *dp=dnew; + g_idle_add(ext_set_mic_gain, (gpointer) dp); + break; + /////////////////////////////////////////////////////////// "MODEDOWN" + /////////////////////////////////////////////////////////// "MODEUP" + case MODE_DOWN: + case MODE_UP: + switch (type) { + case MIDI_KEY: + new=(action == MODE_UP) ? 1 : -1; + break; + case MIDI_WHEEL: + new=val > 0 ? 1 : -1; + break; + case MIDI_KNOB: + // cycle through all the modes + new = ((MODES-1) * val) / 100 - vfo[active_receiver->id].mode; + break; + default: + // do not change + // we should not come here anyway + new=0; + break; + } + if (new != 0) { + new+=vfo[active_receiver->id].mode; + if (new >= MODES) new=0; + if (new <0) new=MODES-1; + g_idle_add(ext_vfo_mode_changed, GINT_TO_POINTER(new)); + } + break; + /////////////////////////////////////////////////////////// "MOX" + case MIDI_MOX: // only key supported + if (type == MIDI_KEY && can_transmit) { + new = !mox; + g_idle_add(ext_mox_update, GINT_TO_POINTER(new)); + } + break; + /////////////////////////////////////////////////////////// "MUTE" + case MIDI_MUTE: + if (type == MIDI_KEY) { + g_idle_add(ext_mute_update,NULL); + } + break; + /////////////////////////////////////////////////////////// "NOISEBLANKER" + case MIDI_NB: // only key supported + // cycle through NoiseBlanker settings: OFF, NB, NB2 + if (type == MIDI_KEY) { + if (active_receiver->nb) { + active_receiver->nb = 0; + active_receiver->nb2= 1; + } else if (active_receiver->nb2) { + active_receiver->nb = 0; + active_receiver->nb2= 0; + } else { + active_receiver->nb = 1; + active_receiver->nb2= 0; + } + g_idle_add(ext_vfo_update, NULL); + } + break; + /////////////////////////////////////////////////////////// "NOISEREDUCTION" + case MIDI_NR: // only key supported + // cycle through NoiseReduction settings: OFF, NR1, NR2 + if (type == MIDI_KEY) { + if (active_receiver->nr) { + active_receiver->nr = 0; + active_receiver->nr2= 1; + } else if (active_receiver->nr2) { + active_receiver->nr = 0; + active_receiver->nr2= 0; + } else { + active_receiver->nr = 1; + active_receiver->nr2= 0; + } + g_idle_add(ext_update_noise, NULL); + g_idle_add(ext_vfo_update, NULL); + } + break; + /////////////////////////////////////////////////////////// "PAN" + case MIDI_PAN: // wheel and knob + switch (type) { + case MIDI_WHEEL: + g_idle_add(ext_pan_update,GINT_TO_POINTER(val)); + break; + case MIDI_KNOB: + g_idle_add(ext_pan_set,GINT_TO_POINTER(val)); + break; + default: + // no action for keys (we should not come here anyway) + break; + } + break; + /////////////////////////////////////////////////////////// "PANHIGH" + case PAN_HIGH: // wheel or knob + switch (type) { + case MIDI_WHEEL: + if (mox) { + // TX panadapter affected + transmitter->panadapter_high += val; + } else { + active_receiver->panadapter_high += val; + } + break; + case MIDI_KNOB: + // Adjust "high water" in the range -50 ... 0 dBm + new = -50 + val/2; + if (mox) { + transmitter->panadapter_high = new; + } else { + active_receiver->panadapter_high = new; + } + break; + default: + // do nothing + // we should not come here anyway + break; + } + g_idle_add(ext_vfo_update, NULL); + break; + /////////////////////////////////////////////////////////// "PANLOW" + case PAN_LOW: // wheel and knob + switch (type) { + case MIDI_WHEEL: + if (isTransmitting()) { + // TX panadapter affected + transmitter->panadapter_low += val; + } else { + active_receiver->panadapter_low += val; + } + break; + case MIDI_KNOB: + if (isTransmitting()) { + // TX panadapter: use values -100 through -50 + new = -100 + val/2; + transmitter->panadapter_low =new; + } else { + // RX panadapter: use values -140 through -90 + new = -140 + val/2; + active_receiver->panadapter_low = new; + } + break; + default: + // do nothing + // we should not come here anyway + break; + } + g_idle_add(ext_vfo_update, NULL); + break; + /////////////////////////////////////////////////////////// "PREAMP" + case PRE: // only key supported + if (type == MIDI_KEY) { + // + // Normally on/off, but for CHARLY25, cycle through three + // possible states. Current HPSDR hardware does no have + // switch'able preamps. + // + int c25= (filter_board == CHARLY25); + new = active_receiver->preamp + active_receiver->dither; + new++; + if (c25) { + if (new >2) new=0; + } else { + if (new >1) new=0; + } + switch (new) { + case 0: + active_receiver->preamp=0; + if (c25) active_receiver->dither=0; + break; + case 1: + active_receiver->preamp=1; + if (c25) active_receiver->dither=0; + break; + case 2: + active_receiver->preamp=1; + if (c25) active_receiver->dither=1; + break; + } + g_idle_add(ext_update_att_preamp, NULL); + } + break; + /////////////////////////////////////////////////////////// "PURESIGNAL" + case MIDI_PS: // only key supported +#ifdef PURESIGNAL + // toggle PURESIGNAL + if (type == MIDI_KEY) { + new=!(transmitter->puresignal); + g_idle_add(ext_tx_set_ps,GINT_TO_POINTER(new)); + } +#endif + break; + /////////////////////////////////////////////////////////// "RFGAIN" + case MIDI_RF_GAIN: // knob or wheel supported + if (type == MIDI_KNOB) { + new=val; + } else if (type == MIDI_WHEEL) { + new=(int)active_receiver->rf_gain+val; + } + g_idle_add(ext_set_rf_gain, GINT_TO_POINTER((int)new)); + break; + /////////////////////////////////////////////////////////// "RFPOWER" + case TX_DRIVE: // knob or wheel supported + switch (type) { + case MIDI_KNOB: + dnew = val; + break; + case MIDI_WHEEL: + dnew=transmitter->drive + val; + if (dnew < 0.0) dnew=0.0; if (dnew > 100.0) dnew=100.0; + break; + default: + // do not change value + // we should not come here anyway + dnew=transmitter->drive; + break; + } + dp=malloc(sizeof(double)); + *dp=dnew; + g_idle_add(ext_set_drive, (gpointer) dp); + break; + /////////////////////////////////////////////////////////// "RITCLEAR" + case MIDI_RIT_CLEAR: // only key supported + if (type == MIDI_KEY) { + // clear RIT value + vfo[active_receiver->id].rit = new; + g_idle_add(ext_vfo_update, NULL); + } + /////////////////////////////////////////////////////////// "RITSTEP" + case RIT_STEP: // key or wheel supported + // This cycles between RIT increments 1, 10, 100, 1, 10, 100, ... + switch (type) { + case MIDI_KEY: + // key cycles through in upward direction + val=1; + // FALLTHROUGH + case MIDI_WHEEL: + // wheel cycles upward or downward + if (val > 0) { + rit_increment=10*rit_increment; + } else { + rit_increment=rit_increment/10; + } + if (rit_increment < 1) rit_increment=100; + if (rit_increment > 100) rit_increment=1; + break; + default: + // do nothing + break; + } + g_idle_add(ext_vfo_update, NULL); + break; + /////////////////////////////////////////////////////////// "RITTOGGLE" + case RIT_TOGGLE: // only key supported + if (type == MIDI_KEY) { + // enable/disable RIT + new=vfo[active_receiver->id].rit_enabled; + vfo[active_receiver->id].rit_enabled = new ? 0 : 1; + g_idle_add(ext_vfo_update, NULL); + } + break; + /////////////////////////////////////////////////////////// "RITVAL" + case RIT_VAL: // wheel or knob + switch (type) { + case MIDI_WHEEL: + // This changes the RIT value incrementally, + // but we restrict the change to +/ 9.999 kHz + new = vfo[active_receiver->id].rit + val*rit_increment; + if (new > 9999) new= 9999; + if (new < -9999) new=-9999; + vfo[active_receiver->id].rit = new; + break; + case MIDI_KNOB: + // knob: adjust in the range +/ 50*rit_increment + new = (val-50) * rit_increment; + vfo[active_receiver->id].rit = new; + break; + default: + // do nothing + // we should not come here anyway + break; + } + // enable/disable RIT according to RIT value + vfo[active_receiver->id].rit_enabled = (vfo[active_receiver->id].rit == 0) ? 0 : 1; + g_idle_add(ext_vfo_update, NULL); + break; + /////////////////////////////////////////////////////////// "SAT" + case MIDI_SAT: + switch (sat_mode) { + case SAT_NONE: + sat_mode=SAT_MODE; + break; + case SAT_MODE: + sat_mode=RSAT_MODE; + break; + case RSAT_MODE: + default: + sat_mode=SAT_NONE; + break; + } + g_idle_add(ext_vfo_update, NULL); + break; + /////////////////////////////////////////////////////////// "SNB" + case SNB: // only key supported + if (type == MIDI_KEY) { + g_idle_add(ext_snb_update, NULL); + } + break; + /////////////////////////////////////////////////////////// "SPLIT" + case MIDI_SPLIT: // only key supported + // toggle split mode + if (type == MIDI_KEY) { + g_idle_add(ext_split_toggle, NULL); + } + break; + /////////////////////////////////////////////////////////// "SWAPRX" + case SWAP_RX: // only key supported + if (type == MIDI_KEY && receivers == 2) { + new=active_receiver->id; // 0 or 1 + new= (new == 1) ? 0 : 1; // id of currently inactive receiver + active_receiver=receiver[new]; + g_idle_add(menu_active_receiver_changed,NULL); + g_idle_add(ext_vfo_update,NULL); + g_idle_add(sliders_active_receiver_changed,NULL); + } + break; + /////////////////////////////////////////////////////////// "SWAPVFO" + case SWAP_VFO: // only key supported + if (type == MIDI_KEY) { + g_idle_add(ext_vfo_a_swap_b,NULL); + } + break; + /////////////////////////////////////////////////////////// "TUNE" + case MIDI_TUNE: // only key supported + if (type == MIDI_KEY && can_transmit) { + new = !tune; + g_idle_add(ext_tune_update, GINT_TO_POINTER(new)); + } + break; + /////////////////////////////////////////////////////////// "VFOA" + /////////////////////////////////////////////////////////// "VFOB" + case VFOA: // only wheel supported + case VFOB: // only wheel supported + if (type == MIDI_WHEEL && !locked) { + ip=malloc(2*sizeof(int)); + *ip = (action == VFOA) ? 0 : 1; // could use (action - VFOA) to support even more VFOs + *(ip+1)=val; + g_idle_add(ext_vfo_id_step, ip); + } + break; + /////////////////////////////////////////////////////////// "VFOSTEPDOWN" + /////////////////////////////////////////////////////////// "VFOSTEPUP" + case VFO_STEP_DOWN: // key or wheel supported + case VFO_STEP_UP: + switch (type) { + case MIDI_KEY: + new = (action == VFO_STEP_UP) ? 1 : -1; + g_idle_add(ext_update_vfo_step, GINT_TO_POINTER(new)); + break; + case MIDI_WHEEL: + new = (val > 0) ? 1 : -1; + g_idle_add(ext_update_vfo_step, GINT_TO_POINTER(new)); + break; + default: + // do nothing + // we should not come here anyway + break; + } + break; + /////////////////////////////////////////////////////////// "VOX" + case VOX: // only key supported + // toggle VOX + if (type == MIDI_KEY) { + vox_enabled = !vox_enabled; + g_idle_add(ext_vfo_update, NULL); + } + break; + /////////////////////////////////////////////////////////// "VOXLEVEL" + case VOXLEVEL: // knob or wheel supported + switch (type) { + case MIDI_WHEEL: + // This changes the value incrementally, + // but stay within limits (0.0 through 1.0) + vox_threshold += (double) val * 0.01; + if (vox_threshold > 1.0) vox_threshold=1.0; + if (vox_threshold < 0.0) vox_threshold=0.0; + break; + case MIDI_KNOB: + vox_threshold = 0.01 * (double) val; + break; + default: + // do nothing + // we should not come here anyway + break; + } + // VOX level not shown on screen, hence no VFO update + break; + /////////////////////////////////////////////////////////// "XITCLEAR" + case MIDI_XIT_CLEAR: // only key supported + if (type == MIDI_KEY) { + // this clears the XIT value and disables XIT + if(can_transmit) { + transmitter->xit = 0; + transmitter->xit_enabled = 0; + g_idle_add(ext_vfo_update, NULL); + } + } + break; + /////////////////////////////////////////////////////////// "XITVAL" + case XIT_VAL: // wheel and knob supported. + if (can_transmit) { + switch (type) { + case MIDI_WHEEL: + // This changes the XIT value incrementally, + // but we restrict the change to +/ 9.999 kHz + new = transmitter->xit + val*rit_increment; + if (new > 9999) new= 9999; + if (new < -9999) new=-9999; + transmitter->xit = new; + break; + case MIDI_KNOB: + // knob: adjust in the range +/ 50*rit_increment + new = (val-50) * rit_increment; + transmitter->xit = new; + break; + default: + // do nothing + // we should not come here anyway + break; + } + // enable/disable XIT according to XIT value + transmitter->xit_enabled = (transmitter->xit == 0) ? 0 : 1; + g_idle_add(ext_vfo_update, NULL); + } + break; + /////////////////////////////////////////////////////////// "ZOOM" + case MIDI_ZOOM: // wheel and knob + switch (type) { + case MIDI_WHEEL: +g_print("MIDI_ZOOM: MIDI_WHEEL: val=%d\n",val); + g_idle_add(ext_zoom_update,GINT_TO_POINTER(val)); + break; + case MIDI_KNOB: +g_print("MIDI_ZOOM: MIDI_KNOB: val=%d\n",val); + g_idle_add(ext_zoom_set,GINT_TO_POINTER(val)); + break; + default: + // no action for keys (should not come here anyway) + break; + } + break; + /////////////////////////////////////////////////////////// "ZOOMDOWN" + /////////////////////////////////////////////////////////// "ZOOMUP" + case ZOOM_UP: // key + case ZOOM_DOWN: // key + switch (type) { + case MIDI_KEY: + new = (action == ZOOM_UP) ? 1 : -1; + g_idle_add(ext_zoom_update,GINT_TO_POINTER(new)); + break; + case MIDI_WHEEL: + new = (val > 0) ? 1 : -1; + g_idle_add(ext_zoom_update,GINT_TO_POINTER(new)); + break; + default: + // do nothing + // we should not come here anyway + break; + } + break; + */ + case ACTION_NONE: + // No error message, this is the "official" action for un-used controller buttons. + break; + default: + // This means we have forgotten to implement an action, so we inform on stderr. + fprintf(stderr,"Unimplemented MIDI action: A=%d\n", (int) action); + } +} diff --git a/radio.c b/radio.c index 624df82..290c42c 100644 --- a/radio.c +++ b/radio.c @@ -59,6 +59,13 @@ #include "rigctl.h" #include "receiver_dialog.h" +#ifdef MIDI +// rather than including MIDI.h with all its internal stuff +// (e.g. enum components) we just declare the single bit thereof +// we need here to make a strict compiler happy. +int MIDIstartup(); +#endif + static GtkWidget *add_receiver_b; static GtkWidget *add_wideband_b; @@ -1139,6 +1146,8 @@ g_print("create_radio for %s %d\n",d->name,d->device); r->sat_mode=SAT_NONE; r->mute_rx_while_transmitting=FALSE; + r->midi = FALSE; + r->dialog=NULL; @@ -1217,6 +1226,19 @@ g_print("create_radio for %s %d\n",d->name,d->device); } } */ + + // + // MIDIstartup must not be called before the radio is completely set up, since + // then MIDI can asynchronously trigger actions which require the radio already + // running. So this is the last thing we do when starting the radio. + // +#ifdef MIDI + int rv = MIDIstartup(); + if (rv == 0) { + radio->midi = TRUE; + } +#endif + g_idle_add(radio_start,(gpointer)r); diff --git a/radio.h b/radio.h index dcb93f3..3f0a456 100644 --- a/radio.h +++ b/radio.h @@ -216,6 +216,8 @@ typedef struct _radio { gint sat_mode; gboolean mute_rx_while_transmitting; + + gboolean midi; } RADIO; diff --git a/radio_info.c b/radio_info.c index 0a6f15f..5d02945 100644 --- a/radio_info.c +++ b/radio_info.c @@ -175,7 +175,12 @@ void update_radio_info(RECEIVER *rx) { RoundedRectangle(cr, 85, top_y, 25.0, 6.0, INFO_FALSE); } // Alsa MIDI server - RoundedRectangle(cr, 125, top_y, 25.0, 6.0, INFO_FALSE); + if (radio->midi) { + RoundedRectangle(cr, 125, top_y, 25.0, 6.0, INFO_TRUE); + } else { + RoundedRectangle(cr, 125, top_y, 25.0, 6.0, INFO_FALSE); + } + // Sat mode if (radio->sat_mode==SAT_NONE) { RoundedRectangle(cr, 165, top_y, 25.0, 6.0, INFO_FALSE); @@ -228,7 +233,7 @@ void update_radio_info(RECEIVER *rx) { cairo_move_to(cr, 85, top_y + 7); cairo_show_text(cr, "DUP"); // Alsa MIDI server - SetColour(cr, DARK_TEXT); + SetColour(cr, OFF_WHITE); cairo_move_to(cr, 123, top_y + 7); cairo_show_text(cr, "MIDI"); // Sat mode