From 4dcd5450756a36eb61c4ad28fec876b39472e7d0 Mon Sep 17 00:00:00 2001 From: trasz Date: Sat, 11 Oct 2008 22:44:38 +0000 Subject: [PATCH] Import jack-keyboard 2.5. git-svn-id: svn://svn.code.sf.net/p/jack-keyboard/code/trunk@2 1fa2bf75-7d80-4145-9e94-f9b4e25a1cb2 --- AUTHORS | 2 + COPYING | 24 + Makefile.am | 2 + NEWS | 160 ++++ README | 130 +++ TODO | 13 + configure.ac | 81 ++ man/Makefile.am | 4 + man/jack-keyboard.1 | 186 ++++ pixmaps/Makefile.am | 17 + pixmaps/jack-keyboard.png | Bin 0 -> 215 bytes src/Makefile.am | 10 + src/jack-keyboard.c | 1865 +++++++++++++++++++++++++++++++++++++ src/jack-keyboard.desktop | 8 + src/pianokeyboard.c | 686 ++++++++++++++ src/pianokeyboard.h | 66 ++ 16 files changed, 3254 insertions(+) create mode 100644 AUTHORS create mode 100644 COPYING create mode 100644 Makefile.am create mode 100644 NEWS create mode 100644 README create mode 100644 TODO create mode 100644 configure.ac create mode 100644 man/Makefile.am create mode 100644 man/jack-keyboard.1 create mode 100644 pixmaps/Makefile.am create mode 100644 pixmaps/jack-keyboard.png create mode 100644 src/Makefile.am create mode 100644 src/jack-keyboard.c create mode 100644 src/jack-keyboard.desktop create mode 100644 src/pianokeyboard.c create mode 100644 src/pianokeyboard.h 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 0000000000000000000000000000000000000000..9f734135f11e0a9e3832b8d266015e050a277558 GIT binary patch literal 215 zcmeAS@N?(olHy`uVBq!ia0vp^LLkh+1SD^+kpz+qjKx9jP7LeL$-D$|*pj^6T^Rm@ z;DWu&Cj&(|3p^r=85p>QL70(Y)*K0-AbW|YuPgfvMm9l1(K**LCIE#bOI#yLobz*Y zQ}ap~oQqNuOHxx5$}>wc6x=<11Hv2m#DR*8JY5_^G|ngg`TyUZ84Q$_m6?Sax(}yL wbe$n_f@$%swq<5wTT&XQNx3-}O~_(q&`{!6SmOVo8>pMX)78&qol`;+0P^xWxc~qF literal 0 HcmV?d00001 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__ */ +