diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..ca7b296 --- /dev/null +++ b/AUTHORS @@ -0,0 +1,2 @@ +Edward Tomasz Napierała + diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..ad6bbbc --- /dev/null +++ b/COPYING @@ -0,0 +1,24 @@ +Copyright (c) 2007, 2008 Edward Tomasz Napierała +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE. + diff --git a/Makefile.am b/Makefile.am new file mode 100644 index 0000000..e5086d1 --- /dev/null +++ b/Makefile.am @@ -0,0 +1,2 @@ +SUBDIRS = src man pixmaps + diff --git a/NEWS b/NEWS new file mode 100644 index 0000000..269b27e --- /dev/null +++ b/NEWS @@ -0,0 +1,160 @@ +User-visible changes between 2.4 and 2.5 include: + + - Autotools. This should make life easier for distributors. ;-) + +User-visible changes between 2.3 and 2.4 include: + + - Keyboard layout switching. Now you can specify "-l QWERTZ" + or "-l AZERTY" and keyboard layout will be (hopefully) usable. + + - Implement output rate limiting; specifying "-r 31.25" in a command + line will limit jack-keyboard to the rate mandated by the MIDI + specification. + + - Remove the "-n" option; not connecting to the MIDI port at startup + is the default behaviour. + + - Start jackd automatically, even when not compiled against LASH. + + - Fix "make install" on systems without /usr/local/share/icons/hicolor. + +User-visible changes between 2.2 and 2.3 include: + + - Fix "make install" on systems without /usr/local/bin. + + - Fix crash with jackdmp, reported and tested by Juuso Alasuutari. + + - Make it possible to switch between windows using Alt-Tab + when the keyboard is grabbed. + +User-visible changes between 2.1 and 2.2 include: + + - Add 4 to "Octave". What that means is, C4 is in octave #4, + not #0. + + - Icon ;-) + + - Documentation updates. + +User-visible changes between 2.0 and 2.1 include: + + - It's possible to change "high" and "low" velocity. To change + the former, press and hold Shift key and move slider. To change + the latter, do the same with Ctrl key. + + Moving the slider without Ctrl or Shift held changes "normal" + velocity. + + - Fix ugly memory corruption that could cause strange behaviour + (including crashes) in pianola mode. + +User-visible changes between 1.8 and 2.0 include: + + - GUI; now jack-keyboard should be more intuitive. It can be turned + off using -G option. + + - Support keyboard grabbing - with the -K option, jack-keyboard + will get all the keyboard input, even when it does not have focus. + In other words, you can play while mousing in different program + (synth, for example) at the same time. + + Note that this will not work if another application grabs + the keyboard. + + - LASH support. + + - Install ".desktop" file. + +Changes between 1.7 and 1.8: + + - Fix compilation with SVN version of JACK. + +Changes between 1.6 and 1.7: + + - Add "keyboard cue" to show you which part of virtual keyboard your + PC keyboard keys are currently mapped to. It looks a little ugly, + so it's disabled by default; you can enable it using "-C" option. + + - Add an "-u" option to automatically send bank/program change messages + after reconnecting + + - Warn when bank/program shown in title bar could be different than the + one used by the synth + + - Fix setting bank/program from the command line + +Changes between 1.5 and 1.6: + + - Send messages with proper time offsets. If you don't like this, + you can make jack-keyboard behave same as before by using -t option + + - Implement MIDI channel switching (submitted by Nedko Arnaudov) + + - Make it possible to enter bank/program number directly using keypad + + - Make bank switching actually work + + - Don't crash when clicking _and_ releasing mouse button on the gray area + +Changes between 1.4 and 1.5: + + - Add "pianola mode" - jack-keyboard now has a MIDI input port; events received + on this port will change the visible state of keyboard - basically, you will + see the notes played. Note that, by default, jack-keyboard will refuse to + connect to other clients named 'jack-keyboard'; this is to prevent loops. + + - Make panic key more reliable + + - Numeric keys (top row on the alphanumeric keyboard) pressed with Shift held + should work correctly now + + - Threading fixes - if jack-keyboard behaved in unpredictable way, it should + be fixed now + + - Add manual page + + - Add 'install' Makefile target ;-) + +Changes between 1.3 and 1.4: + + - Don't stop the notes when moving mouse outside of the window (suggested by ctaf @ #lad) + + - Show some useful information in the window title + + - Rewrite MIDI input switching to make it more robust + + - Remove the command line MIDI port selection. Just use Insert/Delete keys to cycle to the + port you need + + - Don't refuse to start when there are no MIDI input ports to connect to (suggested by Nedko Arnaudov) + +Changes between 1.2 and 1.3: + + - Use the ringbuffer instead of old 'table of notes'. Now the notes won't + get lost even under high CPU load + + - Add 'panic key', under 'Esc' key, that stops all sounds + + - Add Bank Change (Home/End) + +Changes between 1.1 and 1.2: + + - Make it possible to run several instances of jack-keyboard at the same time + + - Add the ability to switch MIDI inputs at runtime (Insert/Delete keys) + + - Warn about JACK timing problems + + - Minor display optimization + + - Fix crash after pressing highest (#127) note + +Changes between 1.0 and 1.1: + + - Speed up redrawing + + - Add MIDI port selection ("jack-keyboard 3" will connect to fourth available + JACK MIDI input port) + + - Fix sustain key (space) + diff --git a/README b/README new file mode 100644 index 0000000..02aa815 --- /dev/null +++ b/README @@ -0,0 +1,130 @@ +What is it? +----------- + +jack-keyboard is a virtual MIDI keyboard - a program that allows you to +send JACK MIDI events (play ;-) using your PC keyboard. It's somewhat +similar to vkeybd, except it uses JACK MIDI instead of ALSA, and the +keyboard mapping is much better - it uses the same layout as trackers +(like Impulse Tracker) did, so you have two and half octaves under your +fingers. + +How to compile it? +------------------ + +If you're using FreeBSD, install from ports - audio/jack-keyboard. + +Otherwise, you need JACK with MIDI support, gtk+ 2.6 or higher, +make(1), gcc and all the standard headers. Untar the file, type +'make install' and that's it. If there is any problem, drop me +an email (trasz@FreeBSD.org) and I will help you. Really. + +How to use it? +-------------- + +You need JACK with MIDI support and some softsynth that accepts +JACK MIDI as input. Ghostess, http://home.jps.net/~musound/, +is a good choice. Of course you will also need some DSSI plugin +that will make the actual sound. WhySynth is nice. + +When you have all of these installed: first, run jackd. Then run +ghostess with a plugin of choice. Then run jack-keyboard. Press +'z' key. You should hear sound. + +Keyboard +-------- + +Keyboard mapping is the same as in Impulse Tracker. This is your +QWERTY keyboard: + + +----+----+ +----+----+----+ +----+----+ + | 2 | 3 | | 5 | 6 | 7 | | 9 | 0 | ++----+----+----+----+----+----+----+----+----+----+ +| q | w | e | r | t | y | u | i | o | p | ++----+----+----+----+----+----+----+----+----+----+ + | s | d | | g | h | j | + +----+----+----+----+----+----+----+ + | z | x | c | v | b | n | m | + +----+----+----+----+----+----+----+ + +And this is MIDI mapping. + + +----+----+ +----+----+----+ +----+----+ + |C#5 |D#5 | |F#5 |G#5 |A#5 | |C#6 |D#6 | ++----+----+----+----+----+----+----+----+----+----+ +| C5 | D5 | E5 | F5 | G5 | A5 | B5 | C6 | D6 | E6 | ++----+----+----+----+----+----+----+----+----+----+ + |C#4 |D#4 | |F#4 |G#4 |A#4 | + +----+----+----+----+----+----+----+ + | C4 | D4 | E4 | F4 | G4 | A4 | B4 | + +----+----+----+----+----+----+----+ + +Spacebar is a sustain key. Holding it when pressing or releasing key +will make that key sustained, i.e. Note Off MIDI event won't be sent +after releasing the key. To release (stop) all the sustained notes, +press and release spacebar. + +Holding Shift when pressing note will make it louder (it increases +velocity). Holding Ctrl will do the opposite. You can change the +default velocity by moving the Velocity slider. You can change the +"high" and "low" velocity values by moving the slider while holding +Shift or Ctrl keys. + +Pressing "-" and "+" keys on numeric keypad changes the octave your +keyboard is mapped to. Pressing "*" and "/" on numeric keypad changes +MIDI program (instrument). Pressing Insert or Delete keys will connect +jack-keyboard to the next/previous MIDI input port (it will cycle +between running instances of ghostess, for example). Home and End keys +change the MIDI channel. Page Up and Page Down keys switch the MIDI +bank. + +Esc works as a panic key - when you press it, all sound stops. + +Setting channel/bank/program number directly +-------------------------------------------- + +To switch directly to a channel, bank or program, enter its number on +the numeric keypad (it won't be shown in any way) and press Home or End +(to change channel), Page Up or Page Down (to change bank) or "/" or +"*" (to change program). For example, to change to program number 123, +type, on the numeric keypad, "123/", without quotes. + +Titlebar +-------- + +When -G xor -T is given, some informational messages in the title bar +appear. They are supposed to be self explanatory. If you see +"bank/program change not sent", it means that the bank/program numbers +as seen in the title bar were not sent. In other words, synth the +jack-keyboard is connected to may use different values. This happens +at startup and after switching between synths (using Insert/Delete +keys). To send bank/program change at startup, use -b and -p parame- +ters. To automatically send bank/program change after reconnect, use +the -u option. + +Pianola mode +------------ + +In addition to the MIDI output port, jack-keyboard also opens MIDI +input (listening) port. MIDI events going into this port will be +passed to the output port unmodified, except for channel number, which +will be set to the one jack-keyboard is configured to use. Note On and +Note Off MIDI events will cause visible effect (pressing and releasing) +on keys, just like if they were being pressed using keyboard or mouse. + +jack-keyboard will never connect to it's own MIDI input port. It will +also refuse to connect to any other client whose name begins in "jack- +keyboard", unless the "-k" option is given. It is, however, possible +to connect these ports manually, using jack_connect or qjackctl; this +may create feedback loop. + +License +------- + +JACK Keyboard is distributed under the BSD license, two clause. + +Contact +------- + +If you have any questions, comments, suggestions, patches or anything, +let me know: Edward Tomasz Napierala . + diff --git a/TODO b/TODO new file mode 100644 index 0000000..fe7e447 --- /dev/null +++ b/TODO @@ -0,0 +1,13 @@ +Things to do, eventually: + + - I know nothing about designing GUIs, and I'm afraid it shows. Redesign. + + - Jack-keyboard does strange things to input focus and keyboard event + handling. Verify that it does not cause any problems. + + - Make the code cleaner. Get rid of all these global variables etc. + + - Tooltips. + + - Fix language (english) errors in documentation and web page. + diff --git a/configure.ac b/configure.ac new file mode 100644 index 0000000..bcc8aeb --- /dev/null +++ b/configure.ac @@ -0,0 +1,81 @@ +# -*- Autoconf -*- +# Process this file with autoconf to produce a configure script. + +AC_PREREQ(2.61) +AC_INIT([jack-keyboard], [2.5], [trasz@FreeBSD.org]) +AM_INIT_AUTOMAKE([-Wall foreign]) +AC_CONFIG_SRCDIR([config.h.in]) +AC_CONFIG_HEADERS([config.h]) + +# Checks for programs. +AC_PROG_CC + +# Checks for libraries. + +# Checks for header files. +AC_HEADER_STDC +AC_CHECK_HEADERS([stdlib.h string.h sys/time.h unistd.h]) + +# Checks for typedefs, structures, and compiler characteristics. +AC_C_CONST +AC_HEADER_TIME +AC_C_VOLATILE + +# Checks for library functions. +AC_FUNC_MALLOC +AC_FUNC_STRTOD +AC_CHECK_FUNCS([gettimeofday memset strcasecmp strdup]) + +PKG_CHECK_MODULES(GTK, gtk+-2.0 >= 2.2) +AC_SUBST(GTK_CFLAGS) +AC_SUBST(GTK_LIBS) + +PKG_CHECK_MODULES(GLIB, glib-2.0 >= 2.2) +AC_SUBST(GLIB_CFLAGS) +AC_SUBST(GLIB_LIBS) + +PKG_CHECK_MODULES(GTHREAD, gthread-2.0 >= 2.2) +AC_SUBST(GTHREAD_CFLAGS) +AC_SUBST(GTHREAD_LIBS) + +AC_ARG_WITH([x11], + [AS_HELP_STRING([--with-x11], + [support keyboard grabbing @<:@default=check@:>@])], + [], + [with_x11=check]) + +AS_IF([test "x$with_x11" != xno], + [PKG_CHECK_MODULES(X11, x11, AC_DEFINE([HAVE_X11], [], [Defined if we have X11 support.]), + [if test "x$with_x11" != xcheck; then + AC_MSG_FAILURE([--with-x11 was given, but x11 was not found]) + fi + ])]) + +AC_SUBST(X11_CFLAGS) +AC_SUBST(X11_LIBS) + +PKG_CHECK_MODULES(JACK, jack >= 0.102.0) +AC_SUBST(JACK_CFLAGS) +AC_SUBST(JACK_LIBS) + +PKG_CHECK_MODULES(JACK_MIDI_NEEDS_NFRAMES, jack < 0.105.00, AC_DEFINE(JACK_MIDI_NEEDS_NFRAMES, 1, [whether or not JACK routines need nframes parameter]), true) + +AC_ARG_WITH([lash], + [AS_HELP_STRING([--with-lash], + [support LASH @<:@default=check@:>@])], + [], + [with_lash=check]) + +AS_IF([test "x$with_lash" != xno], + [PKG_CHECK_MODULES(LASH, lash-1.0, AC_DEFINE([HAVE_LASH], [], [Defined if we have LASH support.]), + [if test "x$with_lash" != xcheck; then + AC_MSG_FAILURE([--with-lash was given, but LASH was not found]) + fi + ])]) + +AC_SUBST(LASH_CFLAGS) +AC_SUBST(LASH_LIBS) + +AC_CONFIG_FILES([Makefile src/Makefile man/Makefile pixmaps/Makefile]) +AC_OUTPUT + diff --git a/man/Makefile.am b/man/Makefile.am new file mode 100644 index 0000000..524ab5b --- /dev/null +++ b/man/Makefile.am @@ -0,0 +1,4 @@ +man_MANS = jack-keyboard.1 + +EXTRA_DIST = $(man_MANS) + diff --git a/man/jack-keyboard.1 b/man/jack-keyboard.1 new file mode 100644 index 0000000..a6ed5ef --- /dev/null +++ b/man/jack-keyboard.1 @@ -0,0 +1,186 @@ +.\" This manpage has been automatically generated by docbook2man +.\" from a DocBook document. This tool can be found at: +.\" +.\" Please send any bug reports, improvements, comments, patches, +.\" etc. to Steve Cheng . +.TH "JACK-KEYBOARD" "1" "20 April 2008" "jack-keyboard 2.4" "" + +.SH NAME +jack-keyboard \- A virtual keyboard for JACK MIDI +.SH SYNOPSIS + +\fBjack-keyboard\fR [ \fB-C\fR ] [ \fB-G\fR ] [ \fB-K\fR ] [ \fB-T\fR ] [ \fB-V\fR ] [ \fB-a \fIinput port\fB\fR ] [ \fB-k\fR ] [ \fB-r \fIrate\fB\fR ] [ \fB-t\fR ] [ \fB-u\fR ] [ \fB-c \fIchannel\fB\fR ] [ \fB-b \fIbank\fB\fR ] [ \fB-p \fIprogram\fB\fR ] [ \fB-l \fIlayout\fB\fR ] + +.SH "OPTIONS" +.TP +\fB-C\fR +Enable "keyboard cue" - two +horizontal lines over a part of keyboard; keys under the lower line are mapped +to the lower row of your PC keyboard; keys under the upper line are mapped +to the upper row. +.TP +\fB-G\fR +Disable GUI. It makes \fBjack-keyboard\fR look like it did before version 2.0. +.TP +\fB-K\fR +Grab the keyboard. This makes \fBjack-keyboard\fR receive keyboard events +even when it does not have focus. In other words, you can play while mousing in a different +window. + +Note: It's not reliable yet. It does not work when some other application keeps +the keyboard grabbed. It does not work with GNOME. Even when it seems to work, +some keyboard events may get lost. +.TP +\fB-T\fR +Toggle titlebar, on which channel/bank/program information is shown. +With -G option, it disables titlebar; otherwise it enables it. +.TP +\fB-V\fR +Print version number to standard output and exit. +.TP +\fB-a \fIinput port\fB\fR +Automatically connect to the named input port. Note that this may cause problems with LASH. +.TP +\fB-k\fR +Allow connecting to other instances of jack-keyboard (see PIANOLA MODE below). +Without this option, \fBjack-keyboard\fR will refuse to connect to any +JACK client whose name starts in "jack-keyboard"; this is to prevent loops. +Note that it's impossible to connect instance of \fBjack-keyboard\fR +to itself, even with this option set. +.TP +\fB-r \fIrate\fB\fR +Set rate limit to \fIrate\fR, in Kbaud. Limit +defined by the MIDI specification is 31.25. By default this parameter is zero, that +is, rate limiting is disabled. +.TP +\fB-t\fR +Send all MIDI messages with zero time offset, making them play as soon +as they reach the synth. This was the default behavior before version 1.6. +.TP +\fB-u\fR +By default, \fBjack-keyboard\fR does not send program/bank +change messages after reconnecting, so the newly connected instrument +remains at previous settings. This option changes that behaviour. +.TP +\fB-c \fIchannel\fB\fR +Set initial MIDI channel to \fIchannel\fR; +by default it's 1. +.TP +\fB-b \fIbank\fB\fR +Set initial MIDI bank to \fIbank\fR\&. +With this option, \fBjack-keyboard\fR will send the bank/program +change once, when it connects. +.TP +\fB-p \fIprogram\fB\fR +Set initial MIDI program to \fIprogram\fR\&. +With this option, \fBjack-keyboard\fR will send the bank/program +change once, when it connects. +.TP +\fB-l \fIlayout\fB\fR +Specify the layout of computer keyboard being used. Valid arguments are QWERTY, +QWERTZ and AZERTY. Default is QWERTY. +.SH "DESCRIPTION" +.PP +\fBjack-keyboard\fR is a virtual MIDI keyboard - a program that allows +you to send JACK MIDI events (play ;-) using your PC keyboard. It's +somewhat similar to \fBvkeybd\fR, except it uses JACK MIDI instead of +ALSA, and the keyboard mapping is much better - it uses the same +layout as trackers (like Impulse Tracker) did, so you have two and +half octaves under your fingers. +.SH "KEY BINDINGS" +.PP +Keyboard mapping is the same as in Impulse Tracker. This is your +QWERTY keyboard: + +.nf + +----+----+ +----+----+----+ +----+----+ + | 2 | 3 | | 5 | 6 | 7 | | 9 | 0 | + +----+----+----+----+----+----+----+----+----+----+ + | Q | W | E | R | T | Y | U | I | O | P | + +----+----+----+----+----+----+----+----+----+----+ + | S | D | | G | H | J | + +----+----+----+----+----+----+----+ + | Z | X | C | V | B | N | M | + +----+----+----+----+----+----+----+ + +.fi +And this is MIDI mapping: + +.nf + +----+----+ +----+----+----+ +----+----+ + |C#5 |D#5 | |F#5 |G#5 |A#5 | |C#6 |D#6 | + +----+----+----+----+----+----+----+----+----+----+ + | C5 | D5 | E5 | F5 | G5 | A5 | B5 | C6 | D6 | E6 | + +----+----+----+----+----+----+----+----+----+----+ + |C#4 |D#4 | |F#4 |G#4 |A#4 | + +----+----+----+----+----+----+----+ + | C4 | D4 | E4 | F4 | G4 | A4 | B4 | + +----+----+----+----+----+----+----+ + +.fi +.PP +Spacebar is a sustain key. Holding it when pressing or releasing key +will make that key sustained, i.e. Note Off MIDI event won't be sent +after releasing the key. To release (stop) all the sustained notes, +press and release spacebar. +.PP +Holding Shift when pressing note will make it louder (it increases +velocity). Holding Ctrl will do the opposite. You can change the +default velocity by moving the Velocity slider. You can change the "high" +and "low" velocity values by moving the slider while holding Shift +or Ctrl keys. +.PP +Pressing "-" and "+" +keys on numeric keypad changes the octave your keyboard is mapped to. +Pressing "*" and "/" on numeric keypad changes +MIDI program (instrument). Pressing Insert or Delete keys will +connect \fBjack-keyboard\fR to the next/previous MIDI input port (it will cycle between running +instances of ghostess, for example). Home and End keys change the MIDI channel. +Page Up and Page Down keys switch the MIDI bank. +.PP +Esc works as a panic key - when you press it, all sound stops. +.SH "SETTING CHANNEL/BANK/PROGRAM NUMBER DIRECTLY" +.PP +To switch directly to a channel, bank or program, enter its number on the numeric +keypad (it won't be shown in any way) and press Home or End (to change channel), +Page Up or Page Down (to change bank) or "/" or "*" +(to change program). For example, to change to program number 123, +type, on the numeric keypad, "123/", without quotes. +.SH "TITLEBAR" +.PP +When \fB-G\fR xor \fB-T\fR is given, some informational +messages in the title bar appear. They are supposed to be self explanatory. +If you see "bank/program change not sent", +it means that the bank/program numbers as seen in the title bar were not sent. In other words, +synth the \fBjack-keyboard\fR is connected to may use different values. This happens +at startup and after switching between synths (using Insert/Delete keys). To send bank/program +change at startup, use \fB-b\fR and \fB-p\fR parameters. To automatically +send bank/program change after reconnect, use the \fB-u\fR option. +.SH "PIANOLA MODE" +.PP +In addition to the MIDI output port, \fBjack-keyboard\fR also opens MIDI input (listening) port. +MIDI events going into this port will be passed to the output port unmodified, except for channel number, +which will be set to the one \fBjack-keyboard\fR is configured to use. Note On and Note Off +MIDI events will cause visible effect (pressing and releasing) on keys, just like if they were being pressed +using keyboard or mouse. +.PP +\fBjack-keyboard\fR will never connect to it's own MIDI input port. It will also refuse +to connect to any other client whose name begins in "jack-keyboard", unless the "-k" option is given. +It is, however, possible to connect these ports manually, using \fBjack_connect\fR +or \fBqjackctl\fR; this may create feedback loop. +.SH "SEE ALSO" +.PP +\fBjackd\fR(1), +\fBghostess\fR(1), +\fBqjackctl\fR(1) +.SH "BUGS" +.PP +Key grabbing is unreliable. +.PP +Many PC keyboards have problems with polyphony. For example, with the one I'm using right now, +it's impossible to press "c", "v" and "b" at the same time. It's a hardware problem, not the +software one. +.PP +The spin widgets used to set channel/bank/program number don't take focus, so the value cannot be entered +into them in the usual way. It's because \fBjack-keyboard\fR already uses numeric keys +for different purposes. You can still directly enter channel/bank/program in a way described above. diff --git a/pixmaps/Makefile.am b/pixmaps/Makefile.am new file mode 100644 index 0000000..a7011ab --- /dev/null +++ b/pixmaps/Makefile.am @@ -0,0 +1,17 @@ +pixmapsdir = $(datadir)/pixmaps +pixmaps_DATA = jack-keyboard.png +EXTRA_DIST = $(pixmaps_DATA) + +gtk_update_icon_cache = gtk-update-icon-cache -f -t $(datadir)/icons/hicolor + +install-data-hook: update-icon-cache +uninstall-hook: update-icon-cache +update-icon-cache: + @-if test -z "$(DESTDIR)"; then \ + echo "Updating Gtk icon cache."; \ + $(gtk_update_icon_cache); \ + else \ + echo "*** Icon cache not updated. After (un)install, run this:"; \ + echo "*** $(gtk_update_icon_cache)"; \ + fi + diff --git a/pixmaps/jack-keyboard.png b/pixmaps/jack-keyboard.png new file mode 100644 index 0000000..9f73413 Binary files /dev/null and b/pixmaps/jack-keyboard.png differ diff --git a/src/Makefile.am b/src/Makefile.am new file mode 100644 index 0000000..b5b3e91 --- /dev/null +++ b/src/Makefile.am @@ -0,0 +1,10 @@ +bin_PROGRAMS = jack-keyboard +jack_keyboard_SOURCES = jack-keyboard.c pianokeyboard.c pianokeyboard.h +jack_keyboard_LDADD = $(GTK_LIBS) $(GLIB_LIBS) $(GTHREAD_LIBS) $(X11_LIBS) $(JACK_LIBS) $(LASH_LIBS) +jack_keyboard_CFLAGS = $(GTK_CFLAGS) $(GLIB_CFLAGS) $(GTHREAD_CFLAGS) $(X11_CFLAGS) $(JACK_CFLAGS) $(LASH_CFLAGS) \ + -DG_LOG_DOMAIN=\"jack-keyboard\" + +desktopdir = $(datadir)/applications +desktop_DATA = jack-keyboard.desktop +EXTRA_DIST = $(desktop_DATA) + diff --git a/src/jack-keyboard.c b/src/jack-keyboard.c new file mode 100644 index 0000000..5f8bc7c --- /dev/null +++ b/src/jack-keyboard.c @@ -0,0 +1,1865 @@ +/*- + * Copyright (c) 2007, 2008 Edward Tomasz Napierała + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +/* + * This is jack-keyboard 2.5, a virtual keyboard for JACK MIDI. + * + * For questions and comments, contact Edward Tomasz Napierala . + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "pianokeyboard.h" + +#ifdef HAVE_LASH +#include +#endif + +#ifdef HAVE_X11 +#include +#endif + +#define NNOTES 127 + +#define VELOCITY_MAX 127 +#define VELOCITY_HIGH 100 +#define VELOCITY_NORMAL 64 +#define VELOCITY_LOW 32 +#define VELOCITY_MIN 1 + +#define OUTPUT_PORT_NAME "midi_out" +#define INPUT_PORT_NAME "midi_in" +#define PACKAGE_NAME "jack-keyboard" +#define PACKAGE_VERSION "2.5" + +jack_port_t *output_port; +jack_port_t *input_port; +int entered_number = -1; +int allow_connecting_to_own_kind = 0; +int enable_gui = 1; +int grab_keyboard_at_startup = 0; +volatile int keyboard_grabbed = 0; +int enable_window_title = 0; +int time_offsets_are_zero = 0; +int send_program_change_at_reconnect = 0; +int send_program_change_once = 0; +int program_change_was_sent = 0; +int velocity_high = VELOCITY_HIGH; +int velocity_normal = VELOCITY_NORMAL; +int velocity_low = VELOCITY_LOW; +int *current_velocity = &velocity_normal; +int octave = 4; +double rate_limit = 0.0; +jack_client_t *jack_client = NULL; + +#ifdef HAVE_LASH +lash_client_t *lash_client; +#endif + +#define MIDI_NOTE_ON 0x90 +#define MIDI_NOTE_OFF 0x80 +#define MIDI_PROGRAM_CHANGE 0xC0 +#define MIDI_CONTROLLER 0xB0 +#define MIDI_RESET 0xFF +#define MIDI_HOLD_PEDAL 64 +#define MIDI_ALL_SOUND_OFF 120 +#define MIDI_ALL_MIDI_CONTROLLERS_OFF 121 +#define MIDI_ALL_NOTES_OFF 123 +#define MIDI_BANK_SELECT_MSB 0 +#define MIDI_BANK_SELECT_LSB 32 + +#define BANK_MIN 0 +#define BANK_MAX 127 +#define PROGRAM_MIN 0 +#define PROGRAM_MAX 16383 +#define CHANNEL_MIN 1 +#define CHANNEL_MAX 16 + +GtkWidget *window, *sustain_button, *channel_spin, *bank_spin, *program_spin, *connected_to_combo, + *velocity_hscale, *grab_keyboard_checkbutton, *octave_spin; +PianoKeyboard *keyboard; +GtkListStore *connected_to_store; + +#ifdef HAVE_X11 +Display *dpy; +#endif + +struct MidiMessage { + jack_nframes_t time; + int len; /* Length of MIDI message, in bytes. */ + unsigned char data[3]; +}; + +#define RINGBUFFER_SIZE 1024*sizeof(struct MidiMessage) + +/* Will emit a warning if time between jack callbacks is longer than this. */ +#define MAX_TIME_BETWEEN_CALLBACKS 0.1 + +/* Will emit a warning if execution of jack callback takes longer than this. */ +#define MAX_PROCESSING_TIME 0.01 + +jack_ringbuffer_t *ringbuffer; + +/* Number of currently used program. */ +int program = 0; + +/* Number of currently selected bank. */ +int bank = 0; + +/* Number of currently selected channel (0..15). */ +int channel = 0; + +void draw_note(int key); +void queue_message(struct MidiMessage *ev); + +double +get_time(void) +{ + double seconds; + int ret; + struct timeval tv; + + ret = gettimeofday(&tv, NULL); + + if (ret) { + perror("gettimeofday"); + exit(EX_OSERR); + } + + seconds = tv.tv_sec + tv.tv_usec / 1000000.0; + + return seconds; +} + +double +get_delta_time(void) +{ + static double previously = -1.0; + double now; + double delta; + + now = get_time(); + + if (previously == -1.0) { + previously = now; + + return 0; + } + + delta = now - previously; + previously = now; + + assert(delta >= 0.0); + + return delta; +} + +gboolean +process_received_message_async(gpointer evp) +{ + int i; + struct MidiMessage *ev = (struct MidiMessage *)evp; + + if (ev->data[0] == MIDI_RESET || (ev->data[0] == MIDI_CONTROLLER && + (ev->data[1] == MIDI_ALL_NOTES_OFF || ev->data[1] == MIDI_ALL_SOUND_OFF))) { + + for (i = 0; i < NNOTES; i++) { + piano_keyboard_set_note_off(keyboard, i); + } + } + + if (ev->data[0] == MIDI_NOTE_ON) { + piano_keyboard_set_note_on(keyboard, ev->data[1]); + } + + if (ev->data[0] == MIDI_NOTE_OFF) { + piano_keyboard_set_note_off(keyboard, ev->data[1]); + } + + queue_message(ev); + + return FALSE; +} + +struct MidiMessage * +midi_message_from_midi_event(jack_midi_event_t event) +{ + struct MidiMessage *ev = malloc(sizeof(*ev)); + + if (ev == NULL) { + perror("malloc"); + return NULL; + } + + assert(event.size >= 1 && event.size <= 3); + + ev->len = event.size; + ev->time = event.time; + + memcpy(ev->data, event.buffer, ev->len); + + return ev; +} + +gboolean +warning_async(gpointer s) +{ + const char *str = (const char *)s; + + g_warning(str); + + return FALSE; +} + +void +warn_from_jack_thread_context(const char *str) +{ + g_idle_add(warning_async, (gpointer)str); +} + +void +process_midi_input(jack_nframes_t nframes) +{ + int read, events, i; + void *port_buffer; + jack_midi_event_t event; + + port_buffer = jack_port_get_buffer(input_port, nframes); + if (port_buffer == NULL) { + warn_from_jack_thread_context("jack_port_get_buffer failed, cannot receive anything."); + return; + } + +#ifdef JACK_MIDI_NEEDS_NFRAMES + events = jack_midi_get_event_count(port_buffer, nframes); +#else + events = jack_midi_get_event_count(port_buffer); +#endif + + for (i = 0; i < events; i++) { + struct MidiMessage *rev; + +#ifdef JACK_MIDI_NEEDS_NFRAMES + read = jack_midi_event_get(&event, port_buffer, i, nframes); +#else + read = jack_midi_event_get(&event, port_buffer, i); +#endif + if (read) { + warn_from_jack_thread_context("jack_midi_event_get failed, RECEIVED NOTE LOST."); + continue; + } + + if (event.size > 3) { + warn_from_jack_thread_context("Ignoring MIDI message longer than three bytes, probably a SysEx."); + continue; + } + + rev = midi_message_from_midi_event(event); + if (rev == NULL) { + warn_from_jack_thread_context("midi_message_from_midi_event failed, RECEIVED NOTE LOST."); + continue; + } + + g_idle_add(process_received_message_async, rev); + } +} + +double +nframes_to_ms(jack_nframes_t nframes) +{ + jack_nframes_t sr; + + sr = jack_get_sample_rate(jack_client); + + assert(sr > 0); + + return (nframes * 1000.0) / (double)sr; +} + +void +process_midi_output(jack_nframes_t nframes) +{ + int read, t, bytes_remaining; + unsigned char *buffer; + void *port_buffer; + jack_nframes_t last_frame_time; + struct MidiMessage ev; + + last_frame_time = jack_last_frame_time(jack_client); + + port_buffer = jack_port_get_buffer(output_port, nframes); + if (port_buffer == NULL) { + warn_from_jack_thread_context("jack_port_get_buffer failed, cannot send anything."); + return; + } + +#ifdef JACK_MIDI_NEEDS_NFRAMES + jack_midi_clear_buffer(port_buffer, nframes); +#else + jack_midi_clear_buffer(port_buffer); +#endif + + /* We may push at most one byte per 0.32ms to stay below 31.25 Kbaud limit. */ + bytes_remaining = nframes_to_ms(nframes) * rate_limit; + + while (jack_ringbuffer_read_space(ringbuffer)) { + read = jack_ringbuffer_peek(ringbuffer, (char *)&ev, sizeof(ev)); + + if (read != sizeof(ev)) { + warn_from_jack_thread_context("Short read from the ringbuffer, possible note loss."); + jack_ringbuffer_read_advance(ringbuffer, read); + continue; + } + + bytes_remaining -= ev.len; + + if (rate_limit > 0.0 && bytes_remaining <= 0) { + warn_from_jack_thread_context("Rate limiting in effect."); + break; + } + + t = ev.time + nframes - last_frame_time; + + /* If computed time is too much into the future, we'll need + to send it later. */ + if (t >= (int)nframes) + break; + + /* If computed time is < 0, we missed a cycle because of xrun. */ + if (t < 0) + t = 0; + + if (time_offsets_are_zero) + t = 0; + + jack_ringbuffer_read_advance(ringbuffer, sizeof(ev)); + +#ifdef JACK_MIDI_NEEDS_NFRAMES + buffer = jack_midi_event_reserve(port_buffer, t, ev.len, nframes); +#else + buffer = jack_midi_event_reserve(port_buffer, t, ev.len); +#endif + + if (buffer == NULL) { + warn_from_jack_thread_context("jack_midi_event_reserve failed, NOTE LOST."); + break; + } + + memcpy(buffer, ev.data, ev.len); + } +} + +int +process_callback(jack_nframes_t nframes, void *notused) +{ +#ifdef MEASURE_TIME + if (get_delta_time() > MAX_TIME_BETWEEN_CALLBACKS) { + warn_from_jack_thread_context("Had to wait too long for JACK callback; scheduling problem?"); + } +#endif + + /* Check for impossible condition that actually happened to me, caused by some problem between jackd and OSS4. */ + if (nframes <= 0) { + warn_from_jack_thread_context("Process callback called with nframes = 0; bug in JACK?"); + return 0; + } + + process_midi_input(nframes); + process_midi_output(nframes); + +#ifdef MEASURE_TIME + if (get_delta_time() > MAX_PROCESSING_TIME) { + warn_from_jack_thread_context("Processing took too long; scheduling problem?"); + } +#endif + + return 0; +} + +void +queue_message(struct MidiMessage *ev) +{ + int written; + + if (jack_ringbuffer_write_space(ringbuffer) < sizeof(*ev)) { + g_critical("Not enough space in the ringbuffer, NOTE LOST."); + return; + } + + written = jack_ringbuffer_write(ringbuffer, (char *)ev, sizeof(*ev)); + + if (written != sizeof(*ev)) + g_warning("jack_ringbuffer_write failed, NOTE LOST."); +} + +void +queue_new_message(int b0, int b1, int b2) +{ + struct MidiMessage ev; + + /* For MIDI messages that specify a channel number, filter the original + channel number out and add our own. */ + if (b0 >= 0x80 && b0 <= 0xEF) { + b0 &= 0xF0; + b0 += channel; + } + + if (b1 == -1) { + ev.len = 1; + ev.data[0] = b0; + + } else if (b2 == -1) { + ev.len = 2; + ev.data[0] = b0; + ev.data[1] = b1; + + } else { + ev.len = 3; + ev.data[0] = b0; + ev.data[1] = b1; + ev.data[2] = b2; + } + + ev.time = jack_frame_time(jack_client); + + queue_message(&ev); +} + +gboolean +update_connected_to_combo_async(gpointer notused) +{ + int i, count = 0; + const char **connected, **available, *my_name; + GtkTreeIter iter; + + if (jack_client == NULL || output_port == NULL) + return FALSE; + + connected = jack_port_get_connections(output_port); + available = jack_get_ports(jack_client, NULL, JACK_DEFAULT_MIDI_TYPE, JackPortIsInput); + my_name = jack_port_name(input_port); + + assert(my_name); + + /* There will be at least one listening MIDI port - the one we create. */ + assert(available); + + if (available != NULL) { + gtk_list_store_clear(connected_to_store); + + for (i = 0; available[i] != NULL; i++) { + if (!strcmp(available[i], my_name) || (!allow_connecting_to_own_kind && + !strncmp(available[i], my_name, strlen(PACKAGE_NAME)))) + + continue; + + count++; + + gtk_list_store_append(connected_to_store, &iter); + gtk_list_store_set(connected_to_store, &iter, 0, available[i], -1); + + if (connected != NULL && connected[0] != NULL && !strcmp(available[i], connected[0])) + gtk_combo_box_set_active_iter(GTK_COMBO_BOX(connected_to_combo), &iter); + } + } + + if (count > 0) + gtk_widget_set_sensitive(connected_to_combo, TRUE); + else + gtk_widget_set_sensitive(connected_to_combo, FALSE); + + if (connected != NULL) + free(connected); + + free(available); + + return FALSE; +} + +void +draw_window_title(void) +{ + int i, off = 0; + char title[256]; + const char **connected_ports; + + if (window == NULL) + return; + + if (enable_window_title && jack_client != NULL) { + connected_ports = jack_port_get_connections(output_port); + + off += snprintf(title, sizeof(title) - off, "%s: channel %d, bank %d, program %d", + jack_get_client_name(jack_client), channel + 1, bank, program); + + if (!program_change_was_sent) + off += snprintf(title + off, sizeof(title) - off, " (bank/program change not sent)"); + + if (connected_ports == NULL || connected_ports[0] == NULL) { + off += snprintf(title + off, sizeof(title) - off, ", NOT CONNECTED"); + + } else { + off += snprintf(title + off, sizeof(title) - off, ", connected to "); + + for (i = 0; connected_ports[i] != NULL; i++) { + off += snprintf(title + off, sizeof(title) - off, "%s%s", + i == 0 ? "" : ", ", connected_ports[i]); + } + } + + if (connected_ports != NULL) + free(connected_ports); + + gtk_window_set_title(GTK_WINDOW(window), title); + } else { + /* May be null if JACK is not initialized yet. */ + if (jack_client != NULL) + gtk_window_set_title(GTK_WINDOW(window), jack_get_client_name(jack_client)); + else + gtk_window_set_title(GTK_WINDOW(window), PACKAGE_NAME); + } +} + +gboolean +update_window_title_async(gpointer notused) +{ + if (window != NULL) + draw_window_title(); + + return FALSE; +} + +int +graph_order_callback(void *notused) +{ + g_idle_add(update_window_title_async, NULL); + g_idle_add(update_connected_to_combo_async, NULL); + + return 0; +} + +void +send_program_change(void) +{ + if (jack_port_connected(output_port) == 0) + return; + + queue_new_message(MIDI_CONTROLLER, MIDI_BANK_SELECT_LSB, bank % 128); + queue_new_message(MIDI_CONTROLLER, MIDI_BANK_SELECT_MSB, bank / 128); + queue_new_message(MIDI_PROGRAM_CHANGE, program, -1); + + program_change_was_sent = 1; +} + +/* Connects to the specified input port, disconnecting already connected ports. */ +int +connect_to_input_port(const char *port) +{ + int ret; + + if (!strcmp(port, jack_port_name(input_port))) + return -1; + + if (!allow_connecting_to_own_kind) { + if (!strncmp(port, jack_port_name(input_port), strlen(PACKAGE_NAME))) + return -2; + } + + ret = jack_port_disconnect(jack_client, output_port); + if (ret) { + g_warning("Cannot disconnect MIDI port."); + + return -3; + } + + ret = jack_connect(jack_client, jack_port_name(output_port), port); + if (ret) { + g_warning("Cannot connect to %s.", port); + + return -4; + } + + g_warning("Connected to %s.", port); + + program_change_was_sent = 0; + + if (send_program_change_at_reconnect || send_program_change_once) { + send_program_change(); + draw_window_title(); + } + + send_program_change_once = 0; + + return 0; +} + +void +connect_to_another_input_port(int up_not_down) +{ + const char **available_midi_ports; + const char **connected_ports; + const char *current = NULL; + int i, max, current_index; + + available_midi_ports = jack_get_ports(jack_client, NULL, JACK_DEFAULT_MIDI_TYPE, JackPortIsInput); + + /* There will be at least one listening MIDI port - the one we create. */ + assert(available_midi_ports); + + /* + * max is the highest possible index into available_midi_ports[], + * i.e. number of elements - 1. + */ + for (max = 0; available_midi_ports[max + 1] != NULL; max++); + + /* Only one input port - our own. */ + if (max == 0) { + g_warning("No listening JACK MIDI input ports found. " + "Run some softsynth and press Insert or Delete key."); + + return; + } + + connected_ports = jack_port_get_connections(output_port); + + if (connected_ports != NULL && connected_ports[0] != NULL) + current = connected_ports[0]; + else + current = available_midi_ports[0]; + + /* + * current is the index of currently connected port into available_midi_ports[]. + */ + for (i = 0; i <= max && strcmp(available_midi_ports[i], current); i++); + + current_index = i; + + assert(!strcmp(available_midi_ports[current_index], current)); + + /* XXX: rewrite. */ + if (up_not_down) { + for (i = current_index + 1; i <= max; i++) { + assert(available_midi_ports[i] != NULL); + + if (connect_to_input_port(available_midi_ports[i]) == 0) + goto connected; + } + + for (i = 0; i <= current_index; i++) { + assert(available_midi_ports[i] != NULL); + + if (connect_to_input_port(available_midi_ports[i]) == 0) + goto connected; + } + + } else { + for (i = current_index - 1; i >= 0; i--) { + assert(available_midi_ports[i] != NULL); + + if (connect_to_input_port(available_midi_ports[i]) == 0) + goto connected; + } + + for (i = max; i >= current_index; i--) { + assert(available_midi_ports[i] != NULL); + + if (connect_to_input_port(available_midi_ports[i]) == 0) + goto connected; + } + } + + g_warning("Cannot connect to any of the input ports."); + +connected: + free(available_midi_ports); + + if (connected_ports != NULL) + free(connected_ports); +} + +void +connect_to_next_input_port(void) +{ + connect_to_another_input_port(1); +} + +void +connect_to_prev_input_port(void) +{ + connect_to_another_input_port(0); +} + +void +init_jack(void) +{ + int err; + +#ifdef HAVE_LASH + lash_event_t *event; +#endif + + jack_client = jack_client_open(PACKAGE_NAME, JackNullOption, NULL); + + if (jack_client == NULL) { + g_critical("Could not connect to the JACK server; run jackd first?"); + exit(EX_UNAVAILABLE); + } + + ringbuffer = jack_ringbuffer_create(RINGBUFFER_SIZE); + + if (ringbuffer == NULL) { + g_critical("Cannot create JACK ringbuffer."); + exit(EX_SOFTWARE); + } + + jack_ringbuffer_mlock(ringbuffer); + +#ifdef HAVE_LASH + event = lash_event_new_with_type(LASH_Client_Name); + assert (event); /* Documentation does not say anything about return value. */ + lash_event_set_string(event, jack_get_client_name(jack_client)); + lash_send_event(lash_client, event); + + lash_jack_client_name(lash_client, jack_get_client_name(jack_client)); +#endif + + err = jack_set_process_callback(jack_client, process_callback, 0); + if (err) { + g_critical("Could not register JACK process callback."); + exit(EX_UNAVAILABLE); + } + + err = jack_set_graph_order_callback(jack_client, graph_order_callback, 0); + if (err) { + g_critical("Could not register JACK graph order callback."); + exit(EX_UNAVAILABLE); + } + + output_port = jack_port_register(jack_client, OUTPUT_PORT_NAME, JACK_DEFAULT_MIDI_TYPE, + JackPortIsOutput, 0); + + if (output_port == NULL) { + g_critical("Could not register JACK output port."); + exit(EX_UNAVAILABLE); + } + + input_port = jack_port_register(jack_client, INPUT_PORT_NAME, JACK_DEFAULT_MIDI_TYPE, + JackPortIsInput, 0); + + if (input_port == NULL) { + g_critical("Could not register JACK input port."); + exit(EX_UNAVAILABLE); + } + + if (jack_activate(jack_client)) { + g_critical("Cannot activate JACK client."); + exit(EX_UNAVAILABLE); + } +} + +#ifdef HAVE_LASH + +void +load_config_from_lash(void) +{ + lash_config_t *config; + const char *key; + int value; + + while ((config = lash_get_config(lash_client))) { + + key = lash_config_get_key(config); + value = lash_config_get_value_int(config); + + if (!strcmp(key, "channel")) { + if (value < CHANNEL_MIN || value > CHANNEL_MAX) { + g_warning("Bad value '%d' for 'channel' property received from LASH.", value); + } else { + gtk_spin_button_set_value(GTK_SPIN_BUTTON(channel_spin), value); + } + + } else if (!strcmp(key, "bank")) { + if (value < BANK_MIN || value > BANK_MAX) { + g_warning("Bad value '%d' for 'bank' property received from LASH.", value); + } else { + gtk_spin_button_set_value(GTK_SPIN_BUTTON(bank_spin), value); + } + + } else if (!strcmp(key, "program")) { + if (value < PROGRAM_MIN || value > PROGRAM_MAX) { + g_warning("Bad value '%d' for 'program' property received from LASH.", value); + } else { + gtk_spin_button_set_value(GTK_SPIN_BUTTON(program_spin), value); + } + + } else if (!strcmp(key, "keyboard_grabbed")) { + if (value < 0 || value > 1) { + g_warning("Bad value '%d' for 'keyboard_grabbed' property received from LASH.", value); + } else { + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(grab_keyboard_checkbutton), value); + } + + } else if (!strcmp(key, "octave")) { + if (value < OCTAVE_MIN || value > OCTAVE_MAX) { + g_warning("Bad value '%d' for 'octave' property received from LASH.", value); + } else { + gtk_spin_button_set_value(GTK_SPIN_BUTTON(octave_spin), value); + } + + } else if (!strcmp(key, "velocity_normal")) { + if (value < VELOCITY_MIN || value > VELOCITY_MAX) { + g_warning("Bad value '%d' for 'velocity_normal' property received from LASH.", value); + } else { + velocity_normal = value; + gtk_range_set_value(GTK_RANGE(velocity_hscale), *current_velocity); + } + + } else if (!strcmp(key, "velocity_high")) { + if (value < VELOCITY_MIN || value > VELOCITY_MAX) { + g_warning("Bad value '%d' for 'velocity_high' property received from LASH.", value); + } else { + velocity_high = value; + gtk_range_set_value(GTK_RANGE(velocity_hscale), *current_velocity); + } + + } else if (!strcmp(key, "velocity_low")) { + if (value < VELOCITY_MIN || value > VELOCITY_MAX) { + g_warning("Bad value '%d' for 'velocity_low' property received from LASH.", value); + } else { + velocity_low = value; + gtk_range_set_value(GTK_RANGE(velocity_hscale), *current_velocity); + } + + } else { + g_warning("Received unknown config key '%s' (value '%d') from LASH.", key, value); + } + + lash_config_destroy(config); + } +} + +void +save_config_int(const char *name, int value) +{ + lash_config_t *config; + + config = lash_config_new_with_key(name); + lash_config_set_value_int(config, value); + lash_send_config(lash_client, config); +} + +void +save_config_into_lash(void) +{ + save_config_int("channel", channel + 1); + save_config_int("bank", bank); + save_config_int("program", program); + save_config_int("keyboard_grabbed", keyboard_grabbed); + save_config_int("octave", octave); + save_config_int("velocity_normal", velocity_normal); + save_config_int("velocity_high", velocity_high); + save_config_int("velocity_low", velocity_low); +} + +gboolean +lash_callback(gpointer notused) +{ + lash_event_t *event; + + while ((event = lash_get_event(lash_client))) { + switch (lash_event_get_type(event)) { + case LASH_Restore_Data_Set: + load_config_from_lash(); + lash_send_event(lash_client, event); + + break; + + case LASH_Save_Data_Set: + save_config_into_lash(); + lash_send_event(lash_client, event); + + break; + + case LASH_Quit: + g_warning("Exiting due to LASH request."); + exit(EX_OK); + + break; + + default: + g_warning("Receieved unknown LASH event of type %d.", lash_event_get_type(event)); + lash_event_destroy(event); + } + } + + return TRUE; +} + +void +init_lash(lash_args_t *args) +{ + /* XXX: Am I doing the right thing wrt protocol version? */ + lash_client = lash_init(args, PACKAGE_NAME, LASH_Config_Data_Set, LASH_PROTOCOL(2, 0)); + + if (!lash_server_connected(lash_client)) { + g_critical("Cannot initialize LASH. Continuing anyway."); + /* exit(EX_UNAVAILABLE); */ + + return; + } + + /* Schedule a function to process LASH events, ten times per second. */ + g_timeout_add(100, lash_callback, NULL); +} + +#endif /* HAVE_LASH */ + +gboolean +sustain_event_handler(GtkToggleButton *widget, gpointer pressed) +{ + if (pressed) { + gtk_toggle_button_set_active(widget, TRUE); + piano_keyboard_sustain_press(keyboard); + } else { + gtk_toggle_button_set_active(widget, FALSE); + piano_keyboard_sustain_release(keyboard); + } + + return FALSE; +} + +void +channel_event_handler(GtkSpinButton *spinbutton, gpointer notused) +{ + channel = gtk_spin_button_get_value(spinbutton) - 1; + + draw_window_title(); +} + +void +bank_event_handler(GtkSpinButton *spinbutton, gpointer notused) +{ + bank = gtk_spin_button_get_value(spinbutton); + + send_program_change(); + draw_window_title(); +} + +void +program_event_handler(GtkSpinButton *spinbutton, gpointer notused) +{ + program = gtk_spin_button_get_value(spinbutton); + + send_program_change(); + draw_window_title(); +} + +void +connected_to_event_handler (GtkComboBox *widget, gpointer notused) +{ + GtkTreeIter iter; + gchar *connect_to; + const char **connected_ports; + + if (gtk_combo_box_get_active_iter(GTK_COMBO_BOX(connected_to_combo), &iter) == FALSE) + return; + + gtk_tree_model_get(GTK_TREE_MODEL(connected_to_store), &iter, 0, &connect_to, -1); + + connected_ports = jack_port_get_connections(output_port); + if (connected_ports != NULL && connected_ports[0] != NULL && !strcmp(connect_to, connected_ports[0])) { + free(connected_ports); + return; + } + + connect_to_input_port(connect_to); + + free(connected_ports); +} + +void +velocity_event_handler(GtkRange *range, gpointer notused) +{ + assert(current_velocity); + + *current_velocity = gtk_range_get_value(range); +} + +#ifdef HAVE_X11 + +int grab_x_error_handler(Display *dpy, XErrorEvent *notused) +{ + keyboard_grabbed = 0; + + return 42; /* Returned value is ignored. */ +} + +GdkFilterReturn +keyboard_grab_filter(GdkXEvent *xevent, GdkEvent *event, gpointer notused) +{ + XEvent *xe; + XKeyEvent *xke; + + xe = (XEvent *)xevent; + + if (xe->type != KeyPress && xe->type != KeyRelease) + return GDK_FILTER_CONTINUE; + + xke = (XKeyEvent *)xevent; + + /* Lie to GDK, pretending we are the proper recipient of this XEvent. Without it, + GDK would discard it. */ + xke->window = GDK_WINDOW_XWINDOW(window->window); + + return GDK_FILTER_CONTINUE; +} + +void +ungrab_keyboard(void) +{ + static int (*standard_x_error_handler)(Display *dpy, XErrorEvent *notused); + + standard_x_error_handler = XSetErrorHandler(grab_x_error_handler); + + XUngrabKey(dpy, AnyKey, AnyModifier, GDK_ROOT_WINDOW()); + XSync(dpy, FALSE); + + XSetErrorHandler(standard_x_error_handler); + + keyboard_grabbed = 0; +} + +void +grab_keyboard(void) +{ + int i; + static int (*standard_x_error_handler)(Display *dpy, XErrorEvent *notused); + + KeySym keys_to_grab[] = { + XK_1, XK_2, XK_3, XK_4, XK_5, XK_6, XK_7, XK_8, XK_9, XK_0, XK_minus, XK_equal, + XK_q, XK_w, XK_e, XK_r, XK_t, XK_y, XK_u, XK_i, XK_o, XK_p, + XK_a, XK_s, XK_d, XK_f, XK_g, XK_h, XK_j, XK_k, XK_l, + XK_z, XK_x, XK_c, XK_v, XK_b, XK_n, XK_m, + XK_Shift_L, XK_Shift_R, XK_Control_L, XK_Control_R, XK_space, + XK_Insert, XK_Delete, XK_Home, XK_End, XK_Page_Up, XK_Page_Down, + XK_KP_1, XK_KP_2, XK_KP_3, XK_KP_4, XK_KP_5, XK_KP_6, XK_KP_7, XK_KP_8, XK_KP_9, XK_KP_0, + XK_Num_Lock, XK_KP_Multiply, XK_KP_Divide, XK_KP_Subtract, XK_KP_Add, XK_KP_Enter, + XK_Escape, + + /* End of list. */ + XK_VoidSymbol}; + + if (keyboard_grabbed) + return; + + dpy = GDK_WINDOW_XDISPLAY(window->window); + + keyboard_grabbed = 1; + + standard_x_error_handler = XSetErrorHandler(grab_x_error_handler); + + for (i = 0; keys_to_grab[i] != XK_VoidSymbol; i++) { + KeyCode kc = XKeysymToKeycode(dpy, keys_to_grab[i]); + + if (kc == 0) { + g_critical("XKeysymToKeycode failed. Please report this to the author."); + continue; + } + + XGrabKey(dpy, kc, 0, GDK_ROOT_WINDOW(), True, GrabModeAsync, GrabModeAsync); + XGrabKey(dpy, kc, ShiftMask, GDK_ROOT_WINDOW(), True, GrabModeAsync, GrabModeAsync); + XGrabKey(dpy, kc, ControlMask, GDK_ROOT_WINDOW(), True, GrabModeAsync, GrabModeAsync); + XGrabKey(dpy, kc, ShiftMask | ControlMask, GDK_ROOT_WINDOW(), True, GrabModeAsync, GrabModeAsync); + } + + XSync(dpy, FALSE); + + XSetErrorHandler(standard_x_error_handler); + + if (keyboard_grabbed == 0) { + g_critical("XGrabKey() failed; keyboard grabbing not possible. " + "Maybe some other application grabbed the keyboard?"); + /* Make sure we don't keep any keys grabbed. Only one of XGrabKey invocations failed, others might have been successfull. */ + ungrab_keyboard(); + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(grab_keyboard_checkbutton), keyboard_grabbed); + + return; + } + + g_atexit(ungrab_keyboard); + + gdk_window_add_filter(NULL, keyboard_grab_filter, NULL); +} + +#else /* ! HAVE_X11 */ + +void +ungrab_keyboard(void) +{ +} + +void +grab_keyboard(void) +{ + g_critical("Compiled without XGrabKey support; keyboard grabbing not possible."); +} + +#endif /* ! HAVE_X11 */ + +void +grab_keyboard_handler(GtkToggleButton *togglebutton, gpointer notused) +{ + gboolean active = gtk_toggle_button_get_active(togglebutton); + + if (active) + grab_keyboard(); + else + ungrab_keyboard(); +} + +void +octave_event_handler(GtkSpinButton *spinbutton, gpointer notused) +{ + octave = gtk_spin_button_get_value(spinbutton); + piano_keyboard_set_octave(keyboard, octave); +} + +void +panic(void) +{ + int i; + + /* + * These two have to be sent first, in case we have no room in the + * ringbuffer for all these MIDI_NOTE_OFF messages sent five lines below. + */ + queue_new_message(MIDI_CONTROLLER, MIDI_ALL_NOTES_OFF, 0); + queue_new_message(MIDI_CONTROLLER, MIDI_ALL_SOUND_OFF, 0); + + for (i = 0; i < NNOTES; i++) { + queue_new_message(MIDI_NOTE_OFF, i, 0); + piano_keyboard_set_note_off(keyboard, i); + usleep(100); + } + + queue_new_message(MIDI_CONTROLLER, MIDI_HOLD_PEDAL, 0); + queue_new_message(MIDI_CONTROLLER, MIDI_ALL_MIDI_CONTROLLERS_OFF, 0); + queue_new_message(MIDI_CONTROLLER, MIDI_ALL_NOTES_OFF, 0); + queue_new_message(MIDI_CONTROLLER, MIDI_ALL_SOUND_OFF, 0); + queue_new_message(MIDI_RESET, -1, -1); +} + +void +add_digit(int digit) +{ + if (entered_number == -1) + entered_number = 0; + else + entered_number *= 10; + + entered_number += digit; +} + +int +maybe_add_digit(GdkEventKey *event) +{ + /* + * User can enter a number from the keypad; after that, pressing + * '*'/'/', Page Up/Page Down etc will set program/bank/whatever + * to the number that was entered. + */ + /* + * XXX: This is silly. Find a way to enter the number without + * all these contitional instructions. + */ + if (event->keyval == GDK_KP_0 || event->keyval == GDK_KP_Insert) { + if (event->type == GDK_KEY_PRESS) + add_digit(0); + + return TRUE; + } + + if (event->keyval == GDK_KP_1 || event->keyval == GDK_KP_End) { + if (event->type == GDK_KEY_PRESS) + add_digit(1); + + return TRUE; + } + + if (event->keyval == GDK_KP_2 || event->keyval == GDK_KP_Down) { + if (event->type == GDK_KEY_PRESS) + add_digit(2); + + return TRUE; + } + + if (event->keyval == GDK_KP_3 || event->keyval == GDK_KP_Page_Down) { + if (event->type == GDK_KEY_PRESS) + add_digit(3); + + return TRUE; + } + + if (event->keyval == GDK_KP_4 || event->keyval == GDK_KP_Left) { + if (event->type == GDK_KEY_PRESS) + add_digit(4); + + return TRUE; + } + + if (event->keyval == GDK_KP_5 || event->keyval == GDK_KP_Begin) { + if (event->type == GDK_KEY_PRESS) + add_digit(5); + + return TRUE; + } + + if (event->keyval == GDK_KP_6 || event->keyval == GDK_KP_Right) { + if (event->type == GDK_KEY_PRESS) + add_digit(6); + + return TRUE; + } + + if (event->keyval == GDK_KP_7 || event->keyval == GDK_KP_Home) { + if (event->type == GDK_KEY_PRESS) + add_digit(7); + + return TRUE; + } + + if (event->keyval == GDK_KP_8 || event->keyval == GDK_KP_Up) { + if (event->type == GDK_KEY_PRESS) + add_digit(8); + + return TRUE; + } + + if (event->keyval == GDK_KP_9 || event->keyval == GDK_KP_Page_Up) { + if (event->type == GDK_KEY_PRESS) + add_digit(9); + + return TRUE; + } + + return FALSE; +} + +int +get_entered_number(void) +{ + int tmp; + + tmp = entered_number; + + entered_number = -1; + + return tmp; +} + +int +clip(int val, int lo, int hi) +{ + if (val < lo) + val = lo; + + if (val > hi) + val = hi; + + return val; +} + +gint +keyboard_event_handler(GtkWidget *widget, GdkEventKey *event, gpointer notused) +{ + int tmp; + gboolean retval = FALSE; + + /* Pass signal to piano_keyboard widget. Is there a better way to do this? */ + if (event->type == GDK_KEY_PRESS) + g_signal_emit_by_name(keyboard, "key-press-event", event, &retval); + else + g_signal_emit_by_name(keyboard, "key-release-event", event, &retval); + + if (retval) + return TRUE; + + + if (maybe_add_digit(event)) + return TRUE; + + /* + * '+' key shifts octave up. '-' key shifts octave down. + */ + if (event->keyval == GDK_KP_Add || event->keyval == GDK_equal) { + if (event->type == GDK_KEY_PRESS && octave < OCTAVE_MAX) + gtk_spin_button_set_value(GTK_SPIN_BUTTON(octave_spin), octave + 1); + + return TRUE; + } + + if (event->keyval == GDK_KP_Subtract || event->keyval == GDK_minus) { + if (event->type == GDK_KEY_PRESS && octave > OCTAVE_MIN) + gtk_spin_button_set_value(GTK_SPIN_BUTTON(octave_spin), octave - 1); + + return TRUE; + } + + /* + * '*' character increases program number. '/' character decreases it. + */ + if (event->keyval == GDK_KP_Multiply) { + if (event->type == GDK_KEY_PRESS) { + + tmp = get_entered_number(); + + if (tmp < 0) + tmp = gtk_spin_button_get_value(GTK_SPIN_BUTTON(program_spin)) + 1; + + gtk_spin_button_set_value(GTK_SPIN_BUTTON(program_spin), clip(tmp, PROGRAM_MIN, PROGRAM_MAX)); + } + + return TRUE; + } + + if (event->keyval == GDK_KP_Divide) { + if (event->type == GDK_KEY_PRESS) { + + tmp = get_entered_number(); + + if (tmp < 0) + tmp = gtk_spin_button_get_value(GTK_SPIN_BUTTON(program_spin)) - 1; + + gtk_spin_button_set_value(GTK_SPIN_BUTTON(program_spin), clip(tmp, PROGRAM_MIN, PROGRAM_MAX)); + } + + return TRUE; + } + + /* + * PgUp key increases bank number, PgDown decreases it. + */ + if (event->keyval == GDK_Page_Up) { + if (event->type == GDK_KEY_PRESS) { + + tmp = get_entered_number(); + + if (tmp < 0) + tmp = gtk_spin_button_get_value(GTK_SPIN_BUTTON(bank_spin)) + 1; + + gtk_spin_button_set_value(GTK_SPIN_BUTTON(bank_spin), clip(tmp, BANK_MIN, BANK_MAX)); + } + + return TRUE; + } + + if (event->keyval == GDK_Page_Down) { + if (event->type == GDK_KEY_PRESS) { + + tmp = get_entered_number(); + + if (tmp < 0) + tmp = gtk_spin_button_get_value(GTK_SPIN_BUTTON(bank_spin)) - 1; + + gtk_spin_button_set_value(GTK_SPIN_BUTTON(bank_spin), clip(tmp, BANK_MIN, BANK_MAX)); + } + + return TRUE; + } + + /* + * Home key increases channel number, End decreases it. + */ + if (event->keyval == GDK_Home) { + if (event->type == GDK_KEY_PRESS) { + + tmp = get_entered_number(); + + if (tmp < 0) + tmp = gtk_spin_button_get_value(GTK_SPIN_BUTTON(channel_spin)) + 1; + + gtk_spin_button_set_value(GTK_SPIN_BUTTON(channel_spin), clip(tmp, CHANNEL_MIN, CHANNEL_MAX)); + } + + return TRUE; + } + + if (event->keyval == GDK_End) { + if (event->type == GDK_KEY_PRESS) { + + tmp = get_entered_number(); + + if (tmp < 0) + tmp = gtk_spin_button_get_value(GTK_SPIN_BUTTON(channel_spin)) - 1; + + gtk_spin_button_set_value(GTK_SPIN_BUTTON(channel_spin), clip(tmp, CHANNEL_MIN, CHANNEL_MAX)); + } + + return TRUE; + } + + /* + * Insert key connects to the next input port. Delete connects to the previous one. + */ + if (event->keyval == GDK_Insert) { + if (event->type == GDK_KEY_PRESS) + connect_to_next_input_port(); + + return TRUE; + } + + if (event->keyval == GDK_Delete) { + if (event->type == GDK_KEY_PRESS) + connect_to_prev_input_port(); + + return TRUE; + } + + if (event->keyval == GDK_Escape) { + if (event->type == GDK_KEY_PRESS) + panic(); + + return TRUE; + } + + /* + * Spacebar works as a 'sustain' key. Holding spacebar while + * releasing note will cause the note to continue. Pressing and + * releasing spacebar without pressing any note keys will make all + * the sustained notes end (it will send 'note off' midi messages for + * all the sustained notes). + */ + if (event->keyval == GDK_space) { + if (event->type == GDK_KEY_PRESS) + gtk_button_pressed(GTK_BUTTON(sustain_button)); + else + gtk_button_released(GTK_BUTTON(sustain_button)); + + return TRUE; + } + + /* + * Shift increases velocity, i.e. holding it while pressing note key + * will make the sound louder. Ctrl decreases velocity. + */ + if (event->keyval == GDK_Shift_L || event->keyval == GDK_Shift_R) { + if (event->type == GDK_KEY_PRESS) + current_velocity = &velocity_high; + else + current_velocity = &velocity_normal; + + gtk_range_set_value(GTK_RANGE(velocity_hscale), *current_velocity); + + return TRUE; + + } + + if (event->keyval == GDK_Control_L || event->keyval == GDK_Control_R) { + if (event->type == GDK_KEY_PRESS) + current_velocity = &velocity_low; + else + current_velocity = &velocity_normal; + + gtk_range_set_value(GTK_RANGE(velocity_hscale), *current_velocity); + + return TRUE; + } + + return FALSE; +} + +void +note_on_event_handler(GtkWidget *widget, int note) +{ + assert(current_velocity); + + queue_new_message(MIDI_NOTE_ON, note, *current_velocity); +} + +void +note_off_event_handler(GtkWidget *widget, int note) +{ + assert(current_velocity); + + queue_new_message(MIDI_NOTE_OFF, note, *current_velocity); +} + +void +init_gtk_1(int *argc, char ***argv) +{ + GdkPixbuf *icon = NULL; + GError *error = NULL; + + gtk_init(argc, argv); + + icon = gtk_icon_theme_load_icon(gtk_icon_theme_get_default(), "jack-keyboard", 48, 0, &error); + + if (icon == NULL) { + fprintf(stderr, "%s: Cannot load icon: %s.\n", G_LOG_DOMAIN, error->message); + g_error_free(error); + + } else { + gtk_window_set_default_icon(icon); + } + + /* Window. */ + window = gtk_window_new(GTK_WINDOW_TOPLEVEL); + gtk_container_set_border_width (GTK_CONTAINER (window), 2); +} + +void +init_gtk_2(void) +{ + GtkTable *table; + GtkWidget *label; + GtkCellRenderer *renderer; + + /* Table. */ + table = GTK_TABLE(gtk_table_new(4, 8, FALSE)); + gtk_table_set_row_spacings(table, 5); + gtk_table_set_col_spacings(table, 5); + + if (enable_gui) + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(table)); + + /* Channel label and spin. */ + label = gtk_label_new("Channel:"); + gtk_misc_set_alignment(GTK_MISC(label), 1, 0.5); + gtk_table_attach(table, label, 0, 1, 0, 1, GTK_EXPAND | GTK_FILL, GTK_FILL, 0, 0); + channel_spin = gtk_spin_button_new_with_range(1, CHANNEL_MAX, 1); + GTK_WIDGET_UNSET_FLAGS(channel_spin, GTK_CAN_FOCUS); + gtk_table_attach(table, channel_spin, 1, 2, 0, 1, GTK_EXPAND | GTK_FILL, GTK_FILL, 0, 0); + g_signal_connect(G_OBJECT(channel_spin), "value-changed", G_CALLBACK(channel_event_handler), NULL); + + /* Bank label and spin. */ + label = gtk_label_new("Bank:"); + gtk_misc_set_alignment(GTK_MISC(label), 1, 0.5); + gtk_table_attach(table, label, 2, 3, 0, 1, GTK_EXPAND | GTK_FILL, GTK_FILL, 0, 0); + bank_spin = gtk_spin_button_new_with_range(0, BANK_MAX, 1); + GTK_WIDGET_UNSET_FLAGS(bank_spin, GTK_CAN_FOCUS); + gtk_table_attach(table, bank_spin, 3, 4, 0, 1, GTK_EXPAND | GTK_FILL, GTK_FILL, 0, 0); + g_signal_connect(G_OBJECT(bank_spin), "value-changed", G_CALLBACK(bank_event_handler), NULL); + + /* Program label and spin. */ + label = gtk_label_new("Program:"); + gtk_misc_set_alignment(GTK_MISC(label), 1, 0.5); + gtk_table_attach(table, label, 4, 5, 0, 1, GTK_EXPAND | GTK_FILL, GTK_FILL, 0, 0); + program_spin = gtk_spin_button_new_with_range(0, PROGRAM_MAX, 1); + GTK_WIDGET_UNSET_FLAGS(program_spin, GTK_CAN_FOCUS); + gtk_table_attach(table, program_spin, 5, 6, 0, 1, GTK_EXPAND | GTK_FILL, GTK_FILL, 0, 0); + g_signal_connect(G_OBJECT(program_spin), "value-changed", G_CALLBACK(program_event_handler), NULL); + + /* "Connected to" label and combo box. */ + label = gtk_label_new("Connected to:"); + gtk_misc_set_alignment(GTK_MISC(label), 1, 0.5); + gtk_table_attach(table, label, 6, 7, 0, 1, GTK_EXPAND | GTK_FILL, GTK_FILL, 0, 0); + + connected_to_store = gtk_list_store_new(1, G_TYPE_STRING); + connected_to_combo = gtk_combo_box_new_with_model(GTK_TREE_MODEL(connected_to_store)); + + renderer = gtk_cell_renderer_text_new (); + gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(connected_to_combo), renderer, FALSE); + gtk_cell_layout_set_attributes (GTK_CELL_LAYOUT(connected_to_combo), renderer, "text", 0, NULL); + + GTK_WIDGET_UNSET_FLAGS(connected_to_combo, GTK_CAN_FOCUS); + gtk_combo_box_set_focus_on_click(GTK_COMBO_BOX(connected_to_combo), FALSE); + gtk_table_attach(table, connected_to_combo, 7, 8, 0, 1, GTK_EXPAND | GTK_FILL, GTK_FILL, 0, 0); + gtk_widget_set_size_request(GTK_WIDGET(connected_to_combo), 200, -1); + g_signal_connect(G_OBJECT(connected_to_combo), "changed", G_CALLBACK(connected_to_event_handler), NULL); + + /* Octave label and spin. */ + label = gtk_label_new("Octave:"); + gtk_misc_set_alignment(GTK_MISC(label), 1, 0.5); + gtk_table_attach(table, label, 0, 1, 1, 2, GTK_EXPAND | GTK_FILL, GTK_FILL, 0, 0); + octave_spin = gtk_spin_button_new_with_range(OCTAVE_MIN, OCTAVE_MAX, 1); + GTK_WIDGET_UNSET_FLAGS(octave_spin, GTK_CAN_FOCUS); + gtk_spin_button_set_value(GTK_SPIN_BUTTON(octave_spin), octave); + gtk_table_attach(table, octave_spin, 1, 2, 1, 2, GTK_EXPAND | GTK_FILL, GTK_FILL, 0, 0); + g_signal_connect(G_OBJECT(octave_spin), "value-changed", G_CALLBACK(octave_event_handler), NULL); + + /* "Grab keyboard" label and checkbutton. */ + label = gtk_label_new("Grab keyboard:"); + gtk_misc_set_alignment(GTK_MISC(label), 1, 0.5); + gtk_table_attach(table, label, 4, 5, 1, 2, GTK_EXPAND | GTK_FILL, GTK_FILL, 0, 0); + grab_keyboard_checkbutton = gtk_check_button_new(); + GTK_WIDGET_UNSET_FLAGS(grab_keyboard_checkbutton, GTK_CAN_FOCUS); + g_signal_connect(G_OBJECT(grab_keyboard_checkbutton), "toggled", G_CALLBACK(grab_keyboard_handler), NULL); + gtk_table_attach(table, grab_keyboard_checkbutton, 5, 6, 1, 2, GTK_EXPAND | GTK_FILL, GTK_FILL, 0, 0); + + /* Velocity label and hscale */ + label = gtk_label_new("Velocity:"); + gtk_misc_set_alignment(GTK_MISC(label), 1, 0.5); + gtk_table_attach(table, label, 6, 7, 1, 2, GTK_EXPAND | GTK_FILL, GTK_FILL, 0, 0); + + velocity_hscale = gtk_hscale_new_with_range(VELOCITY_MIN, VELOCITY_MAX, 1); + gtk_scale_set_draw_value(GTK_SCALE(velocity_hscale), FALSE); + GTK_WIDGET_UNSET_FLAGS(velocity_hscale, GTK_CAN_FOCUS); + g_signal_connect(G_OBJECT(velocity_hscale), "value-changed", G_CALLBACK(velocity_event_handler), NULL); + gtk_range_set_value(GTK_RANGE(velocity_hscale), VELOCITY_NORMAL); + gtk_table_attach(table, velocity_hscale, 7, 8, 1, 2, GTK_EXPAND | GTK_FILL, GTK_FILL, 0, 0); + gtk_widget_set_size_request(GTK_WIDGET(velocity_hscale), 200, -1); + + /* Sustain. It's a toggle button, not an ordinary one, because we want gtk_whatever_set_active() to work.*/ + sustain_button = gtk_toggle_button_new_with_label("Sustain"); + gtk_button_set_focus_on_click(GTK_BUTTON(sustain_button), FALSE); + gtk_table_attach(table, sustain_button, 0, 8, 2, 3, GTK_EXPAND | GTK_FILL, GTK_FILL, 0, 0); + g_signal_connect(G_OBJECT(sustain_button), "pressed", G_CALLBACK(sustain_event_handler), (void *)1); + g_signal_connect(G_OBJECT(sustain_button), "released", G_CALLBACK(sustain_event_handler), (void *)0); + + /* PianoKeyboard widget. */ + keyboard = PIANO_KEYBOARD(piano_keyboard_new()); + + if (enable_gui) + gtk_table_attach_defaults(table, GTK_WIDGET(keyboard), 0, 8, 3, 4); + else + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(keyboard)); + + g_signal_connect(G_OBJECT(keyboard), "note-on", G_CALLBACK(note_on_event_handler), NULL); + g_signal_connect(G_OBJECT(keyboard), "note-off", G_CALLBACK(note_off_event_handler), NULL); + g_signal_connect(G_OBJECT(window), "destroy", G_CALLBACK(gtk_main_quit), NULL); + g_signal_connect(G_OBJECT(window), "key-press-event", G_CALLBACK(keyboard_event_handler), NULL); + g_signal_connect(G_OBJECT(window), "key-release-event", G_CALLBACK(keyboard_event_handler), NULL); + gtk_widget_show_all(window); + + draw_window_title(); +} + +void +log_handler(const gchar *log_domain, GLogLevelFlags log_level, const gchar *message, gpointer notused) +{ + GtkWidget *dialog; + + fprintf(stderr, "%s: %s\n", log_domain, message); + + if ((log_level | G_LOG_LEVEL_CRITICAL) == G_LOG_LEVEL_CRITICAL) { + dialog = gtk_message_dialog_new(GTK_WINDOW(window), GTK_DIALOG_DESTROY_WITH_PARENT, GTK_MESSAGE_ERROR, + GTK_BUTTONS_CLOSE, message); + + gtk_dialog_run(GTK_DIALOG(dialog)); + + gtk_widget_destroy(dialog); + } +} + +void +show_version(void) +{ + fprintf(stdout, "%s\n", PACKAGE_STRING); + + exit(EX_OK); +} + +void +usage(void) +{ + fprintf(stderr, "usage: jack-keyboard [-CGKTVktur] [ -a ] [-c ] [-b ] [-p ] [-l ]\n"); + fprintf(stderr, " where is MIDI channel to use for output, from 1 to 16,\n"); + fprintf(stderr, " is MIDI bank to use, from 0 to 16383,\n"); + fprintf(stderr, " is MIDI program to use, from 0 to 127,\n"); + fprintf(stderr, " and is either QWERTY, QWERTZ or AZERTY.\n"); + fprintf(stderr, "See manual page for details.\n"); + + exit(EX_USAGE); +} + +int +main(int argc, char *argv[]) +{ + int ch, enable_keyboard_cue = 0, initial_channel = 1, initial_bank = 0, initial_program = 0; + char *keyboard_layout = NULL, *autoconnect_port_name = NULL; + +#ifdef HAVE_LASH + lash_args_t *lash_args; +#endif + + g_thread_init(NULL); + +#ifdef HAVE_LASH + lash_args = lash_extract_args(&argc, &argv); +#endif + + init_gtk_1(&argc, &argv); + + g_log_set_default_handler(log_handler, NULL); + + while ((ch = getopt(argc, argv, "CGKTVa:nktur:c:b:p:l:")) != -1) { + switch (ch) { + case 'C': + enable_keyboard_cue = 1; + break; + + case 'G': + enable_gui = 0; + enable_window_title = !enable_window_title; + break; + + case 'K': + grab_keyboard_at_startup = 1; + break; + + case 'T': + enable_window_title = !enable_window_title; + break; + + case 'V': + show_version(); + break; + + case 'a': + autoconnect_port_name = strdup(optarg); + break; + + case 'n': + /* Do nothing; backward compatibility. */ + break; + + case 'k': + allow_connecting_to_own_kind = 1; + break; + + case 'l': + keyboard_layout = strdup(optarg); + break; + + case 't': + time_offsets_are_zero = 1; + break; + + case 'u': + send_program_change_at_reconnect = 1; + break; + + case 'c': + initial_channel = atoi(optarg); + + if (initial_channel < CHANNEL_MIN || initial_channel > CHANNEL_MAX) { + g_critical("Invalid MIDI channel number specified on the command line; " + "valid values are %d-%d.", CHANNEL_MIN, CHANNEL_MAX); + + exit(EX_USAGE); + } + + break; + + case 'b': + initial_bank = atoi(optarg); + + send_program_change_once = 1; + + if (initial_bank < BANK_MIN || initial_bank > BANK_MAX) { + g_critical("Invalid MIDI bank number specified on the command line; " + "valid values are %d-%d.", BANK_MIN, BANK_MAX); + + exit(EX_USAGE); + } + + break; + + case 'p': + initial_program = atoi(optarg); + + send_program_change_once = 1; + + if (initial_program < PROGRAM_MIN || initial_program > PROGRAM_MAX) { + g_critical("Invalid MIDI program number specified on the command line; " + "valid values are %d-%d.", PROGRAM_MIN, PROGRAM_MAX); + + exit(EX_USAGE); + } + + break; + + case 'r': + rate_limit = strtod(optarg, NULL); + if (rate_limit <= 0.0) { + g_critical("Invalid rate limit specified.\n"); + + exit(EX_USAGE); + } + + break; + + case '?': + default: + usage(); + } + } + + argc -= optind; + argv += optind; + + init_gtk_2(); + + if (keyboard_layout != NULL) { + int ret = piano_keyboard_set_keyboard_layout(keyboard, keyboard_layout); + + if (ret) { + g_critical("Invalid layout, proper choices are QWERTY, QWERTZ and AZERTY."); + exit(EX_USAGE); + } + } + +#ifdef HAVE_LASH + init_lash(lash_args); +#endif + + init_jack(); + + if (autoconnect_port_name) { + if (connect_to_input_port(autoconnect_port_name)) { + g_critical("Couldn't connect to '%s', exiting.", autoconnect_port_name); + exit(EX_UNAVAILABLE); + } + } + + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(grab_keyboard_checkbutton), grab_keyboard_at_startup); + gtk_spin_button_set_value(GTK_SPIN_BUTTON(channel_spin), initial_channel); + gtk_spin_button_set_value(GTK_SPIN_BUTTON(bank_spin), initial_bank); + gtk_spin_button_set_value(GTK_SPIN_BUTTON(program_spin), initial_program); + piano_keyboard_set_keyboard_cue(keyboard, enable_keyboard_cue); + + gtk_main(); + + return 0; +} + diff --git a/src/jack-keyboard.desktop b/src/jack-keyboard.desktop new file mode 100644 index 0000000..59451a6 --- /dev/null +++ b/src/jack-keyboard.desktop @@ -0,0 +1,8 @@ +[Desktop Entry] +Name=JACK Keyboard +Comment=Virtual keyboard for JACK MIDI +Exec=jack-keyboard +Icon=jack-keyboard.png +Categories=Application;AudioVideo;Audio;Midi; +Terminal=false +Type=Application diff --git a/src/pianokeyboard.c b/src/pianokeyboard.c new file mode 100644 index 0000000..2c0b785 --- /dev/null +++ b/src/pianokeyboard.c @@ -0,0 +1,686 @@ +/*- + * Copyright (c) 2007, 2008 Edward Tomasz Napierała + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +/* + * This is piano_keyboard, piano keyboard-like GTK+ widget. It contains + * no MIDI-specific code. + * + * For questions and comments, contact Edward Tomasz Napierala . + */ + +#include +#include +#include +#include + +#include "pianokeyboard.h" + +#define PIANO_KEYBOARD_DEFAULT_WIDTH 730 +#define PIANO_KEYBOARD_DEFAULT_HEIGHT 70 + +enum { + NOTE_ON_SIGNAL, + NOTE_OFF_SIGNAL, + LAST_SIGNAL +}; + +static guint piano_keyboard_signals[LAST_SIGNAL] = { 0 }; + +static void +draw_keyboard_cue(PianoKeyboard *pk) +{ + int w = pk->notes[0].w; + int h = pk->notes[0].h; + + GdkGC *gc = GTK_WIDGET(pk)->style->fg_gc[0]; + + int first_note_in_lower_row = (pk->octave + 5) * 12; + int last_note_in_lower_row = (pk->octave + 6) * 12 - 1; + int first_note_in_higher_row = (pk->octave + 6) * 12; + int last_note_in_higher_row = (pk->octave + 7) * 12 + 4; + + gdk_draw_line(GTK_WIDGET(pk)->window, gc, pk->notes[first_note_in_lower_row].x + 3, + h - 6, pk->notes[last_note_in_lower_row].x + w - 3, h - 6); + + gdk_draw_line(GTK_WIDGET(pk)->window, gc, pk->notes[first_note_in_higher_row].x + 3, + h - 9, pk->notes[last_note_in_higher_row].x + w - 3, h - 9); +} + +static void +draw_note(PianoKeyboard *pk, int note) +{ + GdkColor black = {0, 0, 0, 0}; + GdkColor white = {0, 65535, 65535, 65535}; + + GdkGC *gc = GTK_WIDGET(pk)->style->fg_gc[0]; + GtkWidget *widget; + + int is_white = pk->notes[note].white; + + int x = pk->notes[note].x; + int w = pk->notes[note].w; + int h = pk->notes[note].h; + + if (pk->notes[note].pressed || pk->notes[note].sustained) + is_white = !is_white; + + if (is_white) + gdk_gc_set_rgb_fg_color(gc, &white); + else + gdk_gc_set_rgb_fg_color(gc, &black); + + gdk_draw_rectangle(GTK_WIDGET(pk)->window, gc, TRUE, x, 0, w, h); + gdk_gc_set_rgb_fg_color(gc, &black); + gdk_draw_rectangle(GTK_WIDGET(pk)->window, gc, FALSE, x, 0, w, h); + + if (pk->enable_keyboard_cue) + draw_keyboard_cue(pk); + + /* We need to redraw black keys that partially obscure the white one. */ + if (note < NNOTES - 2 && !pk->notes[note + 1].white) + draw_note(pk, note + 1); + + if (note > 0 && !pk->notes[note - 1].white) + draw_note(pk, note - 1); + + /* + * XXX: This doesn't really belong here. Originally I wanted to pack PianoKeyboard into GtkFrame + * packed into GtkAlignment. I failed to make it behave the way I want. GtkFrame would need + * to adapt to the "proper" size of PianoKeyboard, i.e. to the useful_width, not allocated width; + * that didn't work. + */ + widget = GTK_WIDGET(pk); + gtk_paint_shadow(widget->style, widget->window, GTK_STATE_NORMAL, GTK_SHADOW_IN, NULL, widget, NULL, pk->widget_margin, 0, + widget->allocation.width - pk->widget_margin * 2 + 1, widget->allocation.height); +} + +static int +press_key(PianoKeyboard *pk, int key) +{ + assert(key >= 0); + assert(key < NNOTES); + + pk->maybe_stop_sustained_notes = 0; + + /* This is for keyboard autorepeat protection. */ + if (pk->notes[key].pressed) + return 0; + + if (pk->sustain_new_notes) + pk->notes[key].sustained = 1; + else + pk->notes[key].sustained = 0; + + pk->notes[key].pressed = 1; + + g_signal_emit_by_name(GTK_WIDGET(pk), "note-on", key); + draw_note(pk, key); + + return 1; +} + +static int +release_key(PianoKeyboard *pk, int key) +{ + assert(key >= 0); + assert(key < NNOTES); + + pk->maybe_stop_sustained_notes = 0; + + if (!pk->notes[key].pressed) + return 0; + + if (pk->sustain_new_notes) + pk->notes[key].sustained = 1; + + pk->notes[key].pressed = 0; + + if (pk->notes[key].sustained) + return 0; + + g_signal_emit_by_name(GTK_WIDGET(pk), "note-off", key); + draw_note(pk, key); + + return 1; +} + +static void +stop_unsustained_notes(PianoKeyboard *pk) +{ + int i; + + for (i = 0; i < NNOTES; i++) { + if (pk->notes[i].pressed && !pk->notes[i].sustained) { + pk->notes[i].pressed = 0; + g_signal_emit_by_name(GTK_WIDGET(pk), "note-off", i); + draw_note(pk, i); + } + } +} + +static void +stop_sustained_notes(PianoKeyboard *pk) +{ + int i; + + for (i = 0; i < NNOTES; i++) { + if (pk->notes[i].sustained) { + pk->notes[i].pressed = 0; + pk->notes[i].sustained = 0; + g_signal_emit_by_name(GTK_WIDGET(pk), "note-off", i); + draw_note(pk, i); + } + } +} + +static int +key_binding(PianoKeyboard *pk, const char *key) +{ + gpointer notused, note; + gboolean found; + + assert(pk->key_bindings != NULL); + + found = g_hash_table_lookup_extended(pk->key_bindings, key, ¬used, ¬e); + + if (!found) + return -1; + + return (int)note; +} + +static void +bind_key(PianoKeyboard *pk, const char *key, int note) +{ + assert(pk->key_bindings != NULL); + + g_hash_table_insert(pk->key_bindings, (gpointer)key, (gpointer)note); +} + +static void +clear_notes(PianoKeyboard *pk) +{ + assert(pk->key_bindings != NULL); + + g_hash_table_remove_all(pk->key_bindings); +} + +static void +bind_keys_qwerty(PianoKeyboard *pk) +{ + clear_notes(pk); + + /* Lower keyboard row - "zxcvbnm". */ + bind_key(pk, "z", 12); /* C0 */ + bind_key(pk, "s", 13); + bind_key(pk, "x", 14); + bind_key(pk, "d", 15); + bind_key(pk, "c", 16); + bind_key(pk, "v", 17); + bind_key(pk, "g", 18); + bind_key(pk, "b", 19); + bind_key(pk, "h", 20); + bind_key(pk, "n", 21); + bind_key(pk, "j", 22); + bind_key(pk, "m", 23); + + /* Upper keyboard row, first octave - "qwertyu". */ + bind_key(pk, "q", 24); + bind_key(pk, "2", 25); + bind_key(pk, "w", 26); + bind_key(pk, "3", 27); + bind_key(pk, "e", 28); + bind_key(pk, "r", 29); + bind_key(pk, "5", 30); + bind_key(pk, "t", 31); + bind_key(pk, "6", 32); + bind_key(pk, "y", 33); + bind_key(pk, "7", 34); + bind_key(pk, "u", 35); + + /* Upper keyboard row, the rest - "iop". */ + bind_key(pk, "i", 36); + bind_key(pk, "9", 37); + bind_key(pk, "o", 38); + bind_key(pk, "0", 39); + bind_key(pk, "p", 40); +} + +static void +bind_keys_qwertz(PianoKeyboard *pk) +{ + bind_keys_qwerty(pk); + + /* The only difference between QWERTY and QWERTZ is that the "y" and "z" are swapped together. */ + bind_key(pk, "y", 12); + bind_key(pk, "z", 33); +} + +static void +bind_keys_azerty(PianoKeyboard *pk) +{ + clear_notes(pk); + + /* Lower keyboard row - "wxcvbn,". */ + bind_key(pk, "w", 12); /* C0 */ + bind_key(pk, "s", 13); + bind_key(pk, "x", 14); + bind_key(pk, "d", 15); + bind_key(pk, "c", 16); + bind_key(pk, "v", 17); + bind_key(pk, "g", 18); + bind_key(pk, "b", 19); + bind_key(pk, "h", 20); + bind_key(pk, "n", 21); + bind_key(pk, "j", 22); + bind_key(pk, "comma", 23); + + /* Upper keyboard row, first octave - "azertyu". */ + bind_key(pk, "a", 24); + bind_key(pk, "eacute", 25); + bind_key(pk, "z", 26); + bind_key(pk, "quotedbl", 27); + bind_key(pk, "e", 28); + bind_key(pk, "r", 29); + bind_key(pk, "parenleft", 30); + bind_key(pk, "t", 31); + bind_key(pk, "minus", 32); + bind_key(pk, "y", 33); + bind_key(pk, "egrave", 34); + bind_key(pk, "u", 35); + + /* Upper keyboard row, the rest - "iop". */ + bind_key(pk, "i", 36); + bind_key(pk, "ccedilla", 37); + bind_key(pk, "o", 38); + bind_key(pk, "agrave", 39); + bind_key(pk, "p", 40); +} + +static gint +keyboard_event_handler(GtkWidget *mk, GdkEventKey *event, gpointer notused) +{ + int note; + char *key; + guint keyval; + GdkKeymapKey kk; + PianoKeyboard *pk = PIANO_KEYBOARD(mk); + + /* We're not using event->keyval, because we need keyval with level set to 0. + E.g. if user holds Shift and presses '7', we want to get a '7', not '&'. */ + kk.keycode = event->hardware_keycode; + kk.level = 0; + kk.group = 0; + + keyval = gdk_keymap_lookup_key(NULL, &kk); + + key = gdk_keyval_name(gdk_keyval_to_lower(keyval)); + + if (key == NULL) { + g_message("gtk_keyval_name() returned NULL; please report this."); + return FALSE; + } + + note = key_binding(pk, key); + + if (note < 0) { + /* Key was not bound. Maybe it's one of the keys handled in jack-keyboard.c. */ + return FALSE; + } + + note += pk->octave * 12; + + assert(note >= 0); + assert(note < NNOTES); + + if (event->type == GDK_KEY_PRESS) { + press_key(pk, note); + + } else if (event->type == GDK_KEY_RELEASE) { + release_key(pk, note); + } + + return TRUE; +} + +static int +get_note_for_xy(PianoKeyboard *pk, int x, int y) +{ + int height = GTK_WIDGET(pk)->allocation.height; + int note; + + if (y <= height / 2) { + for (note = 0; note < NNOTES - 1; note++) { + if (pk->notes[note].white) + continue; + + if (x >= pk->notes[note].x && x <= pk->notes[note].x + pk->notes[note].w) + return note; + } + } + + for (note = 0; note < NNOTES - 1; note++) { + if (!pk->notes[note].white) + continue; + + if (x >= pk->notes[note].x && x <= pk->notes[note].x + pk->notes[note].w) + return note; + } + + return -1; +} + +static gboolean +mouse_button_event_handler(PianoKeyboard *pk, GdkEventButton *event, gpointer notused) +{ + int x = event->x; + int y = event->y; + + int note = get_note_for_xy(pk, x, y); + + if (event->button != 1) + return TRUE; + + if (event->type == GDK_BUTTON_PRESS) { + /* This is possible when you make the window a little wider and then click + on the grey area. */ + if (note < 0) { + return TRUE; + } + + if (pk->note_being_pressed_using_mouse >= 0) + release_key(pk, pk->note_being_pressed_using_mouse); + + press_key(pk, note); + pk->note_being_pressed_using_mouse = note; + + } else if (event->type == GDK_BUTTON_RELEASE) { + if (note >= 0) { + release_key(pk, note); + + } else { + if (pk->note_being_pressed_using_mouse >= 0) + release_key(pk, pk->note_being_pressed_using_mouse); + } + + pk->note_being_pressed_using_mouse = -1; + + } + + return TRUE; +} + +static gboolean +mouse_motion_event_handler(PianoKeyboard *pk, GdkEventMotion *event, gpointer notused) +{ + int note; + + if ((event->state & GDK_BUTTON1_MASK) == 0) + return TRUE; + + note = get_note_for_xy(pk, event->x, event->y); + + if (note != pk->note_being_pressed_using_mouse && note >= 0) { + + if (pk->note_being_pressed_using_mouse >= 0) + release_key(pk, pk->note_being_pressed_using_mouse); + press_key(pk, note); + pk->note_being_pressed_using_mouse = note; + } + + return TRUE; +} + +static gboolean +piano_keyboard_expose(GtkWidget *widget, GdkEventExpose *event) +{ + int i; + PianoKeyboard *pk = PIANO_KEYBOARD(widget); + + for (i = 0; i < NNOTES; i++) + draw_note(pk, i); + + return TRUE; +} + +static void +piano_keyboard_size_request(GtkWidget *widget, GtkRequisition *requisition) +{ + requisition->width = PIANO_KEYBOARD_DEFAULT_WIDTH; + requisition->height = PIANO_KEYBOARD_DEFAULT_HEIGHT; +} + +static void +recompute_dimensions(PianoKeyboard *pk) +{ + int number_of_white_keys = (NNOTES - 1) * (7.0 / 12.0); + + int key_width; + int black_key_width; + int useful_width; + + int note; + int white_key = 0; + int note_in_octave; + + int width = GTK_WIDGET(pk)->allocation.width; + int height = GTK_WIDGET(pk)->allocation.height; + + key_width = width / number_of_white_keys; + black_key_width = key_width * 0.8; + useful_width = number_of_white_keys * key_width; + pk->widget_margin = (width - useful_width) / 2; + + for (note = 0, white_key = 0; note < NNOTES - 2; note++) { + note_in_octave = note % 12; + + if (note_in_octave == 1 || note_in_octave == 3 || note_in_octave == 6 || + note_in_octave == 8 || note_in_octave == 10) { + + /* This note is black key. */ + pk->notes[note].x = pk->widget_margin + white_key * key_width - black_key_width / 2; + pk->notes[note].w = black_key_width; + pk->notes[note].h = height / 2; + pk->notes[note].white = 0; + + continue; + } + + /* This note is white key. */ + pk->notes[note].x = pk->widget_margin + white_key * key_width; + pk->notes[note].w = key_width; + pk->notes[note].h = height; + pk->notes[note].white = 1; + + white_key++; + } +} + +static void +piano_keyboard_size_allocate(GtkWidget *widget, GtkAllocation *allocation) +{ + /* XXX: Are these two needed? */ + g_return_if_fail(widget != NULL); + g_return_if_fail(allocation != NULL); + + widget->allocation = *allocation; + + recompute_dimensions(PIANO_KEYBOARD(widget)); + + if (GTK_WIDGET_REALIZED(widget)) { + gdk_window_move_resize (widget->window, allocation->x, allocation->y, allocation->width, allocation->height); + } +} + +static void +piano_keyboard_class_init(PianoKeyboardClass *klass) +{ + GtkWidgetClass *widget_klass; + + /* Set up signals. */ + piano_keyboard_signals[NOTE_ON_SIGNAL] = g_signal_new ("note-on", + G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION, + 0, NULL, NULL, g_cclosure_marshal_VOID__INT, G_TYPE_NONE, 1, G_TYPE_INT); + + piano_keyboard_signals[NOTE_OFF_SIGNAL] = g_signal_new ("note-off", + G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION, + 0, NULL, NULL, g_cclosure_marshal_VOID__INT, G_TYPE_NONE, 1, G_TYPE_INT); + + widget_klass = (GtkWidgetClass*) klass; + + widget_klass->expose_event = piano_keyboard_expose; + widget_klass->size_request = piano_keyboard_size_request; + widget_klass->size_allocate = piano_keyboard_size_allocate; +} + +static void +piano_keyboard_init(GtkWidget *mk) +{ + gtk_widget_add_events(mk, GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | GDK_POINTER_MOTION_MASK); + + g_signal_connect(G_OBJECT(mk), "button-press-event", G_CALLBACK(mouse_button_event_handler), NULL); + g_signal_connect(G_OBJECT(mk), "button-release-event", G_CALLBACK(mouse_button_event_handler), NULL); + g_signal_connect(G_OBJECT(mk), "motion-notify-event", G_CALLBACK(mouse_motion_event_handler), NULL); + g_signal_connect(G_OBJECT(mk), "key-press-event", G_CALLBACK(keyboard_event_handler), NULL); + g_signal_connect(G_OBJECT(mk), "key-release-event", G_CALLBACK(keyboard_event_handler), NULL); +} + +GType +piano_keyboard_get_type(void) +{ + static GType mk_type = 0; + + if (!mk_type) { + static const GTypeInfo mk_info = { + sizeof(PianoKeyboardClass), + NULL, /* base_init */ + NULL, /* base_finalize */ + (GClassInitFunc) piano_keyboard_class_init, + NULL, /* class_finalize */ + NULL, /* class_data */ + sizeof (PianoKeyboard), + 0, /* n_preallocs */ + (GInstanceInitFunc) piano_keyboard_init, + }; + + mk_type = g_type_register_static(GTK_TYPE_DRAWING_AREA, "PianoKeyboard", &mk_info, 0); + } + + return mk_type; +} + +GtkWidget * +piano_keyboard_new(void) +{ + GtkWidget *widget = gtk_type_new(piano_keyboard_get_type()); + + PianoKeyboard *pk = PIANO_KEYBOARD(widget); + + pk->maybe_stop_sustained_notes = 0; + pk->sustain_new_notes = 0; + pk->enable_keyboard_cue = 0; + pk->octave = 4; + pk->note_being_pressed_using_mouse = -1; + memset((void *)pk->notes, 0, sizeof(struct Note) * NNOTES); + pk->key_bindings = g_hash_table_new(g_str_hash, g_str_equal); + bind_keys_qwerty(pk); + + return widget; +} + +void +piano_keyboard_set_keyboard_cue(PianoKeyboard *pk, int enabled) +{ + pk->enable_keyboard_cue = enabled; +} + +void +piano_keyboard_sustain_press(PianoKeyboard *pk) +{ + if (!pk->sustain_new_notes) { + pk->sustain_new_notes = 1; + pk->maybe_stop_sustained_notes = 1; + } +} + +void +piano_keyboard_sustain_release(PianoKeyboard *pk) +{ + if (pk->maybe_stop_sustained_notes) + stop_sustained_notes(pk); + + pk->sustain_new_notes = 0; +} + +void +piano_keyboard_set_note_on(PianoKeyboard *pk, int note) +{ + if (pk->notes[note].pressed == 0) { + pk->notes[note].pressed = 1; + draw_note(pk, note); + } +} + +void +piano_keyboard_set_note_off(PianoKeyboard *pk, int note) +{ + if (pk->notes[note].pressed || pk->notes[note].sustained) { + pk->notes[note].pressed = 0; + pk->notes[note].sustained = 0; + draw_note(pk, note); + } +} + +void +piano_keyboard_set_octave(PianoKeyboard *pk, int octave) +{ + stop_unsustained_notes(pk); + pk->octave = octave; + gtk_widget_queue_draw(GTK_WIDGET(pk)); +} + +gboolean +piano_keyboard_set_keyboard_layout(PianoKeyboard *pk, const char *layout) +{ + assert(layout); + + if (!strcasecmp(layout, "QWERTY")) { + bind_keys_qwerty(pk); + + } else if (!strcasecmp(layout, "QWERTZ")) { + bind_keys_qwertz(pk); + + } else if (!strcasecmp(layout, "AZERTY")) { + bind_keys_azerty(pk); + + } else { + /* Unknown layout name. */ + return TRUE; + } + + return FALSE; +} + diff --git a/src/pianokeyboard.h b/src/pianokeyboard.h new file mode 100644 index 0000000..27d9a8a --- /dev/null +++ b/src/pianokeyboard.h @@ -0,0 +1,66 @@ +#ifndef __PIANO_KEYBOARD_H__ +#define __PIANO_KEYBOARD_H__ + +#include +#include + +G_BEGIN_DECLS + +#define TYPE_PIANO_KEYBOARD (piano_keyboard_get_type ()) +#define PIANO_KEYBOARD(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), TYPE_PIANO_KEYBOARD, PianoKeyboard)) +#define PIANO_KEYBOARD_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), TYPE_PIANO_KEYBOARD, PianoKeyboardClass)) +#define IS_PIANO_KEYBOARD(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), TYPE_PIANO_KEYBOARD)) +#define IS_PIANO_KEYBOARD_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), TYPE_PIANO_KEYBOARD)) +#define PIANO_KEYBOARD_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), TYPE_PIANO_KEYBOARD, PianoKeyboardClass)) + +typedef struct _PianoKeyboard PianoKeyboard; +typedef struct _PianoKeyboardClass PianoKeyboardClass; + +#define NNOTES 127 + +#define OCTAVE_MIN -1 +#define OCTAVE_MAX 7 + +struct Note { + int pressed; /* 1 if key is in pressed down state. */ + int sustained; /* 1 if note is sustained. */ + int x; /* Distance between the left edge of the key + * and the left edge of the widget, in pixels. */ + int w; /* Width of the key, in pixels. */ + int h; /* Height of the key, in pixels. */ + int white; /* 1 if key is white; 0 otherwise. */ +}; + +struct _PianoKeyboard +{ + GtkDrawingArea da; + int maybe_stop_sustained_notes; + int sustain_new_notes; + int enable_keyboard_cue; + int octave; + int widget_margin; + int note_being_pressed_using_mouse; + volatile struct Note notes[NNOTES]; + /* Table used to translate from PC keyboard character to MIDI note number. */ + GHashTable *key_bindings; +}; + +struct _PianoKeyboardClass +{ + GtkDrawingAreaClass parent_class; +}; + +GType piano_keyboard_get_type (void) G_GNUC_CONST; +GtkWidget* piano_keyboard_new (void); +void piano_keyboard_sustain_press (PianoKeyboard *pk); +void piano_keyboard_sustain_release (PianoKeyboard *pk); +void piano_keyboard_set_note_on (PianoKeyboard *pk, int note); +void piano_keyboard_set_note_off (PianoKeyboard *pk, int note); +void piano_keyboard_set_keyboard_cue (PianoKeyboard *pk, int enabled); +void piano_keyboard_set_octave (PianoKeyboard *pk, int octave); +gboolean piano_keyboard_set_keyboard_layout (PianoKeyboard *pk, const char *layout); + +G_END_DECLS + +#endif /* __PIANO_KEYBOARD_H__ */ +