changeset 0:3580ffe87dc8

First commit of TouchKeys public pre-release.
author Andrew McPherson <andrewm@eecs.qmul.ac.uk>
date Mon, 11 Nov 2013 18:19:35 +0000
parents
children 6a67be435132
files Builds/Linux/Makefile Builds/Linux32/Makefile Builds/MacOSX/TouchKeys.xcodeproj/project.pbxproj Builds/MacOSX/TouchKeys.xcodeproj/project.xcworkspace/contents.xcworkspacedata Builds/MacOSX/TouchKeys.xcodeproj/project.xcworkspace/xcuserdata/apm.xcuserdatad/UserInterfaceState.xcuserstate Builds/MacOSX/TouchKeys.xcodeproj/project.xcworkspace/xcuserdata/apm.xcuserdatad/WorkspaceSettings.xcsettings Builds/MacOSX/TouchKeys.xcodeproj/xcuserdata/apm.xcuserdatad/xcdebugger/Breakpoints.xcbkptlist Builds/MacOSX/TouchKeys.xcodeproj/xcuserdata/apm.xcuserdatad/xcschemes/TouchKeys.xcscheme Builds/MacOSX/TouchKeys.xcodeproj/xcuserdata/apm.xcuserdatad/xcschemes/xcschememanagement.plist Resources/tk-icon-128.png Resources/tk-icon-256.png Resources/tk-icon-512.png Source/Display/KeyPositionGraphDisplay.cpp Source/Display/KeyPositionGraphDisplay.h Source/Display/KeyboardDisplay.cpp Source/Display/KeyboardDisplay.h Source/Display/OpenGLDisplayBase.h Source/Display/OpenGLJuceCanvas.h Source/Display/RawSensorDisplay.cpp Source/Display/RawSensorDisplay.h Source/GUI/ControlWindowMainComponent.cpp Source/GUI/ControlWindowMainComponent.h Source/GUI/GraphicsDisplayWindow.h Source/GUI/KeyboardZoneComponent.cpp Source/GUI/KeyboardZoneComponent.h Source/GUI/MainWindow.cpp Source/GUI/MainWindow.h Source/GUI/MappingEditorComponent.h Source/GUI/MappingListComponent.cpp Source/GUI/MappingListComponent.h Source/GUI/MappingListItem.cpp Source/GUI/MappingListItem.h Source/Main.cpp Source/MainApplicationController.cpp Source/MainApplicationController.h Source/Mappings/Control/TouchkeyControlMapping.cpp Source/Mappings/Control/TouchkeyControlMapping.h Source/Mappings/Control/TouchkeyControlMappingFactory.cpp Source/Mappings/Control/TouchkeyControlMappingFactory.h Source/Mappings/Control/TouchkeyControlMappingShortEditor.cpp Source/Mappings/Control/TouchkeyControlMappingShortEditor.h Source/Mappings/KeyDivision/TouchkeyKeyDivisionMapping.cpp Source/Mappings/KeyDivision/TouchkeyKeyDivisionMapping.h Source/Mappings/KeyDivision/TouchkeyKeyDivisionMappingFactory.cpp Source/Mappings/KeyDivision/TouchkeyKeyDivisionMappingFactory.h Source/Mappings/MIDIKeyPositionMapping.cpp Source/Mappings/MIDIKeyPositionMapping.h Source/Mappings/MRPMapping.cpp Source/Mappings/MRPMapping.h Source/Mappings/Mapping.cpp Source/Mappings/Mapping.h Source/Mappings/MappingFactory.h Source/Mappings/MappingFactorySplitter.cpp Source/Mappings/MappingFactorySplitter.h Source/Mappings/MappingScheduler.cpp Source/Mappings/MappingScheduler.h Source/Mappings/MultiFingerTrigger/TouchkeyMultiFingerTriggerMapping.cpp Source/Mappings/MultiFingerTrigger/TouchkeyMultiFingerTriggerMapping.h Source/Mappings/MultiFingerTrigger/TouchkeyMultiFingerTriggerMappingFactory.cpp Source/Mappings/MultiFingerTrigger/TouchkeyMultiFingerTriggerMappingFactory.h Source/Mappings/OnsetAngle/TouchkeyOnsetAngleMapping.cpp Source/Mappings/OnsetAngle/TouchkeyOnsetAngleMapping.h Source/Mappings/OnsetAngle/TouchkeyOnsetAngleMappingFactory.cpp Source/Mappings/OnsetAngle/TouchkeyOnsetAngleMappingFactory.h Source/Mappings/PitchBend/TouchkeyPitchBendMapping.cpp Source/Mappings/PitchBend/TouchkeyPitchBendMapping.h Source/Mappings/PitchBend/TouchkeyPitchBendMappingFactory.cpp Source/Mappings/PitchBend/TouchkeyPitchBendMappingFactory.h Source/Mappings/PitchBend/TouchkeyPitchBendMappingShortEditor.cpp Source/Mappings/PitchBend/TouchkeyPitchBendMappingShortEditor.h Source/Mappings/ReleaseAngle/TouchkeyReleaseAngleMapping.cpp Source/Mappings/ReleaseAngle/TouchkeyReleaseAngleMapping.h Source/Mappings/ReleaseAngle/TouchkeyReleaseAngleMappingFactory.cpp Source/Mappings/ReleaseAngle/TouchkeyReleaseAngleMappingFactory.h Source/Mappings/TouchkeyBaseMapping.cpp Source/Mappings/TouchkeyBaseMapping.h Source/Mappings/TouchkeyBaseMappingFactory.h Source/Mappings/Vibrato/TouchkeyVibratoMapping.cpp Source/Mappings/Vibrato/TouchkeyVibratoMapping.h Source/Mappings/Vibrato/TouchkeyVibratoMappingFactory.cpp Source/Mappings/Vibrato/TouchkeyVibratoMappingFactory.h Source/Mappings/Vibrato/TouchkeyVibratoMappingShortEditor.cpp Source/Mappings/Vibrato/TouchkeyVibratoMappingShortEditor.h Source/TouchKeys/KeyIdleDetector.cpp Source/TouchKeys/KeyIdleDetector.h Source/TouchKeys/KeyPositionTracker.cpp Source/TouchKeys/KeyPositionTracker.h Source/TouchKeys/KeyTouchFrame.h Source/TouchKeys/LogPlayback.cpp Source/TouchKeys/LogPlayback.h Source/TouchKeys/MidiInputController.cpp Source/TouchKeys/MidiInputController.h Source/TouchKeys/MidiKeyboardSegment.cpp Source/TouchKeys/MidiKeyboardSegment.h Source/TouchKeys/MidiOutputController.cpp Source/TouchKeys/MidiOutputController.h Source/TouchKeys/Osc.cpp Source/TouchKeys/Osc.h Source/TouchKeys/OscMidiConverter.cpp Source/TouchKeys/OscMidiConverter.h Source/TouchKeys/PianoKey.cpp Source/TouchKeys/PianoKey.h Source/TouchKeys/PianoKeyCalibrator.cpp Source/TouchKeys/PianoKeyCalibrator.h Source/TouchKeys/PianoKeyboard.cpp Source/TouchKeys/PianoKeyboard.h Source/TouchKeys/PianoPedal.cpp Source/TouchKeys/PianoPedal.h Source/TouchKeys/PianoTypes.h Source/TouchKeys/TouchkeyDevice.cpp Source/TouchKeys/TouchkeyDevice.h Source/Utility/Accumulator.h Source/Utility/IIRFilter.cpp Source/Utility/IIRFilter.h Source/Utility/LineSegment.h Source/Utility/Node.h Source/Utility/Scheduler.cpp Source/Utility/Scheduler.h Source/Utility/TimerNode.cpp Source/Utility/TimerNode.h Source/Utility/TimestampSynchronizer.cpp Source/Utility/TimestampSynchronizer.h Source/Utility/Trigger.cpp Source/Utility/Trigger.h Source/Utility/Types.h
diffstat 125 files changed, 31157 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Builds/Linux/Makefile	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,444 @@
+# Automatically generated makefile, created by the Introjucer
+# Don't edit this file! Your changes will be overwritten when you re-save the Introjucer project!
+
+# (this disables dependency generation if multiple architectures are set)
+DEPFLAGS := $(if $(word 2, $(TARGET_ARCH)), , -MMD)
+
+ifndef CONFIG
+  CONFIG=Debug
+endif
+
+ifeq ($(CONFIG),Debug)
+  BINDIR := build
+  LIBDIR := build
+  OBJDIR := build/intermediate/Debug
+  OUTDIR := build
+
+  ifeq ($(TARGET_ARCH),)
+    TARGET_ARCH := -march=native
+  endif
+
+  CPPFLAGS := $(DEPFLAGS) -D "LINUX=1" -D "DEBUG=1" -D "_DEBUG=1" -D "JUCER_LINUX_MAKE_7346DA2A=1" -I /usr/include -I /usr/include/freetype2 -I ../../JuceLibraryCode -I ../../../juce/modules
+  CFLAGS += $(CPPFLAGS) $(TARGET_ARCH) -g -ggdb -O0
+  CXXFLAGS += $(CFLAGS)
+  LDFLAGS += $(TARGET_ARCH) -L$(BINDIR) -L$(LIBDIR) -L/usr/X11R6/lib/ -lGL -lX11 -lXext -lXinerama -lasound -ldl -lfreetype -lpthread -lrt /usr/local/lib/liblo.a
+  LDDEPS :=
+  RESFLAGS :=  -D "LINUX=1" -D "DEBUG=1" -D "_DEBUG=1" -D "JUCER_LINUX_MAKE_7346DA2A=1" -I /usr/include -I /usr/include/freetype2 -I ../../JuceLibraryCode -I ../../../juce/modules
+  TARGET := TouchKeys
+  BLDCMD = $(CXX) -o $(OUTDIR)/$(TARGET) $(OBJECTS) $(LDFLAGS) $(RESOURCES) $(TARGET_ARCH)
+endif
+
+ifeq ($(CONFIG),Release)
+  BINDIR := build
+  LIBDIR := build
+  OBJDIR := build/intermediate/Release
+  OUTDIR := build
+
+  ifeq ($(TARGET_ARCH),)
+    TARGET_ARCH := -march=native
+  endif
+
+  CPPFLAGS := $(DEPFLAGS) -D "LINUX=1" -D "NDEBUG=1" -D "JUCER_LINUX_MAKE_7346DA2A=1" -I /usr/include -I /usr/include/freetype2 -I ../../JuceLibraryCode -I ../../../juce/modules
+  CFLAGS += $(CPPFLAGS) $(TARGET_ARCH) -Os
+  CXXFLAGS += $(CFLAGS)
+  LDFLAGS += $(TARGET_ARCH) -L$(BINDIR) -L$(LIBDIR) -fvisibility=hidden -L/usr/X11R6/lib/ -lGL -lX11 -lXext -lXinerama -lasound -ldl -lfreetype -lpthread -lrt /usr/local/lib/liblo.a
+  LDDEPS :=
+  RESFLAGS :=  -D "LINUX=1" -D "NDEBUG=1" -D "JUCER_LINUX_MAKE_7346DA2A=1" -I /usr/include -I /usr/include/freetype2 -I ../../JuceLibraryCode -I ../../../juce/modules
+  TARGET := TouchKeys
+  BLDCMD = $(CXX) -o $(OUTDIR)/$(TARGET) $(OBJECTS) $(LDFLAGS) $(RESOURCES) $(TARGET_ARCH)
+endif
+
+OBJECTS := \
+  $(OBJDIR)/MainWindow_ca618186.o \
+  $(OBJDIR)/KeyboardZoneComponent_fd0d7a77.o \
+  $(OBJDIR)/ControlWindowMainComponent_c67f9014.o \
+  $(OBJDIR)/MappingListComponent_402cf84e.o \
+  $(OBJDIR)/MappingListItem_7a9eebac.o \
+  $(OBJDIR)/TouchkeyVibratoMappingShortEditor_27ad15dd.o \
+  $(OBJDIR)/TouchkeyVibratoMapping_ea5c5156.o \
+  $(OBJDIR)/TouchkeyVibratoMappingFactory_f90040de.o \
+  $(OBJDIR)/TouchkeyReleaseAngleMapping_170b0b0a.o \
+  $(OBJDIR)/TouchkeyReleaseAngleMappingFactory_9052f4aa.o \
+  $(OBJDIR)/TouchkeyPitchBendMappingShortEditor_6afc649d.o \
+  $(OBJDIR)/TouchkeyPitchBendMapping_78aba96.o \
+  $(OBJDIR)/TouchkeyPitchBendMappingFactory_9fc4ef9e.o \
+  $(OBJDIR)/TouchkeyOnsetAngleMapping_a77ca3ca.o \
+  $(OBJDIR)/TouchkeyOnsetAngleMappingFactory_6a4803ea.o \
+  $(OBJDIR)/TouchkeyMultiFingerTriggerMapping_f7bfe8a.o \
+  $(OBJDIR)/TouchkeyMultiFingerTriggerMappingFactory_e811112a.o \
+  $(OBJDIR)/TouchkeyKeyDivisionMapping_cea38eb0.o \
+  $(OBJDIR)/TouchkeyKeyDivisionMappingFactory_33b42a44.o \
+  $(OBJDIR)/TouchkeyControlMappingShortEditor_993f27a5.o \
+  $(OBJDIR)/TouchkeyControlMapping_1e638c8e.o \
+  $(OBJDIR)/TouchkeyControlMappingFactory_1db276a6.o \
+  $(OBJDIR)/MappingScheduler_3b3284f8.o \
+  $(OBJDIR)/TouchkeyBaseMapping_d96a411c.o \
+  $(OBJDIR)/Mapping_57653e8d.o \
+  $(OBJDIR)/MappingFactorySplitter_9525552e.o \
+  $(OBJDIR)/MIDIKeyPositionMapping_750093d2.o \
+  $(OBJDIR)/MRPMapping_742529ce.o \
+  $(OBJDIR)/KeyboardDisplay_d9f334cf.o \
+  $(OBJDIR)/KeyPositionGraphDisplay_5606e2d0.o \
+  $(OBJDIR)/RawSensorDisplay_f15a0e36.o \
+  $(OBJDIR)/IIRFilter_c9874248.o \
+  $(OBJDIR)/Scheduler_5d9eed19.o \
+  $(OBJDIR)/TimerNode_477a3545.o \
+  $(OBJDIR)/TimestampSynchronizer_df7831b6.o \
+  $(OBJDIR)/Trigger_66a771d6.o \
+  $(OBJDIR)/MidiKeyboardSegment_e1be9d70.o \
+  $(OBJDIR)/KeyIdleDetector_2efa023e.o \
+  $(OBJDIR)/KeyPositionTracker_4c4c0dd5.o \
+  $(OBJDIR)/LogPlayback_1debad04.o \
+  $(OBJDIR)/MidiInputController_1864322a.o \
+  $(OBJDIR)/MidiOutputController_a68d4623.o \
+  $(OBJDIR)/Osc_433b9284.o \
+  $(OBJDIR)/OscMidiConverter_75608f25.o \
+  $(OBJDIR)/PianoKey_1afca9bb.o \
+  $(OBJDIR)/PianoKeyboard_f097ab55.o \
+  $(OBJDIR)/PianoKeyCalibrator_6932659c.o \
+  $(OBJDIR)/PianoPedal_34544d96.o \
+  $(OBJDIR)/TouchkeyDevice_be3b7a9b.o \
+  $(OBJDIR)/MainApplicationController_90b344b4.o \
+  $(OBJDIR)/Main_90ebc5c2.o \
+  $(OBJDIR)/BinaryData_ce4232d4.o \
+  $(OBJDIR)/juce_audio_basics_5460bccb.o \
+  $(OBJDIR)/juce_audio_devices_44d00ac9.o \
+  $(OBJDIR)/juce_audio_formats_73515469.o \
+  $(OBJDIR)/juce_core_5f5a3bad.o \
+  $(OBJDIR)/juce_data_structures_7e517fa9.o \
+  $(OBJDIR)/juce_events_88dd09a1.o \
+  $(OBJDIR)/juce_graphics_29b23085.o \
+  $(OBJDIR)/juce_gui_basics_a9efcd81.o \
+  $(OBJDIR)/juce_gui_extra_a9396b49.o \
+  $(OBJDIR)/juce_opengl_492531cd.o \
+
+.PHONY: clean
+
+$(OUTDIR)/$(TARGET): $(OBJECTS) $(LDDEPS) $(RESOURCES)
+	@echo Linking TouchKeys
+	-@mkdir -p $(BINDIR)
+	-@mkdir -p $(LIBDIR)
+	-@mkdir -p $(OUTDIR)
+	@$(BLDCMD)
+
+clean:
+	@echo Cleaning TouchKeys
+	-@rm -f $(OUTDIR)/$(TARGET)
+	-@rm -rf $(OBJDIR)/*
+	-@rm -rf $(OBJDIR)
+
+strip:
+	@echo Stripping TouchKeys
+	-@strip --strip-unneeded $(OUTDIR)/$(TARGET)
+
+$(OBJDIR)/MainWindow_ca618186.o: ../../Source/GUI/MainWindow.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling MainWindow.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/KeyboardZoneComponent_fd0d7a77.o: ../../Source/GUI/KeyboardZoneComponent.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling KeyboardZoneComponent.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/ControlWindowMainComponent_c67f9014.o: ../../Source/GUI/ControlWindowMainComponent.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling ControlWindowMainComponent.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/MappingListComponent_402cf84e.o: ../../Source/GUI/MappingListComponent.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling MappingListComponent.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/MappingListItem_7a9eebac.o: ../../Source/GUI/MappingListItem.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling MappingListItem.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/TouchkeyVibratoMappingShortEditor_27ad15dd.o: ../../Source/Mappings/Vibrato/TouchkeyVibratoMappingShortEditor.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling TouchkeyVibratoMappingShortEditor.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/TouchkeyVibratoMapping_ea5c5156.o: ../../Source/Mappings/Vibrato/TouchkeyVibratoMapping.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling TouchkeyVibratoMapping.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/TouchkeyVibratoMappingFactory_f90040de.o: ../../Source/Mappings/Vibrato/TouchkeyVibratoMappingFactory.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling TouchkeyVibratoMappingFactory.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/TouchkeyReleaseAngleMapping_170b0b0a.o: ../../Source/Mappings/ReleaseAngle/TouchkeyReleaseAngleMapping.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling TouchkeyReleaseAngleMapping.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/TouchkeyReleaseAngleMappingFactory_9052f4aa.o: ../../Source/Mappings/ReleaseAngle/TouchkeyReleaseAngleMappingFactory.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling TouchkeyReleaseAngleMappingFactory.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/TouchkeyPitchBendMappingShortEditor_6afc649d.o: ../../Source/Mappings/PitchBend/TouchkeyPitchBendMappingShortEditor.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling TouchkeyPitchBendMappingShortEditor.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/TouchkeyPitchBendMapping_78aba96.o: ../../Source/Mappings/PitchBend/TouchkeyPitchBendMapping.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling TouchkeyPitchBendMapping.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/TouchkeyPitchBendMappingFactory_9fc4ef9e.o: ../../Source/Mappings/PitchBend/TouchkeyPitchBendMappingFactory.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling TouchkeyPitchBendMappingFactory.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/TouchkeyOnsetAngleMapping_a77ca3ca.o: ../../Source/Mappings/OnsetAngle/TouchkeyOnsetAngleMapping.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling TouchkeyOnsetAngleMapping.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/TouchkeyOnsetAngleMappingFactory_6a4803ea.o: ../../Source/Mappings/OnsetAngle/TouchkeyOnsetAngleMappingFactory.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling TouchkeyOnsetAngleMappingFactory.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/TouchkeyMultiFingerTriggerMapping_f7bfe8a.o: ../../Source/Mappings/MultiFingerTrigger/TouchkeyMultiFingerTriggerMapping.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling TouchkeyMultiFingerTriggerMapping.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/TouchkeyMultiFingerTriggerMappingFactory_e811112a.o: ../../Source/Mappings/MultiFingerTrigger/TouchkeyMultiFingerTriggerMappingFactory.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling TouchkeyMultiFingerTriggerMappingFactory.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/TouchkeyKeyDivisionMapping_cea38eb0.o: ../../Source/Mappings/KeyDivision/TouchkeyKeyDivisionMapping.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling TouchkeyKeyDivisionMapping.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/TouchkeyKeyDivisionMappingFactory_33b42a44.o: ../../Source/Mappings/KeyDivision/TouchkeyKeyDivisionMappingFactory.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling TouchkeyKeyDivisionMappingFactory.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/TouchkeyControlMappingShortEditor_993f27a5.o: ../../Source/Mappings/Control/TouchkeyControlMappingShortEditor.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling TouchkeyControlMappingShortEditor.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/TouchkeyControlMapping_1e638c8e.o: ../../Source/Mappings/Control/TouchkeyControlMapping.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling TouchkeyControlMapping.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/TouchkeyControlMappingFactory_1db276a6.o: ../../Source/Mappings/Control/TouchkeyControlMappingFactory.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling TouchkeyControlMappingFactory.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/MappingScheduler_3b3284f8.o: ../../Source/Mappings/MappingScheduler.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling MappingScheduler.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/TouchkeyBaseMapping_d96a411c.o: ../../Source/Mappings/TouchkeyBaseMapping.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling TouchkeyBaseMapping.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/Mapping_57653e8d.o: ../../Source/Mappings/Mapping.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling Mapping.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/MappingFactorySplitter_9525552e.o: ../../Source/Mappings/MappingFactorySplitter.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling MappingFactorySplitter.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/MIDIKeyPositionMapping_750093d2.o: ../../Source/Mappings/MIDIKeyPositionMapping.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling MIDIKeyPositionMapping.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/MRPMapping_742529ce.o: ../../Source/Mappings/MRPMapping.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling MRPMapping.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/KeyboardDisplay_d9f334cf.o: ../../Source/Display/KeyboardDisplay.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling KeyboardDisplay.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/KeyPositionGraphDisplay_5606e2d0.o: ../../Source/Display/KeyPositionGraphDisplay.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling KeyPositionGraphDisplay.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/RawSensorDisplay_f15a0e36.o: ../../Source/Display/RawSensorDisplay.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling RawSensorDisplay.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/IIRFilter_c9874248.o: ../../Source/Utility/IIRFilter.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling IIRFilter.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/Scheduler_5d9eed19.o: ../../Source/Utility/Scheduler.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling Scheduler.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/TimerNode_477a3545.o: ../../Source/Utility/TimerNode.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling TimerNode.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/TimestampSynchronizer_df7831b6.o: ../../Source/Utility/TimestampSynchronizer.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling TimestampSynchronizer.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/Trigger_66a771d6.o: ../../Source/Utility/Trigger.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling Trigger.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/MidiKeyboardSegment_e1be9d70.o: ../../Source/TouchKeys/MidiKeyboardSegment.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling MidiKeyboardSegment.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/KeyIdleDetector_2efa023e.o: ../../Source/TouchKeys/KeyIdleDetector.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling KeyIdleDetector.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/KeyPositionTracker_4c4c0dd5.o: ../../Source/TouchKeys/KeyPositionTracker.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling KeyPositionTracker.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/LogPlayback_1debad04.o: ../../Source/TouchKeys/LogPlayback.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling LogPlayback.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/MidiInputController_1864322a.o: ../../Source/TouchKeys/MidiInputController.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling MidiInputController.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/MidiOutputController_a68d4623.o: ../../Source/TouchKeys/MidiOutputController.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling MidiOutputController.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/Osc_433b9284.o: ../../Source/TouchKeys/Osc.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling Osc.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/OscMidiConverter_75608f25.o: ../../Source/TouchKeys/OscMidiConverter.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling OscMidiConverter.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/PianoKey_1afca9bb.o: ../../Source/TouchKeys/PianoKey.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling PianoKey.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/PianoKeyboard_f097ab55.o: ../../Source/TouchKeys/PianoKeyboard.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling PianoKeyboard.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/PianoKeyCalibrator_6932659c.o: ../../Source/TouchKeys/PianoKeyCalibrator.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling PianoKeyCalibrator.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/PianoPedal_34544d96.o: ../../Source/TouchKeys/PianoPedal.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling PianoPedal.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/TouchkeyDevice_be3b7a9b.o: ../../Source/TouchKeys/TouchkeyDevice.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling TouchkeyDevice.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/MainApplicationController_90b344b4.o: ../../Source/MainApplicationController.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling MainApplicationController.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/Main_90ebc5c2.o: ../../Source/Main.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling Main.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/BinaryData_ce4232d4.o: ../../JuceLibraryCode/BinaryData.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling BinaryData.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/juce_audio_basics_5460bccb.o: ../../../juce/modules/juce_audio_basics/juce_audio_basics.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling juce_audio_basics.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/juce_audio_devices_44d00ac9.o: ../../../juce/modules/juce_audio_devices/juce_audio_devices.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling juce_audio_devices.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/juce_audio_formats_73515469.o: ../../../juce/modules/juce_audio_formats/juce_audio_formats.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling juce_audio_formats.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/juce_core_5f5a3bad.o: ../../../juce/modules/juce_core/juce_core.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling juce_core.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/juce_data_structures_7e517fa9.o: ../../../juce/modules/juce_data_structures/juce_data_structures.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling juce_data_structures.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/juce_events_88dd09a1.o: ../../../juce/modules/juce_events/juce_events.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling juce_events.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/juce_graphics_29b23085.o: ../../../juce/modules/juce_graphics/juce_graphics.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling juce_graphics.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/juce_gui_basics_a9efcd81.o: ../../../juce/modules/juce_gui_basics/juce_gui_basics.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling juce_gui_basics.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/juce_gui_extra_a9396b49.o: ../../../juce/modules/juce_gui_extra/juce_gui_extra.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling juce_gui_extra.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/juce_opengl_492531cd.o: ../../../juce/modules/juce_opengl/juce_opengl.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling juce_opengl.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+-include $(OBJECTS:%.o=%.d)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Builds/Linux32/Makefile	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,444 @@
+# Automatically generated makefile, created by the Introjucer
+# Don't edit this file! Your changes will be overwritten when you re-save the Introjucer project!
+
+# (this disables dependency generation if multiple architectures are set)
+DEPFLAGS := $(if $(word 2, $(TARGET_ARCH)), , -MMD)
+
+ifndef CONFIG
+  CONFIG=Debug
+endif
+
+ifeq ($(CONFIG),Debug)
+  BINDIR := build
+  LIBDIR := build
+  OBJDIR := build/intermediate/Debug
+  OUTDIR := build
+
+  ifeq ($(TARGET_ARCH),)
+    TARGET_ARCH := -march=native
+  endif
+
+  CPPFLAGS := $(DEPFLAGS) -D "LINUX=1" -D "DEBUG=1" -D "_DEBUG=1" -D "JUCER_LINUX_MAKE_BCF8FE09=1" -I /usr/include -I /usr/include/freetype2 -I ../../JuceLibraryCode -I ../../../juce/modules
+  CFLAGS += $(CPPFLAGS) $(TARGET_ARCH) -g -ggdb -O0 -m32
+  CXXFLAGS += $(CFLAGS)
+  LDFLAGS += $(TARGET_ARCH) -L$(BINDIR) -L$(LIBDIR) -L/usr/X11R6/lib/ -lGL -lX11 -lXext -lXinerama -lasound -ldl -lfreetype -lpthread -lrt -L/usr/lib/i386-linux-gnu -L/usr/lib/i386-linux-gnu/mesa -m32 /usr/local/lib/liblo32.a
+  LDDEPS :=
+  RESFLAGS :=  -D "LINUX=1" -D "DEBUG=1" -D "_DEBUG=1" -D "JUCER_LINUX_MAKE_BCF8FE09=1" -I /usr/include -I /usr/include/freetype2 -I ../../JuceLibraryCode -I ../../../juce/modules
+  TARGET := TouchKeys
+  BLDCMD = $(CXX) -o $(OUTDIR)/$(TARGET) $(OBJECTS) $(LDFLAGS) $(RESOURCES) $(TARGET_ARCH)
+endif
+
+ifeq ($(CONFIG),Release)
+  BINDIR := build
+  LIBDIR := build
+  OBJDIR := build/intermediate/Release
+  OUTDIR := build
+
+  ifeq ($(TARGET_ARCH),)
+    TARGET_ARCH := -march=native
+  endif
+
+  CPPFLAGS := $(DEPFLAGS) -D "LINUX=1" -D "NDEBUG=1" -D "JUCER_LINUX_MAKE_BCF8FE09=1" -I /usr/include -I /usr/include/freetype2 -I ../../JuceLibraryCode -I ../../../juce/modules
+  CFLAGS += $(CPPFLAGS) $(TARGET_ARCH) -Os -m32
+  CXXFLAGS += $(CFLAGS)
+  LDFLAGS += $(TARGET_ARCH) -L$(BINDIR) -L$(LIBDIR) -fvisibility=hidden -L/usr/X11R6/lib/ -lGL -lX11 -lXext -lXinerama -lasound -ldl -lfreetype -lpthread -lrt -L/usr/lib/i386-linux-gnu -L/usr/lib/i386-linux-gnu/mesa -m32 /usr/local/lib/liblo32.a
+  LDDEPS :=
+  RESFLAGS :=  -D "LINUX=1" -D "NDEBUG=1" -D "JUCER_LINUX_MAKE_BCF8FE09=1" -I /usr/include -I /usr/include/freetype2 -I ../../JuceLibraryCode -I ../../../juce/modules
+  TARGET := TouchKeys
+  BLDCMD = $(CXX) -o $(OUTDIR)/$(TARGET) $(OBJECTS) $(LDFLAGS) $(RESOURCES) $(TARGET_ARCH)
+endif
+
+OBJECTS := \
+  $(OBJDIR)/MainWindow_ca618186.o \
+  $(OBJDIR)/KeyboardZoneComponent_fd0d7a77.o \
+  $(OBJDIR)/ControlWindowMainComponent_c67f9014.o \
+  $(OBJDIR)/MappingListComponent_402cf84e.o \
+  $(OBJDIR)/MappingListItem_7a9eebac.o \
+  $(OBJDIR)/TouchkeyVibratoMappingShortEditor_27ad15dd.o \
+  $(OBJDIR)/TouchkeyVibratoMapping_ea5c5156.o \
+  $(OBJDIR)/TouchkeyVibratoMappingFactory_f90040de.o \
+  $(OBJDIR)/TouchkeyReleaseAngleMapping_170b0b0a.o \
+  $(OBJDIR)/TouchkeyReleaseAngleMappingFactory_9052f4aa.o \
+  $(OBJDIR)/TouchkeyPitchBendMappingShortEditor_6afc649d.o \
+  $(OBJDIR)/TouchkeyPitchBendMapping_78aba96.o \
+  $(OBJDIR)/TouchkeyPitchBendMappingFactory_9fc4ef9e.o \
+  $(OBJDIR)/TouchkeyOnsetAngleMapping_a77ca3ca.o \
+  $(OBJDIR)/TouchkeyOnsetAngleMappingFactory_6a4803ea.o \
+  $(OBJDIR)/TouchkeyMultiFingerTriggerMapping_f7bfe8a.o \
+  $(OBJDIR)/TouchkeyMultiFingerTriggerMappingFactory_e811112a.o \
+  $(OBJDIR)/TouchkeyKeyDivisionMapping_cea38eb0.o \
+  $(OBJDIR)/TouchkeyKeyDivisionMappingFactory_33b42a44.o \
+  $(OBJDIR)/TouchkeyControlMappingShortEditor_993f27a5.o \
+  $(OBJDIR)/TouchkeyControlMapping_1e638c8e.o \
+  $(OBJDIR)/TouchkeyControlMappingFactory_1db276a6.o \
+  $(OBJDIR)/MappingScheduler_3b3284f8.o \
+  $(OBJDIR)/TouchkeyBaseMapping_d96a411c.o \
+  $(OBJDIR)/Mapping_57653e8d.o \
+  $(OBJDIR)/MappingFactorySplitter_9525552e.o \
+  $(OBJDIR)/MIDIKeyPositionMapping_750093d2.o \
+  $(OBJDIR)/MRPMapping_742529ce.o \
+  $(OBJDIR)/KeyboardDisplay_d9f334cf.o \
+  $(OBJDIR)/KeyPositionGraphDisplay_5606e2d0.o \
+  $(OBJDIR)/RawSensorDisplay_f15a0e36.o \
+  $(OBJDIR)/IIRFilter_c9874248.o \
+  $(OBJDIR)/Scheduler_5d9eed19.o \
+  $(OBJDIR)/TimerNode_477a3545.o \
+  $(OBJDIR)/TimestampSynchronizer_df7831b6.o \
+  $(OBJDIR)/Trigger_66a771d6.o \
+  $(OBJDIR)/MidiKeyboardSegment_e1be9d70.o \
+  $(OBJDIR)/KeyIdleDetector_2efa023e.o \
+  $(OBJDIR)/KeyPositionTracker_4c4c0dd5.o \
+  $(OBJDIR)/LogPlayback_1debad04.o \
+  $(OBJDIR)/MidiInputController_1864322a.o \
+  $(OBJDIR)/MidiOutputController_a68d4623.o \
+  $(OBJDIR)/Osc_433b9284.o \
+  $(OBJDIR)/OscMidiConverter_75608f25.o \
+  $(OBJDIR)/PianoKey_1afca9bb.o \
+  $(OBJDIR)/PianoKeyboard_f097ab55.o \
+  $(OBJDIR)/PianoKeyCalibrator_6932659c.o \
+  $(OBJDIR)/PianoPedal_34544d96.o \
+  $(OBJDIR)/TouchkeyDevice_be3b7a9b.o \
+  $(OBJDIR)/MainApplicationController_90b344b4.o \
+  $(OBJDIR)/Main_90ebc5c2.o \
+  $(OBJDIR)/BinaryData_ce4232d4.o \
+  $(OBJDIR)/juce_audio_basics_5460bccb.o \
+  $(OBJDIR)/juce_audio_devices_44d00ac9.o \
+  $(OBJDIR)/juce_audio_formats_73515469.o \
+  $(OBJDIR)/juce_core_5f5a3bad.o \
+  $(OBJDIR)/juce_data_structures_7e517fa9.o \
+  $(OBJDIR)/juce_events_88dd09a1.o \
+  $(OBJDIR)/juce_graphics_29b23085.o \
+  $(OBJDIR)/juce_gui_basics_a9efcd81.o \
+  $(OBJDIR)/juce_gui_extra_a9396b49.o \
+  $(OBJDIR)/juce_opengl_492531cd.o \
+
+.PHONY: clean
+
+$(OUTDIR)/$(TARGET): $(OBJECTS) $(LDDEPS) $(RESOURCES)
+	@echo Linking TouchKeys
+	-@mkdir -p $(BINDIR)
+	-@mkdir -p $(LIBDIR)
+	-@mkdir -p $(OUTDIR)
+	@$(BLDCMD)
+
+clean:
+	@echo Cleaning TouchKeys
+	-@rm -f $(OUTDIR)/$(TARGET)
+	-@rm -rf $(OBJDIR)/*
+	-@rm -rf $(OBJDIR)
+
+strip:
+	@echo Stripping TouchKeys
+	-@strip --strip-unneeded $(OUTDIR)/$(TARGET)
+
+$(OBJDIR)/MainWindow_ca618186.o: ../../Source/GUI/MainWindow.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling MainWindow.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/KeyboardZoneComponent_fd0d7a77.o: ../../Source/GUI/KeyboardZoneComponent.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling KeyboardZoneComponent.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/ControlWindowMainComponent_c67f9014.o: ../../Source/GUI/ControlWindowMainComponent.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling ControlWindowMainComponent.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/MappingListComponent_402cf84e.o: ../../Source/GUI/MappingListComponent.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling MappingListComponent.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/MappingListItem_7a9eebac.o: ../../Source/GUI/MappingListItem.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling MappingListItem.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/TouchkeyVibratoMappingShortEditor_27ad15dd.o: ../../Source/Mappings/Vibrato/TouchkeyVibratoMappingShortEditor.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling TouchkeyVibratoMappingShortEditor.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/TouchkeyVibratoMapping_ea5c5156.o: ../../Source/Mappings/Vibrato/TouchkeyVibratoMapping.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling TouchkeyVibratoMapping.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/TouchkeyVibratoMappingFactory_f90040de.o: ../../Source/Mappings/Vibrato/TouchkeyVibratoMappingFactory.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling TouchkeyVibratoMappingFactory.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/TouchkeyReleaseAngleMapping_170b0b0a.o: ../../Source/Mappings/ReleaseAngle/TouchkeyReleaseAngleMapping.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling TouchkeyReleaseAngleMapping.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/TouchkeyReleaseAngleMappingFactory_9052f4aa.o: ../../Source/Mappings/ReleaseAngle/TouchkeyReleaseAngleMappingFactory.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling TouchkeyReleaseAngleMappingFactory.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/TouchkeyPitchBendMappingShortEditor_6afc649d.o: ../../Source/Mappings/PitchBend/TouchkeyPitchBendMappingShortEditor.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling TouchkeyPitchBendMappingShortEditor.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/TouchkeyPitchBendMapping_78aba96.o: ../../Source/Mappings/PitchBend/TouchkeyPitchBendMapping.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling TouchkeyPitchBendMapping.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/TouchkeyPitchBendMappingFactory_9fc4ef9e.o: ../../Source/Mappings/PitchBend/TouchkeyPitchBendMappingFactory.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling TouchkeyPitchBendMappingFactory.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/TouchkeyOnsetAngleMapping_a77ca3ca.o: ../../Source/Mappings/OnsetAngle/TouchkeyOnsetAngleMapping.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling TouchkeyOnsetAngleMapping.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/TouchkeyOnsetAngleMappingFactory_6a4803ea.o: ../../Source/Mappings/OnsetAngle/TouchkeyOnsetAngleMappingFactory.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling TouchkeyOnsetAngleMappingFactory.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/TouchkeyMultiFingerTriggerMapping_f7bfe8a.o: ../../Source/Mappings/MultiFingerTrigger/TouchkeyMultiFingerTriggerMapping.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling TouchkeyMultiFingerTriggerMapping.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/TouchkeyMultiFingerTriggerMappingFactory_e811112a.o: ../../Source/Mappings/MultiFingerTrigger/TouchkeyMultiFingerTriggerMappingFactory.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling TouchkeyMultiFingerTriggerMappingFactory.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/TouchkeyKeyDivisionMapping_cea38eb0.o: ../../Source/Mappings/KeyDivision/TouchkeyKeyDivisionMapping.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling TouchkeyKeyDivisionMapping.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/TouchkeyKeyDivisionMappingFactory_33b42a44.o: ../../Source/Mappings/KeyDivision/TouchkeyKeyDivisionMappingFactory.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling TouchkeyKeyDivisionMappingFactory.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/TouchkeyControlMappingShortEditor_993f27a5.o: ../../Source/Mappings/Control/TouchkeyControlMappingShortEditor.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling TouchkeyControlMappingShortEditor.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/TouchkeyControlMapping_1e638c8e.o: ../../Source/Mappings/Control/TouchkeyControlMapping.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling TouchkeyControlMapping.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/TouchkeyControlMappingFactory_1db276a6.o: ../../Source/Mappings/Control/TouchkeyControlMappingFactory.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling TouchkeyControlMappingFactory.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/MappingScheduler_3b3284f8.o: ../../Source/Mappings/MappingScheduler.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling MappingScheduler.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/TouchkeyBaseMapping_d96a411c.o: ../../Source/Mappings/TouchkeyBaseMapping.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling TouchkeyBaseMapping.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/Mapping_57653e8d.o: ../../Source/Mappings/Mapping.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling Mapping.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/MappingFactorySplitter_9525552e.o: ../../Source/Mappings/MappingFactorySplitter.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling MappingFactorySplitter.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/MIDIKeyPositionMapping_750093d2.o: ../../Source/Mappings/MIDIKeyPositionMapping.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling MIDIKeyPositionMapping.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/MRPMapping_742529ce.o: ../../Source/Mappings/MRPMapping.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling MRPMapping.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/KeyboardDisplay_d9f334cf.o: ../../Source/Display/KeyboardDisplay.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling KeyboardDisplay.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/KeyPositionGraphDisplay_5606e2d0.o: ../../Source/Display/KeyPositionGraphDisplay.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling KeyPositionGraphDisplay.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/RawSensorDisplay_f15a0e36.o: ../../Source/Display/RawSensorDisplay.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling RawSensorDisplay.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/IIRFilter_c9874248.o: ../../Source/Utility/IIRFilter.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling IIRFilter.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/Scheduler_5d9eed19.o: ../../Source/Utility/Scheduler.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling Scheduler.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/TimerNode_477a3545.o: ../../Source/Utility/TimerNode.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling TimerNode.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/TimestampSynchronizer_df7831b6.o: ../../Source/Utility/TimestampSynchronizer.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling TimestampSynchronizer.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/Trigger_66a771d6.o: ../../Source/Utility/Trigger.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling Trigger.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/MidiKeyboardSegment_e1be9d70.o: ../../Source/TouchKeys/MidiKeyboardSegment.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling MidiKeyboardSegment.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/KeyIdleDetector_2efa023e.o: ../../Source/TouchKeys/KeyIdleDetector.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling KeyIdleDetector.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/KeyPositionTracker_4c4c0dd5.o: ../../Source/TouchKeys/KeyPositionTracker.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling KeyPositionTracker.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/LogPlayback_1debad04.o: ../../Source/TouchKeys/LogPlayback.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling LogPlayback.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/MidiInputController_1864322a.o: ../../Source/TouchKeys/MidiInputController.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling MidiInputController.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/MidiOutputController_a68d4623.o: ../../Source/TouchKeys/MidiOutputController.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling MidiOutputController.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/Osc_433b9284.o: ../../Source/TouchKeys/Osc.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling Osc.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/OscMidiConverter_75608f25.o: ../../Source/TouchKeys/OscMidiConverter.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling OscMidiConverter.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/PianoKey_1afca9bb.o: ../../Source/TouchKeys/PianoKey.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling PianoKey.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/PianoKeyboard_f097ab55.o: ../../Source/TouchKeys/PianoKeyboard.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling PianoKeyboard.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/PianoKeyCalibrator_6932659c.o: ../../Source/TouchKeys/PianoKeyCalibrator.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling PianoKeyCalibrator.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/PianoPedal_34544d96.o: ../../Source/TouchKeys/PianoPedal.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling PianoPedal.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/TouchkeyDevice_be3b7a9b.o: ../../Source/TouchKeys/TouchkeyDevice.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling TouchkeyDevice.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/MainApplicationController_90b344b4.o: ../../Source/MainApplicationController.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling MainApplicationController.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/Main_90ebc5c2.o: ../../Source/Main.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling Main.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/BinaryData_ce4232d4.o: ../../JuceLibraryCode/BinaryData.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling BinaryData.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/juce_audio_basics_5460bccb.o: ../../../juce/modules/juce_audio_basics/juce_audio_basics.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling juce_audio_basics.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/juce_audio_devices_44d00ac9.o: ../../../juce/modules/juce_audio_devices/juce_audio_devices.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling juce_audio_devices.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/juce_audio_formats_73515469.o: ../../../juce/modules/juce_audio_formats/juce_audio_formats.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling juce_audio_formats.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/juce_core_5f5a3bad.o: ../../../juce/modules/juce_core/juce_core.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling juce_core.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/juce_data_structures_7e517fa9.o: ../../../juce/modules/juce_data_structures/juce_data_structures.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling juce_data_structures.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/juce_events_88dd09a1.o: ../../../juce/modules/juce_events/juce_events.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling juce_events.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/juce_graphics_29b23085.o: ../../../juce/modules/juce_graphics/juce_graphics.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling juce_graphics.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/juce_gui_basics_a9efcd81.o: ../../../juce/modules/juce_gui_basics/juce_gui_basics.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling juce_gui_basics.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/juce_gui_extra_a9396b49.o: ../../../juce/modules/juce_gui_extra/juce_gui_extra.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling juce_gui_extra.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+$(OBJDIR)/juce_opengl_492531cd.o: ../../../juce/modules/juce_opengl/juce_opengl.cpp
+	-@mkdir -p $(OBJDIR)
+	@echo "Compiling juce_opengl.cpp"
+	@$(CXX) $(CXXFLAGS) -o "$@" -c "$<"
+
+-include $(OBJECTS:%.o=%.d)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Builds/MacOSX/TouchKeys.xcodeproj/project.pbxproj	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,2271 @@
+// !$*UTF8*$!
+{
+	archiveVersion = 1;
+	classes = {
+	};
+	objectVersion = 46;
+	objects = {
+
+		4078BC0343B4DFB6F9816BD5 = { isa = PBXBuildFile; fileRef = 1A9B74287470FD1944123E26; };
+		40646EF394D495C462E347F1 = { isa = PBXBuildFile; fileRef = 7964EC4FD4E5860CF85469A6; };
+		63154C3F49640855CA89CB9B = { isa = PBXBuildFile; fileRef = DF870F58DC21D8A032AE4D03; };
+		2985C4121F644A826FC287AE = { isa = PBXBuildFile; fileRef = 5BC3C1BA534425DE943BCA22; };
+		1A30D1B8526CD3E1AB79110C = { isa = PBXBuildFile; fileRef = 9DEC36437E061C38E07BAC75; };
+		30C3A2842A3EA665887A7CC8 = { isa = PBXBuildFile; fileRef = 20BA5BC9BB93D1041D8F4C73; };
+		FA58715E881C0982BF8E2FCD = { isa = PBXBuildFile; fileRef = 3DFDF8135971D71B8889E84B; };
+		853FB568630B7E920E30E0EE = { isa = PBXBuildFile; fileRef = 6606A45FBF92643F83F78021; };
+		C3F1C6DDE2A344D657712C75 = { isa = PBXBuildFile; fileRef = 7BBF33364D3B65730CEAD5F1; };
+		BADB457195977038A2BF09A4 = { isa = PBXBuildFile; fileRef = F07FDD832AD269D84A40DAF1; };
+		D05E8CCFCD6C2065EDE16DD6 = { isa = PBXBuildFile; fileRef = 5EDDEE17AD20B0C75DF6DF12; };
+		C5AF559E110329B9AB35DFCB = { isa = PBXBuildFile; fileRef = 283FF67DF916C041CE17E244; };
+		2B9C3DDF5283C5EFF1BC924A = { isa = PBXBuildFile; fileRef = 55010ADB4310C9FF2CD11D52; };
+		B8EE628D560C9E474792128A = { isa = PBXBuildFile; fileRef = 7B9FCDB57204606F4A7FDAD4; };
+		1A85E080026A7A0DF0521F27 = { isa = PBXBuildFile; fileRef = 0C9554C2C80A409B9486F101; };
+		CD4F6761CDABB836FBADCD80 = { isa = PBXBuildFile; fileRef = E9E267650C0230141C461A4B; };
+		2378CAD03E1C80BF0AB4AAF5 = { isa = PBXBuildFile; fileRef = BA2C6578AE3F6F2B82656B4F; };
+		998FC7F9C3E8817D8C617391 = { isa = PBXBuildFile; fileRef = 01A3C0D5D0FDDA38CE02C685; };
+		BA527B7D0D87CA5A51EF0D17 = { isa = PBXBuildFile; fileRef = 119E49FA10A84C0AED360605; };
+		B3470DC81675B3D490C27CFD = { isa = PBXBuildFile; fileRef = DE6A8EF9DD39C0260ADD27B0; };
+		5F6E53AD9FA709E7146A1CE6 = { isa = PBXBuildFile; fileRef = 001418B9F0352A2205CDAA96; };
+		BE6B15C227B2372ACE438EC8 = { isa = PBXBuildFile; fileRef = 5DE5675862714BFEFF231027; };
+		13F86C7E26FBFDC47423240D = { isa = PBXBuildFile; fileRef = 91AADDB641BD3F80E9011C65; };
+		0F57CE11332869C573488442 = { isa = PBXBuildFile; fileRef = 25875D01B837C34F01EF8C2F; };
+		0B15BCE918EEF2CCA0AEA482 = { isa = PBXBuildFile; fileRef = F89BC7CFD2C4BC07D31DFA04; };
+		C07B7D53DED5507FB71A2686 = { isa = PBXBuildFile; fileRef = 77D10D2C57D62DF18D3CE862; };
+		310713516FB26036F129F9B4 = { isa = PBXBuildFile; fileRef = F63EB3D2F75EC36B2FBFCDBA; };
+		8FBDE042A025832E21E6F8F4 = { isa = PBXBuildFile; fileRef = F0B30E3238044CF573DFF44D; };
+		BB430A3EF5BE9214B1032DDD = { isa = PBXBuildFile; fileRef = 0A767ED26F1C6854875AC0DB; };
+		3BF49ACD7F6CF44414FB5922 = { isa = PBXBuildFile; fileRef = 879BA74FC54118852648722F; };
+		222381A07D9F4638272B89EF = { isa = PBXBuildFile; fileRef = 77BA61409E24E072AF1E5493; };
+		12510AB6876158F044CAAD7E = { isa = PBXBuildFile; fileRef = 56EF3900C63C00BED0E574D0; };
+		82DFEC1C0E1F1717ECFF46EF = { isa = PBXBuildFile; fileRef = FEB3A41EEF9A3958E3FD1BDB; };
+		2C4FDCA1E6A76506B05D37DD = { isa = PBXBuildFile; fileRef = 0D2D03F562749971564F7773; };
+		6EA3790C69AED29FC35AED08 = { isa = PBXBuildFile; fileRef = 8ADF834CD28E353B15D200C7; };
+		3EDCB8DC01EBCD339722C0B2 = { isa = PBXBuildFile; fileRef = AF8EFA9540E8757E68922E40; };
+		562759D28DA0813B92BD7FEA = { isa = PBXBuildFile; fileRef = F55D37A9CFDBAA406A95E743; };
+		3BCBF5B38F6056D9FE683B0E = { isa = PBXBuildFile; fileRef = A6B7D7B713F01F521411F2D3; };
+		0178872E496D3D0EAC27AE7E = { isa = PBXBuildFile; fileRef = 7A002562A60140BEE1434ECF; };
+		9A83928E33C71ADCAE448871 = { isa = PBXBuildFile; fileRef = 61A7ED9BEB8538FEE656E782; };
+		8B6C8D046A64E8F9A7A8A8A1 = { isa = PBXBuildFile; fileRef = C2762F0E696E257A507595A5; };
+		04F763266CBDE1710ECDFBD1 = { isa = PBXBuildFile; fileRef = C2D442FDEECC11C9BD433379; };
+		CAC2A323D111092F0ACA800D = { isa = PBXBuildFile; fileRef = 3FDF3206CD46EA7C0063B295; };
+		0719C742CAB513BC7D9133DE = { isa = PBXBuildFile; fileRef = 849B3D266CC18D432434AA9B; };
+		0A1BCC62A1753D24D01A3DAD = { isa = PBXBuildFile; fileRef = 8776329610ED3DF98A95ECD2; };
+		4CF799D17D4295DD3F50720D = { isa = PBXBuildFile; fileRef = DBDB2CBDB7DD3D7B9713D4C5; };
+		0209777D29C1CF3F89FBF254 = { isa = PBXBuildFile; fileRef = 32302BE7297F75C489B19CED; };
+		FF44DE892280A28F67C7BFDD = { isa = PBXBuildFile; fileRef = 9D627B675B5F55FD5A203346; };
+		274E9F984B46BBDBF714003B = { isa = PBXBuildFile; fileRef = 4BFF669E2B855A7A576E8CE2; };
+		BA8E15B815FCC3C7830C972E = { isa = PBXBuildFile; fileRef = 68A9E6D3AA5B1E80308A5400; };
+		1AEFD23290811D94FB840E74 = { isa = PBXBuildFile; fileRef = 05A3090F3BD2DAA8D6DF24B6; };
+		5E0BF5CD6F5667E5A0C37651 = { isa = PBXBuildFile; fileRef = B7497C02BCD8C01280DA32B2; };
+		64F134ABEC30EA26D5473D34 = { isa = PBXBuildFile; fileRef = 5A7A1022A19D6DD162029AA6; };
+		53E5A8D73743B36758DA4DA4 = { isa = PBXBuildFile; fileRef = E38A23A31D7FB8E86851666B; };
+		FF8B3EA68E9255C839ABA3F8 = { isa = PBXBuildFile; fileRef = FD7BF71F32457EFD941519E0; };
+		384EC6340C198680B166A0A6 = { isa = PBXBuildFile; fileRef = F0F457D2A8E7EC3DE1CADC28; };
+		BA1577A5D5903CDC1BCFAB06 = { isa = PBXBuildFile; fileRef = 178A386F6FE9CCEAD2ACEA3A; };
+		B98E92A0AAE513E18217810A = { isa = PBXBuildFile; fileRef = 183D81D9E023859A3E499243; };
+		A526339B267B82C838D28D09 = { isa = PBXBuildFile; fileRef = 9C0B692CB270EFA9DB7FA4D7; };
+		5A0669E021464C9CF05C2B95 = { isa = PBXBuildFile; fileRef = 83AE3F5E0B7AE5D956415BCC; };
+		1FDA7AEB064670D6A6B1E5A9 = { isa = PBXBuildFile; fileRef = 67B1EA88315E60BDDEF78A9B; };
+		E1B3A08EBE789ABADC0C025E = { isa = PBXBuildFile; fileRef = 76824FE677CBBE997E6B95AC; };
+		9261B00A2C5E7187B5C04319 = { isa = PBXBuildFile; fileRef = 26646EAE80CBDF47DADEE278; };
+		2FB5C71C709DD3E25C6CC541 = { isa = PBXBuildFile; fileRef = 638AC9A213FFAFC1647D3C78; };
+		B74FCF91BC16623F02F69D86 = { isa = PBXBuildFile; fileRef = E5F79D13171E0F28729529DD; };
+		8394B72883ACCC5CB2A13768 = { isa = PBXBuildFile; fileRef = FBD0A4CCB4013E8B8AA653E2; };
+		5492D1307B92C3F63B9EDF09 = { isa = PBXBuildFile; fileRef = BA641413905F4396597FDF78; };
+		E810879EB266795004A4C22F = { isa = PBXBuildFile; fileRef = 23E81037EDC6EBA8A4145A66; };
+		F68DD438981A3BA730A626CB = { isa = PBXBuildFile; fileRef = 75A33066D1965DA1A60D41B0; };
+		C4A41EFBBFFC08EC2CC75C92 = { isa = PBXBuildFile; fileRef = 5D2E40E321F1A2340FB42A05; };
+		C7C650DB080B12CD2552EC3B = { isa = PBXBuildFile; fileRef = 5FBE2E2D25174AF84CAB065C; };
+		F44235BA2064C306B810CE62 = { isa = PBXBuildFile; fileRef = 91157413471588AA9BBA75D7; };
+		80917C6EBEAD3F8DF0C99FD3 = { isa = PBXBuildFile; fileRef = 007AEF97B8A9D76A0A2D83FC; };
+		52944F2FAC4CEBFB17FB29E4 = { isa = PBXBuildFile; fileRef = 76566BC742E2FB1F6FDC34F5; };
+		D88B64FB4066D913682D5BA3 = { isa = PBXBuildFile; fileRef = 8CA21E32C250B212F23EA8AF; };
+		001418B9F0352A2205CDAA96 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = TouchkeyVibratoMappingFactory.cpp; path = ../../Source/Mappings/Vibrato/TouchkeyVibratoMappingFactory.cpp; sourceTree = "SOURCE_ROOT"; };
+		006F973FB5A7E243D7293AA6 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_Justification.h"; path = "../../../juce/modules/juce_graphics/placement/juce_Justification.h"; sourceTree = "SOURCE_ROOT"; };
+		007AEF97B8A9D76A0A2D83FC = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; name = "juce_gui_basics.mm"; path = "../../../juce/modules/juce_gui_basics/juce_gui_basics.mm"; sourceTree = "SOURCE_ROOT"; };
+		012940D53AFFB34E7515BA0D = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_Colours.h"; path = "../../../juce/modules/juce_graphics/colour/juce_Colours.h"; sourceTree = "SOURCE_ROOT"; };
+		018F96DE10ED9ABDBBE82873 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_ToolbarButton.h"; path = "../../../juce/modules/juce_gui_basics/buttons/juce_ToolbarButton.h"; sourceTree = "SOURCE_ROOT"; };
+		01A3C0D5D0FDDA38CE02C685 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = MappingListItem.cpp; path = ../../Source/GUI/MappingListItem.cpp; sourceTree = "SOURCE_ROOT"; };
+		021AF0A3F7822EA031EE86A1 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_NamedValueSet.h"; path = "../../../juce/modules/juce_core/containers/juce_NamedValueSet.h"; sourceTree = "SOURCE_ROOT"; };
+		021EAA7FB2AF6D2F19996A6D = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_MultiTouchMapper.h"; path = "../../../juce/modules/juce_gui_basics/native/juce_MultiTouchMapper.h"; sourceTree = "SOURCE_ROOT"; };
+		022C3AEC95B6F5C85673E1B9 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_LocalisedStrings.cpp"; path = "../../../juce/modules/juce_core/text/juce_LocalisedStrings.cpp"; sourceTree = "SOURCE_ROOT"; };
+		0241560E0F43D8F64C26A397 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_AudioSource.h"; path = "../../../juce/modules/juce_audio_basics/sources/juce_AudioSource.h"; sourceTree = "SOURCE_ROOT"; };
+		026C6BCB121C0BB163DE9F06 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_NamedValueSet.cpp"; path = "../../../juce/modules/juce_core/containers/juce_NamedValueSet.cpp"; sourceTree = "SOURCE_ROOT"; };
+		02AFF71371B8781BB9914E14 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_MidiFile.cpp"; path = "../../../juce/modules/juce_audio_basics/midi/juce_MidiFile.cpp"; sourceTree = "SOURCE_ROOT"; };
+		02F82B7A43C1B002A55C8645 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_Identifier.cpp"; path = "../../../juce/modules/juce_core/text/juce_Identifier.cpp"; sourceTree = "SOURCE_ROOT"; };
+		035E2239C42EE699D96A082C = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_linux_Files.cpp"; path = "../../../juce/modules/juce_core/native/juce_linux_Files.cpp"; sourceTree = "SOURCE_ROOT"; };
+		0390578A9F961786A2E91266 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_ComponentBoundsConstrainer.cpp"; path = "../../../juce/modules/juce_gui_basics/layout/juce_ComponentBoundsConstrainer.cpp"; sourceTree = "SOURCE_ROOT"; };
+		03944A3451E95129394471C3 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_OpenGL_ios.h"; path = "../../../juce/modules/juce_opengl/native/juce_OpenGL_ios.h"; sourceTree = "SOURCE_ROOT"; };
+		03FD289F5F76E8E1644CE8AB = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_ResizableCornerComponent.cpp"; path = "../../../juce/modules/juce_gui_basics/layout/juce_ResizableCornerComponent.cpp"; sourceTree = "SOURCE_ROOT"; };
+		0436257DE1C3D66483C147A4 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_ChannelRemappingAudioSource.cpp"; path = "../../../juce/modules/juce_audio_basics/sources/juce_ChannelRemappingAudioSource.cpp"; sourceTree = "SOURCE_ROOT"; };
+		04B1699E31A404BB6C7E4D93 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_PopupMenu.cpp"; path = "../../../juce/modules/juce_gui_basics/menus/juce_PopupMenu.cpp"; sourceTree = "SOURCE_ROOT"; };
+		05A3090F3BD2DAA8D6DF24B6 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = KeyIdleDetector.cpp; path = ../../Source/TouchKeys/KeyIdleDetector.cpp; sourceTree = "SOURCE_ROOT"; };
+		06111D0738FEE481A9A0212B = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_posix_NamedPipe.cpp"; path = "../../../juce/modules/juce_core/native/juce_posix_NamedPipe.cpp"; sourceTree = "SOURCE_ROOT"; };
+		06462D0D2425BAF020472691 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_NSViewComponent.h"; path = "../../../juce/modules/juce_gui_extra/embedding/juce_NSViewComponent.h"; sourceTree = "SOURCE_ROOT"; };
+		070312CF9497593454CB10C1 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_OutputStream.cpp"; path = "../../../juce/modules/juce_core/streams/juce_OutputStream.cpp"; sourceTree = "SOURCE_ROOT"; };
+		0738BD96A6B0D207B2656964 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_DynamicLibrary.h"; path = "../../../juce/modules/juce_core/threads/juce_DynamicLibrary.h"; sourceTree = "SOURCE_ROOT"; };
+		0754AE37A2269C502075E46B = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; name = "juce_mac_NSViewComponent.mm"; path = "../../../juce/modules/juce_gui_extra/native/juce_mac_NSViewComponent.mm"; sourceTree = "SOURCE_ROOT"; };
+		077F55BB50ECFDEA65F71DF6 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_ComponentBoundsConstrainer.h"; path = "../../../juce/modules/juce_gui_basics/layout/juce_ComponentBoundsConstrainer.h"; sourceTree = "SOURCE_ROOT"; };
+		07D76602E68244DA48A496C3 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_TextInputTarget.h"; path = "../../../juce/modules/juce_gui_basics/keyboard/juce_TextInputTarget.h"; sourceTree = "SOURCE_ROOT"; };
+		07F3126C9F1842B069672882 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = TouchkeyMultiFingerTriggerMappingFactory.h; path = ../../Source/Mappings/MultiFingerTrigger/TouchkeyMultiFingerTriggerMappingFactory.h; sourceTree = "SOURCE_ROOT"; };
+		0838AE8A8B614755B5F31595 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_ColourSelector.cpp"; path = "../../../juce/modules/juce_gui_extra/misc/juce_ColourSelector.cpp"; sourceTree = "SOURCE_ROOT"; };
+		0900F84A5A94029BABD4F3D0 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_android_Windowing.cpp"; path = "../../../juce/modules/juce_gui_basics/native/juce_android_Windowing.cpp"; sourceTree = "SOURCE_ROOT"; };
+		091527B6BC312B2295FA565A = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = MainApplicationController.h; path = ../../Source/MainApplicationController.h; sourceTree = "SOURCE_ROOT"; };
+		091FB91B90B6E8C215BB309B = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_StretchableObjectResizer.cpp"; path = "../../../juce/modules/juce_gui_basics/layout/juce_StretchableObjectResizer.cpp"; sourceTree = "SOURCE_ROOT"; };
+		0955F3A0CD0B38AA81A04149 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = Trigger.h; path = ../../Source/Utility/Trigger.h; sourceTree = "SOURCE_ROOT"; };
+		09995800CE4E0E3EF8C11EF8 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_AsyncUpdater.h"; path = "../../../juce/modules/juce_events/broadcasters/juce_AsyncUpdater.h"; sourceTree = "SOURCE_ROOT"; };
+		099F9D0711B4409A70F97168 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_ListBox.cpp"; path = "../../../juce/modules/juce_gui_basics/widgets/juce_ListBox.cpp"; sourceTree = "SOURCE_ROOT"; };
+		09AFFC227A610CE469F29590 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_TableListBox.cpp"; path = "../../../juce/modules/juce_gui_basics/widgets/juce_TableListBox.cpp"; sourceTree = "SOURCE_ROOT"; };
+		09DB2381F553CE3AE1190F3C = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_ResamplingAudioSource.cpp"; path = "../../../juce/modules/juce_audio_basics/sources/juce_ResamplingAudioSource.cpp"; sourceTree = "SOURCE_ROOT"; };
+		09E18B170D35E65AABCC3C90 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_AudioFormatReader.cpp"; path = "../../../juce/modules/juce_audio_formats/format/juce_AudioFormatReader.cpp"; sourceTree = "SOURCE_ROOT"; };
+		0A106C01DDEECF61E2D7F51E = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_Image.h"; path = "../../../juce/modules/juce_graphics/images/juce_Image.h"; sourceTree = "SOURCE_ROOT"; };
+		0A486C6A586B60525C9BAB59 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = MappingListItem.h; path = ../../Source/GUI/MappingListItem.h; sourceTree = "SOURCE_ROOT"; };
+		0A5EA54B877D84C939B6F6D7 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = TouchkeyControlMappingShortEditor.h; path = ../../Source/Mappings/Control/TouchkeyControlMappingShortEditor.h; sourceTree = "SOURCE_ROOT"; };
+		0A767ED26F1C6854875AC0DB = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = TouchkeyMultiFingerTriggerMapping.cpp; path = ../../Source/Mappings/MultiFingerTrigger/TouchkeyMultiFingerTriggerMapping.cpp; sourceTree = "SOURCE_ROOT"; };
+		0A982DF57CE775D8F05AD78B = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = TouchkeyMultiFingerTriggerMapping.h; path = ../../Source/Mappings/MultiFingerTrigger/TouchkeyMultiFingerTriggerMapping.h; sourceTree = "SOURCE_ROOT"; };
+		0A9A3C2E3F533AB2FD618DD9 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_InputSource.h"; path = "../../../juce/modules/juce_core/streams/juce_InputSource.h"; sourceTree = "SOURCE_ROOT"; };
+		0B15F842C077C9765284446A = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = TouchkeyPitchBendMapping.h; path = ../../Source/Mappings/PitchBend/TouchkeyPitchBendMapping.h; sourceTree = "SOURCE_ROOT"; };
+		0C516DEBD52810652C1B4F87 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_PreferencesPanel.cpp"; path = "../../../juce/modules/juce_gui_extra/misc/juce_PreferencesPanel.cpp"; sourceTree = "SOURCE_ROOT"; };
+		0C9554C2C80A409B9486F101 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = KeyboardZoneComponent.cpp; path = ../../Source/GUI/KeyboardZoneComponent.cpp; sourceTree = "SOURCE_ROOT"; };
+		0CCFFE7CFFC883C81D7D9441 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_Application.cpp"; path = "../../../juce/modules/juce_gui_basics/application/juce_Application.cpp"; sourceTree = "SOURCE_ROOT"; };
+		0D2D03F562749971564F7773 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = TouchkeyControlMapping.cpp; path = ../../Source/Mappings/Control/TouchkeyControlMapping.cpp; sourceTree = "SOURCE_ROOT"; };
+		0D5355C05137A13E0F9C3577 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_TextButton.cpp"; path = "../../../juce/modules/juce_gui_basics/buttons/juce_TextButton.cpp"; sourceTree = "SOURCE_ROOT"; };
+		0DD2016AB1A5661593E69C5A = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_FilePreviewComponent.h"; path = "../../../juce/modules/juce_gui_basics/filebrowser/juce_FilePreviewComponent.h"; sourceTree = "SOURCE_ROOT"; };
+		0E1393D568B97F822EC620E2 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_DirectoryContentsList.h"; path = "../../../juce/modules/juce_gui_basics/filebrowser/juce_DirectoryContentsList.h"; sourceTree = "SOURCE_ROOT"; };
+		0E25A6A37156A0D0F5063494 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = TouchkeyDevice.h; path = ../../Source/TouchKeys/TouchkeyDevice.h; sourceTree = "SOURCE_ROOT"; };
+		0F654BA2689F1C8D04DE1864 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_win32_Messaging.cpp"; path = "../../../juce/modules/juce_events/native/juce_win32_Messaging.cpp"; sourceTree = "SOURCE_ROOT"; };
+		0F78459420EA5BD1915A0A8A = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_Uuid.cpp"; path = "../../../juce/modules/juce_core/misc/juce_Uuid.cpp"; sourceTree = "SOURCE_ROOT"; };
+		0FD7CCA5B5517C3CF7C2CCA9 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_MessageManager.h"; path = "../../../juce/modules/juce_events/messages/juce_MessageManager.h"; sourceTree = "SOURCE_ROOT"; };
+		0FF0FEC5D686372FEF413FF4 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_MidiMessageCollector.h"; path = "../../../juce/modules/juce_audio_devices/midi_io/juce_MidiMessageCollector.h"; sourceTree = "SOURCE_ROOT"; };
+		0FF3EA4E9EBFF0FE96DF62E2 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_CustomTypeface.cpp"; path = "../../../juce/modules/juce_graphics/fonts/juce_CustomTypeface.cpp"; sourceTree = "SOURCE_ROOT"; };
+		102054A65E6A2C9E170F6C29 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_audio_basics.h"; path = "../../../juce/modules/juce_audio_basics/juce_audio_basics.h"; sourceTree = "SOURCE_ROOT"; };
+		10674C5BE3CBDC1812E82AFF = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_AudioFormat.cpp"; path = "../../../juce/modules/juce_audio_formats/format/juce_AudioFormat.cpp"; sourceTree = "SOURCE_ROOT"; };
+		106C5E90696E6F1CC04EF896 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_TimeSliceThread.h"; path = "../../../juce/modules/juce_core/threads/juce_TimeSliceThread.h"; sourceTree = "SOURCE_ROOT"; };
+		10E1A1A46BECA9BD86A43829 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_ApplicationCommandTarget.cpp"; path = "../../../juce/modules/juce_gui_basics/commands/juce_ApplicationCommandTarget.cpp"; sourceTree = "SOURCE_ROOT"; };
+		1144B3B90A0E8142CCBC8097 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_KeyPressMappingSet.cpp"; path = "../../../juce/modules/juce_gui_basics/commands/juce_KeyPressMappingSet.cpp"; sourceTree = "SOURCE_ROOT"; };
+		1167AC1646F727991030443D = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_ShapeButton.cpp"; path = "../../../juce/modules/juce_gui_basics/buttons/juce_ShapeButton.cpp"; sourceTree = "SOURCE_ROOT"; };
+		119E49FA10A84C0AED360605 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = TouchkeyVibratoMappingShortEditor.cpp; path = ../../Source/Mappings/Vibrato/TouchkeyVibratoMappingShortEditor.cpp; sourceTree = "SOURCE_ROOT"; };
+		11DF49143B2E40D5E8AB074D = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_MemoryOutputStream.cpp"; path = "../../../juce/modules/juce_core/streams/juce_MemoryOutputStream.cpp"; sourceTree = "SOURCE_ROOT"; };
+		11E656746B2425A8E99C2B1F = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_OpenGLTexture.h"; path = "../../../juce/modules/juce_opengl/opengl/juce_OpenGLTexture.h"; sourceTree = "SOURCE_ROOT"; };
+		11E71A3ED179AF068E6D8A9D = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_AudioSampleBuffer.cpp"; path = "../../../juce/modules/juce_audio_basics/buffers/juce_AudioSampleBuffer.cpp"; sourceTree = "SOURCE_ROOT"; };
+		121CF611E14695D8AC8D4AB3 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_ResizableEdgeComponent.h"; path = "../../../juce/modules/juce_gui_basics/layout/juce_ResizableEdgeComponent.h"; sourceTree = "SOURCE_ROOT"; };
+		1256FA69F41BA1090B5C1A16 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_KeyListener.h"; path = "../../../juce/modules/juce_gui_basics/keyboard/juce_KeyListener.h"; sourceTree = "SOURCE_ROOT"; };
+		1291BBAFB29453DAF847D561 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_EdgeTable.h"; path = "../../../juce/modules/juce_graphics/geometry/juce_EdgeTable.h"; sourceTree = "SOURCE_ROOT"; };
+		12EF0EF35B5F1C68F6576FBF = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_OptionalScopedPointer.h"; path = "../../../juce/modules/juce_core/memory/juce_OptionalScopedPointer.h"; sourceTree = "SOURCE_ROOT"; };
+		133CEEB518DC3E0387DDDAAA = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_Time.cpp"; path = "../../../juce/modules/juce_core/time/juce_Time.cpp"; sourceTree = "SOURCE_ROOT"; };
+		13C0DA6E480188AAA99E4283 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_PathStrokeType.h"; path = "../../../juce/modules/juce_graphics/geometry/juce_PathStrokeType.h"; sourceTree = "SOURCE_ROOT"; };
+		13F3F140177E3986BCBC12EB = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_AiffAudioFormat.cpp"; path = "../../../juce/modules/juce_audio_formats/codecs/juce_AiffAudioFormat.cpp"; sourceTree = "SOURCE_ROOT"; };
+		143C31FDEB6ACD8A02F6F861 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_MouseListener.cpp"; path = "../../../juce/modules/juce_gui_basics/mouse/juce_MouseListener.cpp"; sourceTree = "SOURCE_ROOT"; };
+		158AE747154A3811549CECBB = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_DrawableImage.cpp"; path = "../../../juce/modules/juce_gui_basics/drawables/juce_DrawableImage.cpp"; sourceTree = "SOURCE_ROOT"; };
+		158BA823A17ACCB4E6826026 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_LeakedObjectDetector.h"; path = "../../../juce/modules/juce_core/memory/juce_LeakedObjectDetector.h"; sourceTree = "SOURCE_ROOT"; };
+		1596880D3FEA7FDC94B7D71A = { isa = PBXFileReference; lastKnownFileType = image.png; name = "tk-icon-128.png"; path = "../../Resources/tk-icon-128.png"; sourceTree = "SOURCE_ROOT"; };
+		16BA68E8565D501ED6085968 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_WindowsMediaAudioFormat.cpp"; path = "../../../juce/modules/juce_audio_formats/codecs/juce_WindowsMediaAudioFormat.cpp"; sourceTree = "SOURCE_ROOT"; };
+		172BE377D36CE8F6915DECC0 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_AudioFormat.h"; path = "../../../juce/modules/juce_audio_formats/format/juce_AudioFormat.h"; sourceTree = "SOURCE_ROOT"; };
+		176B62DE2BBD7453ADE87482 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_AudioFormatWriter.cpp"; path = "../../../juce/modules/juce_audio_formats/format/juce_AudioFormatWriter.cpp"; sourceTree = "SOURCE_ROOT"; };
+		178A386F6FE9CCEAD2ACEA3A = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = OscMidiConverter.cpp; path = ../../Source/TouchKeys/OscMidiConverter.cpp; sourceTree = "SOURCE_ROOT"; };
+		179FD2EA9B129C845CB4DCA7 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_ScopedXLock.h"; path = "../../../juce/modules/juce_events/native/juce_ScopedXLock.h"; sourceTree = "SOURCE_ROOT"; };
+		17C5876FBE7CBA0393D71B51 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_FileOutputStream.cpp"; path = "../../../juce/modules/juce_core/files/juce_FileOutputStream.cpp"; sourceTree = "SOURCE_ROOT"; };
+		17FC0AB961883C8A5AB58D30 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_Path.cpp"; path = "../../../juce/modules/juce_graphics/geometry/juce_Path.cpp"; sourceTree = "SOURCE_ROOT"; };
+		182845A3D1FBA30CEF9E0DA2 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_ToggleButton.cpp"; path = "../../../juce/modules/juce_gui_basics/buttons/juce_ToggleButton.cpp"; sourceTree = "SOURCE_ROOT"; };
+		183D81D9E023859A3E499243 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = PianoKey.cpp; path = ../../Source/TouchKeys/PianoKey.cpp; sourceTree = "SOURCE_ROOT"; };
+		189E1AEC6F95E5DF992C910D = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_ComponentListener.cpp"; path = "../../../juce/modules/juce_gui_basics/components/juce_ComponentListener.cpp"; sourceTree = "SOURCE_ROOT"; };
+		19241B0EBAE0BB8B2B78C861 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_ByteOrder.h"; path = "../../../juce/modules/juce_core/memory/juce_ByteOrder.h"; sourceTree = "SOURCE_ROOT"; };
+		196DBB540F88035040706D73 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_Component.h"; path = "../../../juce/modules/juce_gui_basics/components/juce_Component.h"; sourceTree = "SOURCE_ROOT"; };
+		19ECE65352DCC21F539ADFDE = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_ModalComponentManager.h"; path = "../../../juce/modules/juce_gui_basics/components/juce_ModalComponentManager.h"; sourceTree = "SOURCE_ROOT"; };
+		1A4837FC0BE8B165FAFD999A = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_ApplicationCommandID.h"; path = "../../../juce/modules/juce_gui_basics/commands/juce_ApplicationCommandID.h"; sourceTree = "SOURCE_ROOT"; };
+		1A67F2C4654C988FD919A612 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_TopLevelWindow.cpp"; path = "../../../juce/modules/juce_gui_basics/windows/juce_TopLevelWindow.cpp"; sourceTree = "SOURCE_ROOT"; };
+		1A96C45EFEBAAD6DA351F6E7 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_BubbleComponent.cpp"; path = "../../../juce/modules/juce_gui_basics/misc/juce_BubbleComponent.cpp"; sourceTree = "SOURCE_ROOT"; };
+		1A9B74287470FD1944123E26 = { isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Accelerate.framework; path = System/Library/Frameworks/Accelerate.framework; sourceTree = SDKROOT; };
+		1B21A0523192F8E570190A8D = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_Sampler.cpp"; path = "../../../juce/modules/juce_audio_formats/sampler/juce_Sampler.cpp"; sourceTree = "SOURCE_ROOT"; };
+		1B47C271A0750D61976A387B = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_ThreadPool.cpp"; path = "../../../juce/modules/juce_core/threads/juce_ThreadPool.cpp"; sourceTree = "SOURCE_ROOT"; };
+		1BAF3D8502C6D43B2CA5CF8A = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_SubregionStream.h"; path = "../../../juce/modules/juce_core/streams/juce_SubregionStream.h"; sourceTree = "SOURCE_ROOT"; };
+		1C1E6DBAE366BBF6B93C6231 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_TreeView.h"; path = "../../../juce/modules/juce_gui_basics/widgets/juce_TreeView.h"; sourceTree = "SOURCE_ROOT"; };
+		1C2CDFD33D4679F91D0FBE57 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_Drawable.cpp"; path = "../../../juce/modules/juce_gui_basics/drawables/juce_Drawable.cpp"; sourceTree = "SOURCE_ROOT"; };
+		1C3B338D3F36E6ADB417204D = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_Array.h"; path = "../../../juce/modules/juce_core/containers/juce_Array.h"; sourceTree = "SOURCE_ROOT"; };
+		1C69D05010E094E31BACC6AA = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_IIRFilterAudioSource.cpp"; path = "../../../juce/modules/juce_audio_basics/sources/juce_IIRFilterAudioSource.cpp"; sourceTree = "SOURCE_ROOT"; };
+		1CD21A58F0E15076E1027B2E = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_ImageEffectFilter.h"; path = "../../../juce/modules/juce_graphics/effects/juce_ImageEffectFilter.h"; sourceTree = "SOURCE_ROOT"; };
+		1D41477949C13C4A9C7F0536 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_JSON.cpp"; path = "../../../juce/modules/juce_core/javascript/juce_JSON.cpp"; sourceTree = "SOURCE_ROOT"; };
+		1D633C390EEF3C24FE44FF27 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_android_Threads.cpp"; path = "../../../juce/modules/juce_core/native/juce_android_Threads.cpp"; sourceTree = "SOURCE_ROOT"; };
+		1DCB093C0AA6B07F8846CE5F = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_AiffAudioFormat.h"; path = "../../../juce/modules/juce_audio_formats/codecs/juce_AiffAudioFormat.h"; sourceTree = "SOURCE_ROOT"; };
+		1DCDF6FC01C7DBDDA7D7AFEA = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = TouchkeyPitchBendMappingShortEditor.h; path = ../../Source/Mappings/PitchBend/TouchkeyPitchBendMappingShortEditor.h; sourceTree = "SOURCE_ROOT"; };
+		1DD5318B2EC76DCE6B3F715C = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_AbstractFifo.h"; path = "../../../juce/modules/juce_core/containers/juce_AbstractFifo.h"; sourceTree = "SOURCE_ROOT"; };
+		1EDD89AA0A5497A9AC685E41 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = TimestampSynchronizer.h; path = ../../Source/Utility/TimestampSynchronizer.h; sourceTree = "SOURCE_ROOT"; };
+		1F101BE64CA15979764736A3 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_XmlDocument.cpp"; path = "../../../juce/modules/juce_core/xml/juce_XmlDocument.cpp"; sourceTree = "SOURCE_ROOT"; };
+		1F4C487096EFF581BE969D75 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_ListBox.h"; path = "../../../juce/modules/juce_gui_basics/widgets/juce_ListBox.h"; sourceTree = "SOURCE_ROOT"; };
+		1F61F541D6F0256136E4F37B = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_OpenGLPixelFormat.cpp"; path = "../../../juce/modules/juce_opengl/opengl/juce_OpenGLPixelFormat.cpp"; sourceTree = "SOURCE_ROOT"; };
+		1F76B83EE0CFAA4761604B94 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_CallbackMessage.h"; path = "../../../juce/modules/juce_events/messages/juce_CallbackMessage.h"; sourceTree = "SOURCE_ROOT"; };
+		1FA09EB888ECFBF0424174B2 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_GlowEffect.cpp"; path = "../../../juce/modules/juce_graphics/effects/juce_GlowEffect.cpp"; sourceTree = "SOURCE_ROOT"; };
+		1FADCBF1B7451DE704A9E5DE = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_Desktop.cpp"; path = "../../../juce/modules/juce_gui_basics/components/juce_Desktop.cpp"; sourceTree = "SOURCE_ROOT"; };
+		1FB429A8B8F85E403CA12650 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_RelativePointPath.h"; path = "../../../juce/modules/juce_gui_basics/positioning/juce_RelativePointPath.h"; sourceTree = "SOURCE_ROOT"; };
+		206A731B98F5606ED6B6F951 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_SystemAudioVolume.h"; path = "../../../juce/modules/juce_audio_devices/audio_io/juce_SystemAudioVolume.h"; sourceTree = "SOURCE_ROOT"; };
+		20BA5BC9BB93D1041D8F4C73 = { isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreMIDI.framework; path = System/Library/Frameworks/CoreMIDI.framework; sourceTree = SDKROOT; };
+		213F98F1913ED639AE895474 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_LookAndFeel_V2.h"; path = "../../../juce/modules/juce_gui_basics/lookandfeel/juce_LookAndFeel_V2.h"; sourceTree = "SOURCE_ROOT"; };
+		216C43FE64E807DFDDE137BB = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_SystemTrayIconComponent.cpp"; path = "../../../juce/modules/juce_gui_extra/misc/juce_SystemTrayIconComponent.cpp"; sourceTree = "SOURCE_ROOT"; };
+		21761A1F87F6E65A6DDF9AF3 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_AttributedString.cpp"; path = "../../../juce/modules/juce_graphics/fonts/juce_AttributedString.cpp"; sourceTree = "SOURCE_ROOT"; };
+		226A0BD39983B2132DDBD732 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_PropertiesFile.cpp"; path = "../../../juce/modules/juce_data_structures/app_properties/juce_PropertiesFile.cpp"; sourceTree = "SOURCE_ROOT"; };
+		22A640D49DAB8EC000C48C37 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_BigInteger.cpp"; path = "../../../juce/modules/juce_core/maths/juce_BigInteger.cpp"; sourceTree = "SOURCE_ROOT"; };
+		22C2B16BCB6F4CA405233C04 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_AudioSampleBuffer.h"; path = "../../../juce/modules/juce_audio_basics/buffers/juce_AudioSampleBuffer.h"; sourceTree = "SOURCE_ROOT"; };
+		23E81037EDC6EBA8A4145A66 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; name = "juce_audio_formats.mm"; path = "../../../juce/modules/juce_audio_formats/juce_audio_formats.mm"; sourceTree = "SOURCE_ROOT"; };
+		24BBD2EEE5685F7BF08524DC = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_NewLine.h"; path = "../../../juce/modules/juce_core/text/juce_NewLine.h"; sourceTree = "SOURCE_ROOT"; };
+		25875D01B837C34F01EF8C2F = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = TouchkeyPitchBendMappingShortEditor.cpp; path = ../../Source/Mappings/PitchBend/TouchkeyPitchBendMappingShortEditor.cpp; sourceTree = "SOURCE_ROOT"; };
+		25E54DE680924E94F31F7727 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_BufferingAudioSource.cpp"; path = "../../../juce/modules/juce_audio_basics/sources/juce_BufferingAudioSource.cpp"; sourceTree = "SOURCE_ROOT"; };
+		265BDA429218A7AE3E5608CD = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_TopLevelWindow.h"; path = "../../../juce/modules/juce_gui_basics/windows/juce_TopLevelWindow.h"; sourceTree = "SOURCE_ROOT"; };
+		26646EAE80CBDF47DADEE278 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = MainApplicationController.cpp; path = ../../Source/MainApplicationController.cpp; sourceTree = "SOURCE_ROOT"; };
+		267AFF098D4880F1941B5201 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_OpenGLPixelFormat.h"; path = "../../../juce/modules/juce_opengl/opengl/juce_OpenGLPixelFormat.h"; sourceTree = "SOURCE_ROOT"; };
+		27034337E8AAB0ED01CA45E9 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_AudioIODevice.cpp"; path = "../../../juce/modules/juce_audio_devices/audio_io/juce_AudioIODevice.cpp"; sourceTree = "SOURCE_ROOT"; };
+		2710E4CE033A62C80F13B7F2 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_MACAddress.cpp"; path = "../../../juce/modules/juce_core/network/juce_MACAddress.cpp"; sourceTree = "SOURCE_ROOT"; };
+		277CDB2F2319FD585A04F00D = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_win32_Registry.cpp"; path = "../../../juce/modules/juce_core/native/juce_win32_Registry.cpp"; sourceTree = "SOURCE_ROOT"; };
+		27829CDA49F7FB509A7B5A1C = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_PropertySet.h"; path = "../../../juce/modules/juce_core/containers/juce_PropertySet.h"; sourceTree = "SOURCE_ROOT"; };
+		283FF67DF916C041CE17E244 = { isa = PBXFileReference; lastKnownFileType = file.nib; name = RecentFilesMenuTemplate.nib; path = RecentFilesMenuTemplate.nib; sourceTree = "SOURCE_ROOT"; };
+		2846850D6D318CBE5662505E = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_DragAndDropContainer.cpp"; path = "../../../juce/modules/juce_gui_basics/mouse/juce_DragAndDropContainer.cpp"; sourceTree = "SOURCE_ROOT"; };
+		286E94AA7893E151671EFACC = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_ComponentMovementWatcher.cpp"; path = "../../../juce/modules/juce_gui_basics/layout/juce_ComponentMovementWatcher.cpp"; sourceTree = "SOURCE_ROOT"; };
+		28A31552383A162DC607EAA0 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_ToolbarItemComponent.h"; path = "../../../juce/modules/juce_gui_basics/widgets/juce_ToolbarItemComponent.h"; sourceTree = "SOURCE_ROOT"; };
+		29057C0D0A47537D39F20C9C = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_MidiMessageSequence.cpp"; path = "../../../juce/modules/juce_audio_basics/midi/juce_MidiMessageSequence.cpp"; sourceTree = "SOURCE_ROOT"; };
+		290AE3B1231EFE4B43011C38 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_Desktop.h"; path = "../../../juce/modules/juce_gui_basics/components/juce_Desktop.h"; sourceTree = "SOURCE_ROOT"; };
+		293AA70C67056ED558221FCD = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = TouchkeyControlMapping.h; path = ../../Source/Mappings/Control/TouchkeyControlMapping.h; sourceTree = "SOURCE_ROOT"; };
+		2A64318B395562B54E8B978D = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_BufferedInputStream.cpp"; path = "../../../juce/modules/juce_core/streams/juce_BufferedInputStream.cpp"; sourceTree = "SOURCE_ROOT"; };
+		2AB205503EDC14D07B0CDFA7 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = TouchkeyBaseMapping.h; path = ../../Source/Mappings/TouchkeyBaseMapping.h; sourceTree = "SOURCE_ROOT"; };
+		2B3A1D8D1A77B39FFBA39EC8 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_XmlElement.cpp"; path = "../../../juce/modules/juce_core/xml/juce_XmlElement.cpp"; sourceTree = "SOURCE_ROOT"; };
+		2B4B4811F717681D3CEA3C06 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_TooltipWindow.cpp"; path = "../../../juce/modules/juce_gui_basics/windows/juce_TooltipWindow.cpp"; sourceTree = "SOURCE_ROOT"; };
+		2B545CB34532967184BB979C = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_DragAndDropTarget.h"; path = "../../../juce/modules/juce_gui_basics/mouse/juce_DragAndDropTarget.h"; sourceTree = "SOURCE_ROOT"; };
+		2C07FA9C85BA4477E7E732BA = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_FileInputStream.cpp"; path = "../../../juce/modules/juce_core/files/juce_FileInputStream.cpp"; sourceTree = "SOURCE_ROOT"; };
+		2C144159C82D78DDD70C4105 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_DialogWindow.cpp"; path = "../../../juce/modules/juce_gui_basics/windows/juce_DialogWindow.cpp"; sourceTree = "SOURCE_ROOT"; };
+		2C14D0A7927411AE5E37293F = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_HyperlinkButton.cpp"; path = "../../../juce/modules/juce_gui_basics/buttons/juce_HyperlinkButton.cpp"; sourceTree = "SOURCE_ROOT"; };
+		2C65A282C235450315BFA7B7 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_FileSearchPath.h"; path = "../../../juce/modules/juce_core/files/juce_FileSearchPath.h"; sourceTree = "SOURCE_ROOT"; };
+		2C8F38028F686154B4760DC4 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_FloatVectorOperations.cpp"; path = "../../../juce/modules/juce_audio_basics/buffers/juce_FloatVectorOperations.cpp"; sourceTree = "SOURCE_ROOT"; };
+		2CBEEB3DDE7452AFAA496A53 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_gui_extra.h"; path = "../../../juce/modules/juce_gui_extra/juce_gui_extra.h"; sourceTree = "SOURCE_ROOT"; };
+		2CEB8C23A0AE5989D02CA488 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_DropShadowEffect.cpp"; path = "../../../juce/modules/juce_graphics/effects/juce_DropShadowEffect.cpp"; sourceTree = "SOURCE_ROOT"; };
+		2CFC336DBA57275297F8966D = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_CodeTokeniser.h"; path = "../../../juce/modules/juce_gui_extra/code_editor/juce_CodeTokeniser.h"; sourceTree = "SOURCE_ROOT"; };
+		2D8CAD385E5572C8E790B98E = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_WebBrowserComponent.h"; path = "../../../juce/modules/juce_gui_extra/misc/juce_WebBrowserComponent.h"; sourceTree = "SOURCE_ROOT"; };
+		2DA07ABEBAE78CBA17AD496C = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_osx_ObjCHelpers.h"; path = "../../../juce/modules/juce_core/native/juce_osx_ObjCHelpers.h"; sourceTree = "SOURCE_ROOT"; };
+		2DB8DD2C8970470DD6C94355 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_IIRFilterAudioSource.h"; path = "../../../juce/modules/juce_audio_basics/sources/juce_IIRFilterAudioSource.h"; sourceTree = "SOURCE_ROOT"; };
+		2DF4F5F337BF6C45F00C2921 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_CharPointer_UTF32.h"; path = "../../../juce/modules/juce_core/text/juce_CharPointer_UTF32.h"; sourceTree = "SOURCE_ROOT"; };
+		2E2351F96F9FEDFB06036B45 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_MenuBarModel.cpp"; path = "../../../juce/modules/juce_gui_basics/menus/juce_MenuBarModel.cpp"; sourceTree = "SOURCE_ROOT"; };
+		2E8E49CCC7727B5DBE866711 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_MACAddress.h"; path = "../../../juce/modules/juce_core/network/juce_MACAddress.h"; sourceTree = "SOURCE_ROOT"; };
+		2ED84C912889F7D0DA71BC31 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_ElementComparator.h"; path = "../../../juce/modules/juce_core/containers/juce_ElementComparator.h"; sourceTree = "SOURCE_ROOT"; };
+		2EFF4ABBAACFB3611A738940 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_Socket.h"; path = "../../../juce/modules/juce_core/network/juce_Socket.h"; sourceTree = "SOURCE_ROOT"; };
+		2F46807285895EC6D5DDA8E3 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_ActionBroadcaster.h"; path = "../../../juce/modules/juce_events/broadcasters/juce_ActionBroadcaster.h"; sourceTree = "SOURCE_ROOT"; };
+		2F5F168DE166CF81749EDE32 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_PerformanceCounter.h"; path = "../../../juce/modules/juce_core/time/juce_PerformanceCounter.h"; sourceTree = "SOURCE_ROOT"; };
+		2F6C87AE0D32BF2600B951B2 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_TooltipWindow.h"; path = "../../../juce/modules/juce_gui_basics/windows/juce_TooltipWindow.h"; sourceTree = "SOURCE_ROOT"; };
+		2FEDA73E284BA8DD022BD7E0 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_TextPropertyComponent.h"; path = "../../../juce/modules/juce_gui_basics/properties/juce_TextPropertyComponent.h"; sourceTree = "SOURCE_ROOT"; };
+		3014C4D84D91D187C834D4D9 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_LookAndFeel_V1.cpp"; path = "../../../juce/modules/juce_gui_basics/lookandfeel/juce_LookAndFeel_V1.cpp"; sourceTree = "SOURCE_ROOT"; };
+		30787E67674DB9065149BCF9 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_ReadWriteLock.h"; path = "../../../juce/modules/juce_core/threads/juce_ReadWriteLock.h"; sourceTree = "SOURCE_ROOT"; };
+		30B72EA5CC760FDC1D68A1D5 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_UndoManager.h"; path = "../../../juce/modules/juce_data_structures/undomanager/juce_UndoManager.h"; sourceTree = "SOURCE_ROOT"; };
+		30BABDF73CAF7CC000817364 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_DirectoryContentsDisplayComponent.cpp"; path = "../../../juce/modules/juce_gui_basics/filebrowser/juce_DirectoryContentsDisplayComponent.cpp"; sourceTree = "SOURCE_ROOT"; };
+		31F03E9FBCC8C55504B03502 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_linux_SystemTrayIcon.cpp"; path = "../../../juce/modules/juce_gui_extra/native/juce_linux_SystemTrayIcon.cpp"; sourceTree = "SOURCE_ROOT"; };
+		3206DFDE463E6BD093541953 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_CoreAudioFormat.cpp"; path = "../../../juce/modules/juce_audio_formats/codecs/juce_CoreAudioFormat.cpp"; sourceTree = "SOURCE_ROOT"; };
+		32302BE7297F75C489B19CED = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = TimerNode.cpp; path = ../../Source/Utility/TimerNode.cpp; sourceTree = "SOURCE_ROOT"; };
+		32C7614FDFD477BFC45C7A05 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_PlatformDefs.h"; path = "../../../juce/modules/juce_core/system/juce_PlatformDefs.h"; sourceTree = "SOURCE_ROOT"; };
+		33486E83204368CF282A73F8 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_MP3AudioFormat.h"; path = "../../../juce/modules/juce_audio_formats/codecs/juce_MP3AudioFormat.h"; sourceTree = "SOURCE_ROOT"; };
+		335BCF6E40FD20E31CFFCCE4 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_CodeDocument.h"; path = "../../../juce/modules/juce_gui_extra/code_editor/juce_CodeDocument.h"; sourceTree = "SOURCE_ROOT"; };
+		336105F72E57EF2412E2D362 = { isa = PBXFileReference; lastKnownFileType = file; name = "juce_module_info"; path = "../../../juce/modules/juce_gui_basics/juce_module_info"; sourceTree = "SOURCE_ROOT"; };
+		34F26056D1BF4026D3CA3DFB = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_TableHeaderComponent.cpp"; path = "../../../juce/modules/juce_gui_basics/widgets/juce_TableHeaderComponent.cpp"; sourceTree = "SOURCE_ROOT"; };
+		34F6C441AE3E69C11C988F58 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_Toolbar.cpp"; path = "../../../juce/modules/juce_gui_basics/widgets/juce_Toolbar.cpp"; sourceTree = "SOURCE_ROOT"; };
+		350BF12B579608D2FBAD6AFC = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_android_FileChooser.cpp"; path = "../../../juce/modules/juce_gui_basics/native/juce_android_FileChooser.cpp"; sourceTree = "SOURCE_ROOT"; };
+		350CAF816F5658A6F048333B = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_ImageButton.cpp"; path = "../../../juce/modules/juce_gui_basics/buttons/juce_ImageButton.cpp"; sourceTree = "SOURCE_ROOT"; };
+		3513EF2F4FDDA4BB173BA3D7 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_win32_WebBrowserComponent.cpp"; path = "../../../juce/modules/juce_gui_extra/native/juce_win32_WebBrowserComponent.cpp"; sourceTree = "SOURCE_ROOT"; };
+		354D2DA37EA34EFC3121E523 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_MidiMessageCollector.cpp"; path = "../../../juce/modules/juce_audio_devices/midi_io/juce_MidiMessageCollector.cpp"; sourceTree = "SOURCE_ROOT"; };
+		35660D5C79544657DACE648E = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_TextDiff.cpp"; path = "../../../juce/modules/juce_core/text/juce_TextDiff.cpp"; sourceTree = "SOURCE_ROOT"; };
+		35C231F40757E0C7B7BE74A4 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_android_Files.cpp"; path = "../../../juce/modules/juce_core/native/juce_android_Files.cpp"; sourceTree = "SOURCE_ROOT"; };
+		369F9740183012CD9E400C49 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_AudioIODeviceType.cpp"; path = "../../../juce/modules/juce_audio_devices/audio_io/juce_AudioIODeviceType.cpp"; sourceTree = "SOURCE_ROOT"; };
+		36F8B4676EACEAFCED73F5A0 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_win32_AudioCDBurner.cpp"; path = "../../../juce/modules/juce_audio_devices/native/juce_win32_AudioCDBurner.cpp"; sourceTree = "SOURCE_ROOT"; };
+		3721BA53A6BAB64AFCE77C35 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_String.cpp"; path = "../../../juce/modules/juce_core/text/juce_String.cpp"; sourceTree = "SOURCE_ROOT"; };
+		3738D2B9C5D2ACBA23938BD6 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_ReferenceCountedArray.h"; path = "../../../juce/modules/juce_core/containers/juce_ReferenceCountedArray.h"; sourceTree = "SOURCE_ROOT"; };
+		37D9F61856238A58FBAF151C = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_CharacterFunctions.h"; path = "../../../juce/modules/juce_core/text/juce_CharacterFunctions.h"; sourceTree = "SOURCE_ROOT"; };
+		3851FE5AD54A9688682B21E9 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_ResizableWindow.h"; path = "../../../juce/modules/juce_gui_basics/windows/juce_ResizableWindow.h"; sourceTree = "SOURCE_ROOT"; };
+		3882417E73E282C25A526368 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_StringArray.cpp"; path = "../../../juce/modules/juce_core/text/juce_StringArray.cpp"; sourceTree = "SOURCE_ROOT"; };
+		390F98D3E43D2BC741B08D5B = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_MemoryOutputStream.h"; path = "../../../juce/modules/juce_core/streams/juce_MemoryOutputStream.h"; sourceTree = "SOURCE_ROOT"; };
+		394A564D17F7FC26D45DD047 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_FileBasedDocument.cpp"; path = "../../../juce/modules/juce_gui_extra/documents/juce_FileBasedDocument.cpp"; sourceTree = "SOURCE_ROOT"; };
+		396B15B57E8411B6131561B1 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_MidiOutput.h"; path = "../../../juce/modules/juce_audio_devices/midi_io/juce_MidiOutput.h"; sourceTree = "SOURCE_ROOT"; };
+		399F9880BC2270EF7FD9BFA1 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_GroupComponent.h"; path = "../../../juce/modules/juce_gui_basics/layout/juce_GroupComponent.h"; sourceTree = "SOURCE_ROOT"; };
+		39A2849DABF6CBB1F554501D = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_GZIPDecompressorInputStream.cpp"; path = "../../../juce/modules/juce_core/zip/juce_GZIPDecompressorInputStream.cpp"; sourceTree = "SOURCE_ROOT"; };
+		39A9EBDF9FF3E480CD2B43EB = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_OpenGLShaderProgram.cpp"; path = "../../../juce/modules/juce_opengl/opengl/juce_OpenGLShaderProgram.cpp"; sourceTree = "SOURCE_ROOT"; };
+		3B0FB247B6CA70098D56B522 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_BufferedInputStream.h"; path = "../../../juce/modules/juce_core/streams/juce_BufferedInputStream.h"; sourceTree = "SOURCE_ROOT"; };
+		3BA1D64E8B635B5EFBD8C416 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = TouchkeyReleaseAngleMappingFactory.h; path = ../../Source/Mappings/ReleaseAngle/TouchkeyReleaseAngleMappingFactory.h; sourceTree = "SOURCE_ROOT"; };
+		3C49A71B859640FC4A39B8F5 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_InterprocessConnection.cpp"; path = "../../../juce/modules/juce_events/interprocess/juce_InterprocessConnection.cpp"; sourceTree = "SOURCE_ROOT"; };
+		3CC9C766A7CA3465A75C1264 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_ModalComponentManager.cpp"; path = "../../../juce/modules/juce_gui_basics/components/juce_ModalComponentManager.cpp"; sourceTree = "SOURCE_ROOT"; };
+		3D220516F82AD82C888C6765 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = KeyboardDisplay.h; path = ../../Source/Display/KeyboardDisplay.h; sourceTree = "SOURCE_ROOT"; };
+		3D29826EB4A5B899C3624500 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_ConcertinaPanel.h"; path = "../../../juce/modules/juce_gui_basics/layout/juce_ConcertinaPanel.h"; sourceTree = "SOURCE_ROOT"; };
+		3D2DF36E0CC698317233B864 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_Label.cpp"; path = "../../../juce/modules/juce_gui_basics/widgets/juce_Label.cpp"; sourceTree = "SOURCE_ROOT"; };
+		3D8CB5CFD9B2031C3E016A0B = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_PathIterator.h"; path = "../../../juce/modules/juce_graphics/geometry/juce_PathIterator.h"; sourceTree = "SOURCE_ROOT"; };
+		3D9C5122C7D61F188F36817E = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_Process.h"; path = "../../../juce/modules/juce_core/threads/juce_Process.h"; sourceTree = "SOURCE_ROOT"; };
+		3DB49071D4E78F56A380391D = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_AudioCDReader.cpp"; path = "../../../juce/modules/juce_audio_devices/audio_cd/juce_AudioCDReader.cpp"; sourceTree = "SOURCE_ROOT"; };
+		3DFDF8135971D71B8889E84B = { isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = DiscRecording.framework; path = System/Library/Frameworks/DiscRecording.framework; sourceTree = SDKROOT; };
+		3E03094D5D1AC333A39F4B7A = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_BigInteger.h"; path = "../../../juce/modules/juce_core/maths/juce_BigInteger.h"; sourceTree = "SOURCE_ROOT"; };
+		3F044B1232BF1AD76CF25C05 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_StringPool.cpp"; path = "../../../juce/modules/juce_core/text/juce_StringPool.cpp"; sourceTree = "SOURCE_ROOT"; };
+		3F12742B404F1BCE9FD1182F = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_ScopedLock.h"; path = "../../../juce/modules/juce_core/threads/juce_ScopedLock.h"; sourceTree = "SOURCE_ROOT"; };
+		3F55433E0CFB772D4CEC69EA = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_AsyncUpdater.cpp"; path = "../../../juce/modules/juce_events/broadcasters/juce_AsyncUpdater.cpp"; sourceTree = "SOURCE_ROOT"; };
+		3F8F763B8A07A2EE8D42C0C2 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_OpenGLHelpers.h"; path = "../../../juce/modules/juce_opengl/opengl/juce_OpenGLHelpers.h"; sourceTree = "SOURCE_ROOT"; };
+		3FA6827CE8208A2676F7D89D = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_OpenGLShaderProgram.h"; path = "../../../juce/modules/juce_opengl/opengl/juce_OpenGLShaderProgram.h"; sourceTree = "SOURCE_ROOT"; };
+		3FBF5C96BBAA8C88CB37943C = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = KeyboardZoneComponent.h; path = ../../Source/GUI/KeyboardZoneComponent.h; sourceTree = "SOURCE_ROOT"; };
+		3FDF3206CD46EA7C0063B295 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = KeyPositionGraphDisplay.cpp; path = ../../Source/Display/KeyPositionGraphDisplay.cpp; sourceTree = "SOURCE_ROOT"; };
+		4025F5D575B9A5A511EF8F30 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = TouchkeyVibratoMapping.h; path = ../../Source/Mappings/Vibrato/TouchkeyVibratoMapping.h; sourceTree = "SOURCE_ROOT"; };
+		40B64B6C8BD06AD2930DA9F0 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_Value.h"; path = "../../../juce/modules/juce_data_structures/values/juce_Value.h"; sourceTree = "SOURCE_ROOT"; };
+		41D8CD12402ACE94C06C12CF = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_LookAndFeel_V3.h"; path = "../../../juce/modules/juce_gui_basics/lookandfeel/juce_LookAndFeel_V3.h"; sourceTree = "SOURCE_ROOT"; };
+		420CD52BA81E8FA04E73B0A1 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_ComponentPeer.cpp"; path = "../../../juce/modules/juce_gui_basics/windows/juce_ComponentPeer.cpp"; sourceTree = "SOURCE_ROOT"; };
+		4218E86125D6E9C9ACB58741 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_linux_AudioCDReader.cpp"; path = "../../../juce/modules/juce_audio_devices/native/juce_linux_AudioCDReader.cpp"; sourceTree = "SOURCE_ROOT"; };
+		42312AF39D46367A8CCAB92F = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = OpenGLJuceCanvas.h; path = ../../Source/Display/OpenGLJuceCanvas.h; sourceTree = "SOURCE_ROOT"; };
+		4262B169F606570751DE5855 = { isa = PBXFileReference; lastKnownFileType = image.png; name = "tk-icon-256.png"; path = "../../Resources/tk-icon-256.png"; sourceTree = "SOURCE_ROOT"; };
+		42C6DE63A9107E8D5B543F73 = { isa = PBXFileReference; lastKnownFileType = file; name = "juce_module_info"; path = "../../../juce/modules/juce_data_structures/juce_module_info"; sourceTree = "SOURCE_ROOT"; };
+		42C95CE7D85568838409D2CF = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_ProgressBar.h"; path = "../../../juce/modules/juce_gui_basics/widgets/juce_ProgressBar.h"; sourceTree = "SOURCE_ROOT"; };
+		42DE2C4C5791303F70375058 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = MidiOutputController.h; path = ../../Source/TouchKeys/MidiOutputController.h; sourceTree = "SOURCE_ROOT"; };
+		435F555EAB5F88A7DE95C8FB = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_Atomic.h"; path = "../../../juce/modules/juce_core/memory/juce_Atomic.h"; sourceTree = "SOURCE_ROOT"; };
+		439AF7EB00EE96C9B8E9B480 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = KeyPositionGraphDisplay.h; path = ../../Source/Display/KeyPositionGraphDisplay.h; sourceTree = "SOURCE_ROOT"; };
+		440B6F250CB865AA74386018 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_SpinLock.h"; path = "../../../juce/modules/juce_core/threads/juce_SpinLock.h"; sourceTree = "SOURCE_ROOT"; };
+		44820B2991A9F69F106501EB = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_PropertyComponent.h"; path = "../../../juce/modules/juce_gui_basics/properties/juce_PropertyComponent.h"; sourceTree = "SOURCE_ROOT"; };
+		451D1DA2175F20466C358449 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_ChangeBroadcaster.h"; path = "../../../juce/modules/juce_events/broadcasters/juce_ChangeBroadcaster.h"; sourceTree = "SOURCE_ROOT"; };
+		4693A834FF8117902FFF4D3F = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_String.h"; path = "../../../juce/modules/juce_core/text/juce_String.h"; sourceTree = "SOURCE_ROOT"; };
+		4722FD5DDEA12074E23626F6 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_ChildProcess.cpp"; path = "../../../juce/modules/juce_core/threads/juce_ChildProcess.cpp"; sourceTree = "SOURCE_ROOT"; };
+		47AFF0730B6F48B44D9AF21F = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_RelativeTime.h"; path = "../../../juce/modules/juce_core/time/juce_RelativeTime.h"; sourceTree = "SOURCE_ROOT"; };
+		47C9F191ACA30DA8866F8162 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_BufferingAudioSource.h"; path = "../../../juce/modules/juce_audio_basics/sources/juce_BufferingAudioSource.h"; sourceTree = "SOURCE_ROOT"; };
+		482B36C5234DB4CED73506B4 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_Timer.cpp"; path = "../../../juce/modules/juce_events/timers/juce_Timer.cpp"; sourceTree = "SOURCE_ROOT"; };
+		486B2D4FE311DCB82E37DF60 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_AudioDataConverters.cpp"; path = "../../../juce/modules/juce_audio_basics/buffers/juce_AudioDataConverters.cpp"; sourceTree = "SOURCE_ROOT"; };
+		4881596CF3AA3B6809FD95FB = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_linux_CommonFile.cpp"; path = "../../../juce/modules/juce_core/native/juce_linux_CommonFile.cpp"; sourceTree = "SOURCE_ROOT"; };
+		48D74A7F2C32E5ED9838F5FE = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_TemporaryFile.h"; path = "../../../juce/modules/juce_core/files/juce_TemporaryFile.h"; sourceTree = "SOURCE_ROOT"; };
+		4953D82D7A84A83E3E7572A3 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_ActionBroadcaster.cpp"; path = "../../../juce/modules/juce_events/broadcasters/juce_ActionBroadcaster.cpp"; sourceTree = "SOURCE_ROOT"; };
+		49745E79E63C5B9BC9A57AB1 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; name = "juce_mac_Windowing.mm"; path = "../../../juce/modules/juce_gui_basics/native/juce_mac_Windowing.mm"; sourceTree = "SOURCE_ROOT"; };
+		4A0BF982E2E874D2FEC6073A = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_OpenGLHelpers.cpp"; path = "../../../juce/modules/juce_opengl/opengl/juce_OpenGLHelpers.cpp"; sourceTree = "SOURCE_ROOT"; };
+		4A136908B5FF7712EBAD9981 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_Application.h"; path = "../../../juce/modules/juce_gui_basics/application/juce_Application.h"; sourceTree = "SOURCE_ROOT"; };
+		4A2C4C9905C7DBE7BD7A0A7D = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_CallOutBox.h"; path = "../../../juce/modules/juce_gui_basics/windows/juce_CallOutBox.h"; sourceTree = "SOURCE_ROOT"; };
+		4A75AF43CD5BD452E2B322A0 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_ProgressBar.cpp"; path = "../../../juce/modules/juce_gui_basics/widgets/juce_ProgressBar.cpp"; sourceTree = "SOURCE_ROOT"; };
+		4AA95F2942C46275D0CC1206 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_Button.cpp"; path = "../../../juce/modules/juce_gui_basics/buttons/juce_Button.cpp"; sourceTree = "SOURCE_ROOT"; };
+		4AD0E0592C453AF63D48A792 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = TimerNode.h; path = ../../Source/Utility/TimerNode.h; sourceTree = "SOURCE_ROOT"; };
+		4AF53AC364CD4D92FA9C66B1 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = RawSensorDisplay.h; path = ../../Source/Display/RawSensorDisplay.h; sourceTree = "SOURCE_ROOT"; };
+		4B5B59C3EB40E81B0EB3FF2A = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = Scheduler.h; path = ../../Source/Utility/Scheduler.h; sourceTree = "SOURCE_ROOT"; };
+		4B6DD49A71F451207F114891 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_MenuBarComponent.cpp"; path = "../../../juce/modules/juce_gui_basics/menus/juce_MenuBarComponent.cpp"; sourceTree = "SOURCE_ROOT"; };
+		4B9F0E9662F5974FEB6D3EA6 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_StringRef.h"; path = "../../../juce/modules/juce_core/text/juce_StringRef.h"; sourceTree = "SOURCE_ROOT"; };
+		4BA89F5127922FC8A0D88602 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_ReverbAudioSource.cpp"; path = "../../../juce/modules/juce_audio_basics/sources/juce_ReverbAudioSource.cpp"; sourceTree = "SOURCE_ROOT"; };
+		4BF12D55902ABB1FC8FC63AF = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_FileInputStream.h"; path = "../../../juce/modules/juce_core/files/juce_FileInputStream.h"; sourceTree = "SOURCE_ROOT"; };
+		4BFF669E2B855A7A576E8CE2 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = Trigger.cpp; path = ../../Source/Utility/Trigger.cpp; sourceTree = "SOURCE_ROOT"; };
+		4C27098692905309308ADA65 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = KeyIdleDetector.h; path = ../../Source/TouchKeys/KeyIdleDetector.h; sourceTree = "SOURCE_ROOT"; };
+		4C8A9DE41142589CE8A66D22 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_DrawableImage.h"; path = "../../../juce/modules/juce_gui_basics/drawables/juce_DrawableImage.h"; sourceTree = "SOURCE_ROOT"; };
+		4CAF6AD5590B3C8C9D2DCA10 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_RelativeParallelogram.h"; path = "../../../juce/modules/juce_gui_basics/positioning/juce_RelativeParallelogram.h"; sourceTree = "SOURCE_ROOT"; };
+		4D3486477C93D169F9841EBD = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_ArrowButton.h"; path = "../../../juce/modules/juce_gui_basics/buttons/juce_ArrowButton.h"; sourceTree = "SOURCE_ROOT"; };
+		4D6A4F688249F5B876849207 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; name = "juce_mac_MessageManager.mm"; path = "../../../juce/modules/juce_events/native/juce_mac_MessageManager.mm"; sourceTree = "SOURCE_ROOT"; };
+		4E546FBB3A7626EFD573D41D = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_ComponentPeer.h"; path = "../../../juce/modules/juce_gui_basics/windows/juce_ComponentPeer.h"; sourceTree = "SOURCE_ROOT"; };
+		4E7E0F0BE092AB6743B03D43 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_MidiFile.h"; path = "../../../juce/modules/juce_audio_basics/midi/juce_MidiFile.h"; sourceTree = "SOURCE_ROOT"; };
+		4E88EE61D4328B092B1F0BB2 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_SortedSet.h"; path = "../../../juce/modules/juce_core/containers/juce_SortedSet.h"; sourceTree = "SOURCE_ROOT"; };
+		4EA0B74126E80DFAA6075D51 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_Logger.cpp"; path = "../../../juce/modules/juce_core/logging/juce_Logger.cpp"; sourceTree = "SOURCE_ROOT"; };
+		4EF55CBE453A2F06A5880E6C = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_MP3AudioFormat.cpp"; path = "../../../juce/modules/juce_audio_formats/codecs/juce_MP3AudioFormat.cpp"; sourceTree = "SOURCE_ROOT"; };
+		4F0FBB923DC40FD7859CA9B6 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_Time.h"; path = "../../../juce/modules/juce_core/time/juce_Time.h"; sourceTree = "SOURCE_ROOT"; };
+		4F63CAE984068E8493AB1C59 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_StretchableLayoutResizerBar.h"; path = "../../../juce/modules/juce_gui_basics/layout/juce_StretchableLayoutResizerBar.h"; sourceTree = "SOURCE_ROOT"; };
+		4FAEFEF7315CD4697A3A4A5F = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_Button.h"; path = "../../../juce/modules/juce_gui_basics/buttons/juce_Button.h"; sourceTree = "SOURCE_ROOT"; };
+		4FF0EE53340B306B09230CAB = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_CPlusPlusCodeTokeniser.h"; path = "../../../juce/modules/juce_gui_extra/code_editor/juce_CPlusPlusCodeTokeniser.h"; sourceTree = "SOURCE_ROOT"; };
+		500B161A0A390BE6A7AC6E67 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_Viewport.cpp"; path = "../../../juce/modules/juce_gui_basics/layout/juce_Viewport.cpp"; sourceTree = "SOURCE_ROOT"; };
+		5092DF5BA82449512AAA7D7B = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_android_SystemStats.cpp"; path = "../../../juce/modules/juce_core/native/juce_android_SystemStats.cpp"; sourceTree = "SOURCE_ROOT"; };
+		50B10AEABDF8CFDB4C1F5198 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_ToolbarItemPalette.cpp"; path = "../../../juce/modules/juce_gui_basics/widgets/juce_ToolbarItemPalette.cpp"; sourceTree = "SOURCE_ROOT"; };
+		50FBD249C6C5C1B4DBA459E6 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_CharPointer_UTF16.h"; path = "../../../juce/modules/juce_core/text/juce_CharPointer_UTF16.h"; sourceTree = "SOURCE_ROOT"; };
+		51824FD31518891951469CBA = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_Font.h"; path = "../../../juce/modules/juce_graphics/fonts/juce_Font.h"; sourceTree = "SOURCE_ROOT"; };
+		52503B83C3EB95429DBCF2B8 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_Random.h"; path = "../../../juce/modules/juce_core/maths/juce_Random.h"; sourceTree = "SOURCE_ROOT"; };
+		52FC4A84B75D864E89E3AB5B = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_Drawable.h"; path = "../../../juce/modules/juce_gui_basics/drawables/juce_Drawable.h"; sourceTree = "SOURCE_ROOT"; };
+		53297CAD72BBB338874C593F = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_win32_AudioCDReader.cpp"; path = "../../../juce/modules/juce_audio_devices/native/juce_win32_AudioCDReader.cpp"; sourceTree = "SOURCE_ROOT"; };
+		53C317A2BC085C625C1ABB49 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_linux_Midi.cpp"; path = "../../../juce/modules/juce_audio_devices/native/juce_linux_Midi.cpp"; sourceTree = "SOURCE_ROOT"; };
+		542B60D386C30F602E8F37C7 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_ToolbarItemComponent.cpp"; path = "../../../juce/modules/juce_gui_basics/widgets/juce_ToolbarItemComponent.cpp"; sourceTree = "SOURCE_ROOT"; };
+		547C4147F20B76FD1596DF3B = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_StringPairArray.h"; path = "../../../juce/modules/juce_core/text/juce_StringPairArray.h"; sourceTree = "SOURCE_ROOT"; };
+		54C9DB3C192D2E3C4BB796F6 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_RenderingHelpers.h"; path = "../../../juce/modules/juce_graphics/native/juce_RenderingHelpers.h"; sourceTree = "SOURCE_ROOT"; };
+		55010ADB4310C9FF2CD11D52 = { isa = PBXFileReference; lastKnownFileType = file.icns; name = Icon.icns; path = Icon.icns; sourceTree = "SOURCE_ROOT"; };
+		5535E1D2AB0D896D5EA1528C = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_GroupComponent.cpp"; path = "../../../juce/modules/juce_gui_basics/layout/juce_GroupComponent.cpp"; sourceTree = "SOURCE_ROOT"; };
+		55614439CEA2AA4C3C83960C = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_Font.cpp"; path = "../../../juce/modules/juce_graphics/fonts/juce_Font.cpp"; sourceTree = "SOURCE_ROOT"; };
+		558764F1D4A196E74A8B4D47 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_TextLayout.h"; path = "../../../juce/modules/juce_graphics/fonts/juce_TextLayout.h"; sourceTree = "SOURCE_ROOT"; };
+		55E99E81FC362EA304FEFC7D = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = PianoKeyCalibrator.h; path = ../../Source/TouchKeys/PianoKeyCalibrator.h; sourceTree = "SOURCE_ROOT"; };
+		560BA4677563ADBFEAA4A9A8 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_BasicNativeHeaders.h"; path = "../../../juce/modules/juce_core/native/juce_BasicNativeHeaders.h"; sourceTree = "SOURCE_ROOT"; };
+		564725F265FA251EA1E85EFE = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_ZipFile.cpp"; path = "../../../juce/modules/juce_core/zip/juce_ZipFile.cpp"; sourceTree = "SOURCE_ROOT"; };
+		56EF3900C63C00BED0E574D0 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = TouchkeyKeyDivisionMappingFactory.cpp; path = ../../Source/Mappings/KeyDivision/TouchkeyKeyDivisionMappingFactory.cpp; sourceTree = "SOURCE_ROOT"; };
+		57370DDA62616114166E89B2 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_Image.cpp"; path = "../../../juce/modules/juce_graphics/images/juce_Image.cpp"; sourceTree = "SOURCE_ROOT"; };
+		577688014EC968E5BB50E11B = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_KeyPressMappingSet.h"; path = "../../../juce/modules/juce_gui_basics/commands/juce_KeyPressMappingSet.h"; sourceTree = "SOURCE_ROOT"; };
+		57E7CFA95F5E9A5C31E0706A = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_Synthesiser.h"; path = "../../../juce/modules/juce_audio_basics/synthesisers/juce_Synthesiser.h"; sourceTree = "SOURCE_ROOT"; };
+		585AE1E2582CD19A8F9FE2D2 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_DropShadower.h"; path = "../../../juce/modules/juce_gui_basics/misc/juce_DropShadower.h"; sourceTree = "SOURCE_ROOT"; };
+		586FCB50DD095947B527E462 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; name = "juce_mac_Network.mm"; path = "../../../juce/modules/juce_core/native/juce_mac_Network.mm"; sourceTree = "SOURCE_ROOT"; };
+		588647ADF4EEADF6DD8C573F = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_linux_Fonts.cpp"; path = "../../../juce/modules/juce_graphics/native/juce_linux_Fonts.cpp"; sourceTree = "SOURCE_ROOT"; };
+		58E550A1B386C2A56A4E5205 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_android_Misc.cpp"; path = "../../../juce/modules/juce_core/native/juce_android_Misc.cpp"; sourceTree = "SOURCE_ROOT"; };
+		595D9603C966DD7A96601770 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_ChangeListener.h"; path = "../../../juce/modules/juce_events/broadcasters/juce_ChangeListener.h"; sourceTree = "SOURCE_ROOT"; };
+		596161CC0A4A8DD05A368B32 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_android_Fonts.cpp"; path = "../../../juce/modules/juce_graphics/native/juce_android_Fonts.cpp"; sourceTree = "SOURCE_ROOT"; };
+		5A557C5800B7FD7879B89A9A = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_MathsFunctions.h"; path = "../../../juce/modules/juce_core/maths/juce_MathsFunctions.h"; sourceTree = "SOURCE_ROOT"; };
+		5A740A5459309CC1144016FD = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_DeletedAtShutdown.h"; path = "../../../juce/modules/juce_events/messages/juce_DeletedAtShutdown.h"; sourceTree = "SOURCE_ROOT"; };
+		5A7A1022A19D6DD162029AA6 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = LogPlayback.cpp; path = ../../Source/TouchKeys/LogPlayback.cpp; sourceTree = "SOURCE_ROOT"; };
+		5AA0C6F1D0EEB64916D79A1F = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_ColourGradient.cpp"; path = "../../../juce/modules/juce_graphics/colour/juce_ColourGradient.cpp"; sourceTree = "SOURCE_ROOT"; };
+		5AA900E1C54219C4371B3907 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_Range.h"; path = "../../../juce/modules/juce_core/maths/juce_Range.h"; sourceTree = "SOURCE_ROOT"; };
+		5AAE89A72D91459EB9548A5D = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_DrawableRectangle.cpp"; path = "../../../juce/modules/juce_gui_basics/drawables/juce_DrawableRectangle.cpp"; sourceTree = "SOURCE_ROOT"; };
+		5AC453F4B30AB8DA51B8B9B0 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_ImagePreviewComponent.h"; path = "../../../juce/modules/juce_gui_basics/filebrowser/juce_ImagePreviewComponent.h"; sourceTree = "SOURCE_ROOT"; };
+		5B03F93B4E808E5B42C5F984 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_UndoManager.cpp"; path = "../../../juce/modules/juce_data_structures/undomanager/juce_UndoManager.cpp"; sourceTree = "SOURCE_ROOT"; };
+		5B1EA934207F64017DAEB18F = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_AudioCDBurner.h"; path = "../../../juce/modules/juce_audio_devices/audio_cd/juce_AudioCDBurner.h"; sourceTree = "SOURCE_ROOT"; };
+		5B83D18A4AF71418DA5F54F4 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_RelativeParallelogram.cpp"; path = "../../../juce/modules/juce_gui_basics/positioning/juce_RelativeParallelogram.cpp"; sourceTree = "SOURCE_ROOT"; };
+		5B851A07868E688F0C765CA1 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_ThreadLocalValue.h"; path = "../../../juce/modules/juce_core/threads/juce_ThreadLocalValue.h"; sourceTree = "SOURCE_ROOT"; };
+		5BC3C1BA534425DE943BCA22 = { isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = System/Library/Frameworks/Cocoa.framework; sourceTree = SDKROOT; };
+		5BDDEB249D4D04F0DAAA97B6 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_FillType.cpp"; path = "../../../juce/modules/juce_graphics/colour/juce_FillType.cpp"; sourceTree = "SOURCE_ROOT"; };
+		5BDFD343F8067BB49E2C2125 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_MemoryBlock.h"; path = "../../../juce/modules/juce_core/memory/juce_MemoryBlock.h"; sourceTree = "SOURCE_ROOT"; };
+		5C085DEDBB1A8F755F800922 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = TouchkeyBaseMappingFactory.h; path = ../../Source/Mappings/TouchkeyBaseMappingFactory.h; sourceTree = "SOURCE_ROOT"; };
+		5CD67ECB4B41375D889C1A2F = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = PianoKey.h; path = ../../Source/TouchKeys/PianoKey.h; sourceTree = "SOURCE_ROOT"; };
+		5CFC24DCE2459CF5A80FD766 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_ReferenceCountedObject.h"; path = "../../../juce/modules/juce_core/memory/juce_ReferenceCountedObject.h"; sourceTree = "SOURCE_ROOT"; };
+		5D1750119D2E86324ED67585 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_MessageListener.h"; path = "../../../juce/modules/juce_events/messages/juce_MessageListener.h"; sourceTree = "SOURCE_ROOT"; };
+		5D2E40E321F1A2340FB42A05 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; name = "juce_data_structures.mm"; path = "../../../juce/modules/juce_data_structures/juce_data_structures.mm"; sourceTree = "SOURCE_ROOT"; };
+		5D5300842AF71C49D1E5EDF5 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_AnimatedPosition.h"; path = "../../../juce/modules/juce_gui_basics/layout/juce_AnimatedPosition.h"; sourceTree = "SOURCE_ROOT"; };
+		5D9DBA8EC90E8161918F12E0 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_TableListBox.h"; path = "../../../juce/modules/juce_gui_basics/widgets/juce_TableListBox.h"; sourceTree = "SOURCE_ROOT"; };
+		5DE5675862714BFEFF231027 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = TouchkeyReleaseAngleMapping.cpp; path = ../../Source/Mappings/ReleaseAngle/TouchkeyReleaseAngleMapping.cpp; sourceTree = "SOURCE_ROOT"; };
+		5DFC9E79432F0E8B0245B05E = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_FileChooser.cpp"; path = "../../../juce/modules/juce_gui_basics/filebrowser/juce_FileChooser.cpp"; sourceTree = "SOURCE_ROOT"; };
+		5E21284BD5A78CCF9533A7CB = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_DynamicObject.h"; path = "../../../juce/modules/juce_core/containers/juce_DynamicObject.h"; sourceTree = "SOURCE_ROOT"; };
+		5E2C8035BF527680811D1CF5 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_RecentlyOpenedFilesList.cpp"; path = "../../../juce/modules/juce_gui_extra/misc/juce_RecentlyOpenedFilesList.cpp"; sourceTree = "SOURCE_ROOT"; };
+		5E4F05C1C0E47ABAD690E924 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_InterprocessConnection.h"; path = "../../../juce/modules/juce_events/interprocess/juce_InterprocessConnection.h"; sourceTree = "SOURCE_ROOT"; };
+		5E9303619B553ED5D8014075 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_linux_Windowing.cpp"; path = "../../../juce/modules/juce_gui_basics/native/juce_linux_Windowing.cpp"; sourceTree = "SOURCE_ROOT"; };
+		5EDDEE17AD20B0C75DF6DF12 = { isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = System/Library/Frameworks/WebKit.framework; sourceTree = SDKROOT; };
+		5F46502A5D1A44BD738467D4 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_TreeView.cpp"; path = "../../../juce/modules/juce_gui_basics/widgets/juce_TreeView.cpp"; sourceTree = "SOURCE_ROOT"; };
+		5F9AD4578F56B8BEC2E311C3 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_linux_ALSA.cpp"; path = "../../../juce/modules/juce_audio_devices/native/juce_linux_ALSA.cpp"; sourceTree = "SOURCE_ROOT"; };
+		5FBE2E2D25174AF84CAB065C = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; name = "juce_events.mm"; path = "../../../juce/modules/juce_events/juce_events.mm"; sourceTree = "SOURCE_ROOT"; };
+		601196F609721F1C314F9F50 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_MouseCursor.cpp"; path = "../../../juce/modules/juce_gui_basics/mouse/juce_MouseCursor.cpp"; sourceTree = "SOURCE_ROOT"; };
+		601CBE152587954A406ED25F = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_SubregionStream.cpp"; path = "../../../juce/modules/juce_core/streams/juce_SubregionStream.cpp"; sourceTree = "SOURCE_ROOT"; };
+		606F2538F89C451EC8BB479D = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = MappingScheduler.h; path = ../../Source/Mappings/MappingScheduler.h; sourceTree = "SOURCE_ROOT"; };
+		608D7036802547CACA0C8EB9 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_HashMap.h"; path = "../../../juce/modules/juce_core/containers/juce_HashMap.h"; sourceTree = "SOURCE_ROOT"; };
+		6099FF0A5378073D75F68649 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_AlertWindow.h"; path = "../../../juce/modules/juce_gui_basics/windows/juce_AlertWindow.h"; sourceTree = "SOURCE_ROOT"; };
+		610013306039A681BE00CDF4 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_DeletedAtShutdown.cpp"; path = "../../../juce/modules/juce_events/messages/juce_DeletedAtShutdown.cpp"; sourceTree = "SOURCE_ROOT"; };
+		61A7ED9BEB8538FEE656E782 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = MIDIKeyPositionMapping.cpp; path = ../../Source/Mappings/MIDIKeyPositionMapping.cpp; sourceTree = "SOURCE_ROOT"; };
+		62A053A579D81CD2C6171B4E = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_linux_JackAudio.cpp"; path = "../../../juce/modules/juce_audio_devices/native/juce_linux_JackAudio.cpp"; sourceTree = "SOURCE_ROOT"; };
+		62A0804DA5C8D8F076538D7D = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_FileInputSource.h"; path = "../../../juce/modules/juce_core/streams/juce_FileInputSource.h"; sourceTree = "SOURCE_ROOT"; };
+		62F2282824CD10B6F82A703F = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_MouseListener.h"; path = "../../../juce/modules/juce_gui_basics/mouse/juce_MouseListener.h"; sourceTree = "SOURCE_ROOT"; };
+		63271E5A2866BBC8014C76F6 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_ValueTree.cpp"; path = "../../../juce/modules/juce_data_structures/values/juce_ValueTree.cpp"; sourceTree = "SOURCE_ROOT"; };
+		6348A0AF83CD2B08A0B0F867 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_OpenGLFrameBuffer.h"; path = "../../../juce/modules/juce_opengl/opengl/juce_OpenGLFrameBuffer.h"; sourceTree = "SOURCE_ROOT"; };
+		638AC9A213FFAFC1647D3C78 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = Main.cpp; path = ../../Source/Main.cpp; sourceTree = "SOURCE_ROOT"; };
+		638FEE22F7BC4DEE41443DC3 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_LowLevelGraphicsSoftwareRenderer.h"; path = "../../../juce/modules/juce_graphics/contexts/juce_LowLevelGraphicsSoftwareRenderer.h"; sourceTree = "SOURCE_ROOT"; };
+		6395794A858E7036D062579C = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_Decibels.h"; path = "../../../juce/modules/juce_audio_basics/effects/juce_Decibels.h"; sourceTree = "SOURCE_ROOT"; };
+		639B0783D7C0F63CACBC8616 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_ImageFileFormat.cpp"; path = "../../../juce/modules/juce_graphics/images/juce_ImageFileFormat.cpp"; sourceTree = "SOURCE_ROOT"; };
+		63BF081465ACDD86B21207F5 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_TabbedComponent.h"; path = "../../../juce/modules/juce_gui_basics/layout/juce_TabbedComponent.h"; sourceTree = "SOURCE_ROOT"; };
+		63E32F29B1D3528BDDE0B72A = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_WeakReference.h"; path = "../../../juce/modules/juce_core/memory/juce_WeakReference.h"; sourceTree = "SOURCE_ROOT"; };
+		63FB3D8BC053ED771331E303 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_ChangeBroadcaster.cpp"; path = "../../../juce/modules/juce_events/broadcasters/juce_ChangeBroadcaster.cpp"; sourceTree = "SOURCE_ROOT"; };
+		641DCDEE4366C19C302BCDC7 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_PixelFormats.h"; path = "../../../juce/modules/juce_graphics/colour/juce_PixelFormats.h"; sourceTree = "SOURCE_ROOT"; };
+		647A3C5CF4D3DE9C169861D1 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_FileListComponent.h"; path = "../../../juce/modules/juce_gui_basics/filebrowser/juce_FileListComponent.h"; sourceTree = "SOURCE_ROOT"; };
+		64C0F5A52B61A2F5A8689840 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_mac_CoreMidi.cpp"; path = "../../../juce/modules/juce_audio_devices/native/juce_mac_CoreMidi.cpp"; sourceTree = "SOURCE_ROOT"; };
+		6500D22CD08FD736625BA949 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_SliderPropertyComponent.h"; path = "../../../juce/modules/juce_gui_basics/properties/juce_SliderPropertyComponent.h"; sourceTree = "SOURCE_ROOT"; };
+		65851006CAF520CAAC0F81B7 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_QuickTimeAudioFormat.h"; path = "../../../juce/modules/juce_audio_formats/codecs/juce_QuickTimeAudioFormat.h"; sourceTree = "SOURCE_ROOT"; };
+		65D27B3F8F17B07DCC171F1C = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_audio_formats.h"; path = "../../../juce/modules/juce_audio_formats/juce_audio_formats.h"; sourceTree = "SOURCE_ROOT"; };
+		6604B7074C60D16C2958058B = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_win32_Files.cpp"; path = "../../../juce/modules/juce_core/native/juce_win32_Files.cpp"; sourceTree = "SOURCE_ROOT"; };
+		6606A45FBF92643F83F78021 = { isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = IOKit.framework; path = System/Library/Frameworks/IOKit.framework; sourceTree = SDKROOT; };
+		6610029938CB08266FAD5120 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_ApplicationCommandManager.cpp"; path = "../../../juce/modules/juce_gui_basics/commands/juce_ApplicationCommandManager.cpp"; sourceTree = "SOURCE_ROOT"; };
+		66F5603A6C984CEFD02530D3 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_SystemTrayIconComponent.h"; path = "../../../juce/modules/juce_gui_extra/misc/juce_SystemTrayIconComponent.h"; sourceTree = "SOURCE_ROOT"; };
+		673187B7CDEE9090F0503F45 = { isa = PBXFileReference; lastKnownFileType = file; name = "juce_module_info"; path = "../../../juce/modules/juce_audio_formats/juce_module_info"; sourceTree = "SOURCE_ROOT"; };
+		67B1EA88315E60BDDEF78A9B = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = PianoPedal.cpp; path = ../../Source/TouchKeys/PianoPedal.cpp; sourceTree = "SOURCE_ROOT"; };
+		67FBBBE888B8FF071E9A6E9B = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_MenuBarModel.h"; path = "../../../juce/modules/juce_gui_basics/menus/juce_MenuBarModel.h"; sourceTree = "SOURCE_ROOT"; };
+		684832C67BAC9A135AD0F420 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_GZIPCompressorOutputStream.h"; path = "../../../juce/modules/juce_core/zip/juce_GZIPCompressorOutputStream.h"; sourceTree = "SOURCE_ROOT"; };
+		688D8F39D56A47170508B993 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_MidiBuffer.cpp"; path = "../../../juce/modules/juce_audio_basics/midi/juce_MidiBuffer.cpp"; sourceTree = "SOURCE_ROOT"; };
+		68A9E6D3AA5B1E80308A5400 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = MidiKeyboardSegment.cpp; path = ../../Source/TouchKeys/MidiKeyboardSegment.cpp; sourceTree = "SOURCE_ROOT"; };
+		68B7DB3155ECA7DF6A79D5EC = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_OwnedArray.h"; path = "../../../juce/modules/juce_core/containers/juce_OwnedArray.h"; sourceTree = "SOURCE_ROOT"; };
+		68EC729C279BCAD684AEBF9C = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_ComponentAnimator.cpp"; path = "../../../juce/modules/juce_gui_basics/layout/juce_ComponentAnimator.cpp"; sourceTree = "SOURCE_ROOT"; };
+		68F90A1AC81BD36F4AFCA49D = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_FlacAudioFormat.h"; path = "../../../juce/modules/juce_audio_formats/codecs/juce_FlacAudioFormat.h"; sourceTree = "SOURCE_ROOT"; };
+		692CFED8D5A615CCE73120FB = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = TouchkeyVibratoMappingShortEditor.h; path = ../../Source/Mappings/Vibrato/TouchkeyVibratoMappingShortEditor.h; sourceTree = "SOURCE_ROOT"; };
+		6A14D41C2C4B1022DFCFDEC9 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_win32_Direct2DGraphicsContext.cpp"; path = "../../../juce/modules/juce_graphics/native/juce_win32_Direct2DGraphicsContext.cpp"; sourceTree = "SOURCE_ROOT"; };
+		6A2C76C28ADEBBEB65637ED4 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_AlertWindow.cpp"; path = "../../../juce/modules/juce_gui_basics/windows/juce_AlertWindow.cpp"; sourceTree = "SOURCE_ROOT"; };
+		6A58296B30F95486340DFD85 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_android_Messaging.cpp"; path = "../../../juce/modules/juce_events/native/juce_android_Messaging.cpp"; sourceTree = "SOURCE_ROOT"; };
+		6A59FD6927F7863EA627B22B = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_Javascript.cpp"; path = "../../../juce/modules/juce_core/javascript/juce_Javascript.cpp"; sourceTree = "SOURCE_ROOT"; };
+		6ABB704327024442CCD18ADD = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_CodeEditorComponent.cpp"; path = "../../../juce/modules/juce_gui_extra/code_editor/juce_CodeEditorComponent.cpp"; sourceTree = "SOURCE_ROOT"; };
+		6B4AAA40F2D016E163663316 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_ResizableCornerComponent.h"; path = "../../../juce/modules/juce_gui_basics/layout/juce_ResizableCornerComponent.h"; sourceTree = "SOURCE_ROOT"; };
+		6B4D53A3F41AB62FA997F22E = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_PropertyComponent.cpp"; path = "../../../juce/modules/juce_gui_basics/properties/juce_PropertyComponent.cpp"; sourceTree = "SOURCE_ROOT"; };
+		6B502138661E9AB628F374F6 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_ComboBox.h"; path = "../../../juce/modules/juce_gui_basics/widgets/juce_ComboBox.h"; sourceTree = "SOURCE_ROOT"; };
+		6B53EFC11817616594E2D9BB = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_LowLevelGraphicsSoftwareRenderer.cpp"; path = "../../../juce/modules/juce_graphics/contexts/juce_LowLevelGraphicsSoftwareRenderer.cpp"; sourceTree = "SOURCE_ROOT"; };
+		6B6237A52DA8FB843E47CE0A = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_DocumentWindow.h"; path = "../../../juce/modules/juce_gui_basics/windows/juce_DocumentWindow.h"; sourceTree = "SOURCE_ROOT"; };
+		6BBD376C70ED0BF7DB547474 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_MemoryMappedAudioFormatReader.h"; path = "../../../juce/modules/juce_audio_formats/format/juce_MemoryMappedAudioFormatReader.h"; sourceTree = "SOURCE_ROOT"; };
+		6C119011A8A9B7A6F092F10D = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_FileLogger.h"; path = "../../../juce/modules/juce_core/logging/juce_FileLogger.h"; sourceTree = "SOURCE_ROOT"; };
+		6C1F7220FB3D51C3E401670D = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_BufferingAudioFormatReader.h"; path = "../../../juce/modules/juce_audio_formats/format/juce_BufferingAudioFormatReader.h"; sourceTree = "SOURCE_ROOT"; };
+		6CA0CAA72433DDE5E6E2BA99 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_ResizableWindow.cpp"; path = "../../../juce/modules/juce_gui_basics/windows/juce_ResizableWindow.cpp"; sourceTree = "SOURCE_ROOT"; };
+		6CD813F180C163518C623E8C = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; name = "juce_mac_CoreGraphicsContext.mm"; path = "../../../juce/modules/juce_graphics/native/juce_mac_CoreGraphicsContext.mm"; sourceTree = "SOURCE_ROOT"; };
+		6CF3B1703B8D1E75091098B0 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_KeyboardFocusTraverser.cpp"; path = "../../../juce/modules/juce_gui_basics/keyboard/juce_KeyboardFocusTraverser.cpp"; sourceTree = "SOURCE_ROOT"; };
+		6CF6DF68577964FD68EFB132 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_OpenGLContext.cpp"; path = "../../../juce/modules/juce_opengl/opengl/juce_OpenGLContext.cpp"; sourceTree = "SOURCE_ROOT"; };
+		6D365652129D06390251A09D = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_OpenGL_linux.h"; path = "../../../juce/modules/juce_opengl/native/juce_OpenGL_linux.h"; sourceTree = "SOURCE_ROOT"; };
+		6D7B40AF156B7244E6D37851 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = LogPlayback.h; path = ../../Source/TouchKeys/LogPlayback.h; sourceTree = "SOURCE_ROOT"; };
+		6DF85AFF4681188E85089BF5 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_AudioDeviceManager.cpp"; path = "../../../juce/modules/juce_audio_devices/audio_io/juce_AudioDeviceManager.cpp"; sourceTree = "SOURCE_ROOT"; };
+		6EE034FF03FFA7308A5766AC = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_SVGParser.cpp"; path = "../../../juce/modules/juce_gui_basics/drawables/juce_SVGParser.cpp"; sourceTree = "SOURCE_ROOT"; };
+		6EF7B06580A10399CCD7BD5F = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; name = "juce_ios_MessageManager.mm"; path = "../../../juce/modules/juce_events/native/juce_ios_MessageManager.mm"; sourceTree = "SOURCE_ROOT"; };
+		6F7A2A1DF08F35D772F72DA9 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; name = "juce_mac_AudioCDReader.mm"; path = "../../../juce/modules/juce_audio_devices/native/juce_mac_AudioCDReader.mm"; sourceTree = "SOURCE_ROOT"; };
+		6F9A5F79F66D8B56E1B62B9B = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_AudioDeviceManager.h"; path = "../../../juce/modules/juce_audio_devices/audio_io/juce_AudioDeviceManager.h"; sourceTree = "SOURCE_ROOT"; };
+		70007C0A1E082ED1349FD91B = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_Typeface.h"; path = "../../../juce/modules/juce_graphics/fonts/juce_Typeface.h"; sourceTree = "SOURCE_ROOT"; };
+		7036D606B54801EF8B582B14 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_MultiDocumentPanel.h"; path = "../../../juce/modules/juce_gui_basics/layout/juce_MultiDocumentPanel.h"; sourceTree = "SOURCE_ROOT"; };
+		707B322BFF21635B171475AE = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_posix_SharedCode.h"; path = "../../../juce/modules/juce_core/native/juce_posix_SharedCode.h"; sourceTree = "SOURCE_ROOT"; };
+		70D03CB09823072EB0FECD50 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_android_Audio.cpp"; path = "../../../juce/modules/juce_audio_devices/native/juce_android_Audio.cpp"; sourceTree = "SOURCE_ROOT"; };
+		70DF91DDFADBF8A69040904F = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_MidiMessage.h"; path = "../../../juce/modules/juce_audio_basics/midi/juce_MidiMessage.h"; sourceTree = "SOURCE_ROOT"; };
+		70E7379DBBD8C42D6DF053AE = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_AudioFormatReaderSource.cpp"; path = "../../../juce/modules/juce_audio_formats/format/juce_AudioFormatReaderSource.cpp"; sourceTree = "SOURCE_ROOT"; };
+		7103BB75D00938B3DEF6F943 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = Node.h; path = ../../Source/Utility/Node.h; sourceTree = "SOURCE_ROOT"; };
+		7172E38F5AA96EB0063A67AC = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_DropShadower.cpp"; path = "../../../juce/modules/juce_gui_basics/misc/juce_DropShadower.cpp"; sourceTree = "SOURCE_ROOT"; };
+		71D4D37B1DBDF821E57F68F3 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_GraphicsContext.cpp"; path = "../../../juce/modules/juce_graphics/contexts/juce_GraphicsContext.cpp"; sourceTree = "SOURCE_ROOT"; };
+		724ABFBF59674B3C9B5C2437 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = TouchkeyKeyDivisionMapping.h; path = ../../Source/Mappings/KeyDivision/TouchkeyKeyDivisionMapping.h; sourceTree = "SOURCE_ROOT"; };
+		724CC1B7FF7ECEC8F25400AF = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_RectangleList.h"; path = "../../../juce/modules/juce_graphics/geometry/juce_RectangleList.h"; sourceTree = "SOURCE_ROOT"; };
+		725AA29F72430257825A0E8B = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = KeyPositionTracker.h; path = ../../Source/TouchKeys/KeyPositionTracker.h; sourceTree = "SOURCE_ROOT"; };
+		726E5869DFBD1AE70FB39109 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_TextPropertyComponent.cpp"; path = "../../../juce/modules/juce_gui_basics/properties/juce_TextPropertyComponent.cpp"; sourceTree = "SOURCE_ROOT"; };
+		72972DBFFD3B26C1A933CD0F = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_android_JNIHelpers.h"; path = "../../../juce/modules/juce_core/native/juce_android_JNIHelpers.h"; sourceTree = "SOURCE_ROOT"; };
+		72D026BC20AD5DA4743D6A68 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_MemoryMappedFile.h"; path = "../../../juce/modules/juce_core/files/juce_MemoryMappedFile.h"; sourceTree = "SOURCE_ROOT"; };
+		72DEAE1CFA0E4495953E162B = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_ApplicationCommandInfo.h"; path = "../../../juce/modules/juce_gui_basics/commands/juce_ApplicationCommandInfo.h"; sourceTree = "SOURCE_ROOT"; };
+		7362738354A361AC4913C252 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_MultiDocumentPanel.cpp"; path = "../../../juce/modules/juce_gui_basics/layout/juce_MultiDocumentPanel.cpp"; sourceTree = "SOURCE_ROOT"; };
+		74367CCDFA812324602A8232 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_Colours.cpp"; path = "../../../juce/modules/juce_graphics/colour/juce_Colours.cpp"; sourceTree = "SOURCE_ROOT"; };
+		743847CB7587F825B89E6FE8 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = JuceHeader.h; path = ../../JuceLibraryCode/JuceHeader.h; sourceTree = "SOURCE_ROOT"; };
+		74A7CB1DEB75A34CDD0CD4C4 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_MessageListener.cpp"; path = "../../../juce/modules/juce_events/messages/juce_MessageListener.cpp"; sourceTree = "SOURCE_ROOT"; };
+		7512B86BFA154BD74FC8AC07 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = Accumulator.h; path = ../../Source/Utility/Accumulator.h; sourceTree = "SOURCE_ROOT"; };
+		752E69ECB400EFBC66DB8081 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_Draggable3DOrientation.h"; path = "../../../juce/modules/juce_opengl/opengl/juce_Draggable3DOrientation.h"; sourceTree = "SOURCE_ROOT"; };
+		75A33066D1965DA1A60D41B0 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; name = "juce_core.mm"; path = "../../../juce/modules/juce_core/juce_core.mm"; sourceTree = "SOURCE_ROOT"; };
+		75CA737B306C29F574308F8E = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_ThreadPool.h"; path = "../../../juce/modules/juce_core/threads/juce_ThreadPool.h"; sourceTree = "SOURCE_ROOT"; };
+		7640817A6F712E83E8127229 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; name = "juce_mac_SystemStats.mm"; path = "../../../juce/modules/juce_core/native/juce_mac_SystemStats.mm"; sourceTree = "SOURCE_ROOT"; };
+		764F6B4DF0E407661F9594C1 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_MidiMessageSequence.h"; path = "../../../juce/modules/juce_audio_basics/midi/juce_MidiMessageSequence.h"; sourceTree = "SOURCE_ROOT"; };
+		76566BC742E2FB1F6FDC34F5 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; name = "juce_gui_extra.mm"; path = "../../../juce/modules/juce_gui_extra/juce_gui_extra.mm"; sourceTree = "SOURCE_ROOT"; };
+		765EE2C3B48F47F94E607FBF = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_File.h"; path = "../../../juce/modules/juce_core/files/juce_File.h"; sourceTree = "SOURCE_ROOT"; };
+		76758FB3B866000523F0B144 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_SplashScreen.cpp"; path = "../../../juce/modules/juce_gui_extra/misc/juce_SplashScreen.cpp"; sourceTree = "SOURCE_ROOT"; };
+		76824FE677CBBE997E6B95AC = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = TouchkeyDevice.cpp; path = ../../Source/TouchKeys/TouchkeyDevice.cpp; sourceTree = "SOURCE_ROOT"; };
+		7688BA989669160877676209 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_FileListComponent.cpp"; path = "../../../juce/modules/juce_gui_basics/filebrowser/juce_FileListComponent.cpp"; sourceTree = "SOURCE_ROOT"; };
+		76ACD5EA5060FE1084BE7407 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_SystemClipboard.h"; path = "../../../juce/modules/juce_gui_basics/keyboard/juce_SystemClipboard.h"; sourceTree = "SOURCE_ROOT"; };
+		7756797D74D0D9D929AA9A80 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_DrawableRectangle.h"; path = "../../../juce/modules/juce_gui_basics/drawables/juce_DrawableRectangle.h"; sourceTree = "SOURCE_ROOT"; };
+		776A2D8E1CDE4CBEFFA042B0 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; name = "juce_mac_Strings.mm"; path = "../../../juce/modules/juce_core/native/juce_mac_Strings.mm"; sourceTree = "SOURCE_ROOT"; };
+		778872C42C72FA6949A2536E = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_ImageComponent.cpp"; path = "../../../juce/modules/juce_gui_basics/widgets/juce_ImageComponent.cpp"; sourceTree = "SOURCE_ROOT"; };
+		77BA61409E24E072AF1E5493 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = TouchkeyKeyDivisionMapping.cpp; path = ../../Source/Mappings/KeyDivision/TouchkeyKeyDivisionMapping.cpp; sourceTree = "SOURCE_ROOT"; };
+		77D10D2C57D62DF18D3CE862 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = TouchkeyPitchBendMappingFactory.cpp; path = ../../Source/Mappings/PitchBend/TouchkeyPitchBendMappingFactory.cpp; sourceTree = "SOURCE_ROOT"; };
+		77E88D86D01AF16261B9E3AC = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_win32_ComSmartPtr.h"; path = "../../../juce/modules/juce_core/native/juce_win32_ComSmartPtr.h"; sourceTree = "SOURCE_ROOT"; };
+		7828BC7ED00966FBBE2A90CB = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_FileBrowserComponent.h"; path = "../../../juce/modules/juce_gui_basics/filebrowser/juce_FileBrowserComponent.h"; sourceTree = "SOURCE_ROOT"; };
+		7964EC4FD4E5860CF85469A6 = { isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AudioToolbox.framework; path = System/Library/Frameworks/AudioToolbox.framework; sourceTree = SDKROOT; };
+		797AB794731D226BD260BA38 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_ComponentListener.h"; path = "../../../juce/modules/juce_gui_basics/components/juce_ComponentListener.h"; sourceTree = "SOURCE_ROOT"; };
+		79892EC92519C81D7E02BDE1 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_gui_basics.h"; path = "../../../juce/modules/juce_gui_basics/juce_gui_basics.h"; sourceTree = "SOURCE_ROOT"; };
+		79EBCB683DF7BD707B29F835 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_ReverbAudioSource.h"; path = "../../../juce/modules/juce_audio_basics/sources/juce_ReverbAudioSource.h"; sourceTree = "SOURCE_ROOT"; };
+		79EC7F603DD271F787FE2EC9 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_LocalisedStrings.h"; path = "../../../juce/modules/juce_core/text/juce_LocalisedStrings.h"; sourceTree = "SOURCE_ROOT"; };
+		79ED04A9195FA283CCBF6492 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_ListenerList.h"; path = "../../../juce/modules/juce_events/broadcasters/juce_ListenerList.h"; sourceTree = "SOURCE_ROOT"; };
+		79FB5A147DA4950BDB10B97B = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_ScrollBar.cpp"; path = "../../../juce/modules/juce_gui_basics/layout/juce_ScrollBar.cpp"; sourceTree = "SOURCE_ROOT"; };
+		7A002562A60140BEE1434ECF = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = MappingFactorySplitter.cpp; path = ../../Source/Mappings/MappingFactorySplitter.cpp; sourceTree = "SOURCE_ROOT"; };
+		7A05F75D7C08D0921F4DBA0E = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_FileDragAndDropTarget.h"; path = "../../../juce/modules/juce_gui_basics/mouse/juce_FileDragAndDropTarget.h"; sourceTree = "SOURCE_ROOT"; };
+		7A2A67299D56679990772E1A = { isa = PBXFileReference; lastKnownFileType = file; name = "juce_module_info"; path = "../../../juce/modules/juce_audio_basics/juce_module_info"; sourceTree = "SOURCE_ROOT"; };
+		7AD3466889EF224CEB709119 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_MouseInputSource.h"; path = "../../../juce/modules/juce_gui_basics/mouse/juce_MouseInputSource.h"; sourceTree = "SOURCE_ROOT"; };
+		7B299BDFEBA4DABD680B7116 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = TouchkeyPitchBendMappingFactory.h; path = ../../Source/Mappings/PitchBend/TouchkeyPitchBendMappingFactory.h; sourceTree = "SOURCE_ROOT"; };
+		7B799825EC1230F6618EC6FA = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_opengl.h"; path = "../../../juce/modules/juce_opengl/juce_opengl.h"; sourceTree = "SOURCE_ROOT"; };
+		7B945396F869A8F9750F3F45 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_win32_Threads.cpp"; path = "../../../juce/modules/juce_core/native/juce_win32_Threads.cpp"; sourceTree = "SOURCE_ROOT"; };
+		7B9FCDB57204606F4A7FDAD4 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = MainWindow.cpp; path = ../../Source/GUI/MainWindow.cpp; sourceTree = "SOURCE_ROOT"; };
+		7BBF33364D3B65730CEAD5F1 = { isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = OpenGL.framework; path = System/Library/Frameworks/OpenGL.framework; sourceTree = SDKROOT; };
+		7CB06A483D91B0345A110791 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_AudioTransportSource.cpp"; path = "../../../juce/modules/juce_audio_devices/sources/juce_AudioTransportSource.cpp"; sourceTree = "SOURCE_ROOT"; };
+		7CF528A9CF4D2D0C77AECA41 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_AudioDataConverters.h"; path = "../../../juce/modules/juce_audio_basics/buffers/juce_AudioDataConverters.h"; sourceTree = "SOURCE_ROOT"; };
+		7D4128713E8FB000F14BF646 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_ImageButton.h"; path = "../../../juce/modules/juce_gui_basics/buttons/juce_ImageButton.h"; sourceTree = "SOURCE_ROOT"; };
+		7D4A55601B7F8AB486B0AF21 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_Matrix3D.h"; path = "../../../juce/modules/juce_opengl/opengl/juce_Matrix3D.h"; sourceTree = "SOURCE_ROOT"; };
+		7D8C65927B21E027450B23F9 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_NamedPipe.cpp"; path = "../../../juce/modules/juce_core/network/juce_NamedPipe.cpp"; sourceTree = "SOURCE_ROOT"; };
+		7DE576785D066AA5B72476F5 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_SystemStats.h"; path = "../../../juce/modules/juce_core/system/juce_SystemStats.h"; sourceTree = "SOURCE_ROOT"; };
+		7EB4672F9008FB273FB0E5A0 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_ComboBox.cpp"; path = "../../../juce/modules/juce_gui_basics/widgets/juce_ComboBox.cpp"; sourceTree = "SOURCE_ROOT"; };
+		7ED45DC66FA713C394B2EA1F = { isa = PBXFileReference; lastKnownFileType = file; name = "juce_module_info"; path = "../../../juce/modules/juce_events/juce_module_info"; sourceTree = "SOURCE_ROOT"; };
+		7EDCB02E405529C4B4A60DA9 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = MIDIKeyPositionMapping.h; path = ../../Source/Mappings/MIDIKeyPositionMapping.h; sourceTree = "SOURCE_ROOT"; };
+		7EE4DAABDC4CF9F13A3B5541 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; name = "juce_ios_UIViewComponent.mm"; path = "../../../juce/modules/juce_gui_extra/native/juce_ios_UIViewComponent.mm"; sourceTree = "SOURCE_ROOT"; };
+		7EE90399313A416997604AAE = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_FileFilter.h"; path = "../../../juce/modules/juce_gui_basics/filebrowser/juce_FileFilter.h"; sourceTree = "SOURCE_ROOT"; };
+		801654FBD686CBFC671446FB = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_TabbedButtonBar.h"; path = "../../../juce/modules/juce_gui_basics/layout/juce_TabbedButtonBar.h"; sourceTree = "SOURCE_ROOT"; };
+		80474D6CC77F91AADA740AC3 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_android_Midi.cpp"; path = "../../../juce/modules/juce_audio_devices/native/juce_android_Midi.cpp"; sourceTree = "SOURCE_ROOT"; };
+		80878F914A6A514CE5FEDD3C = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_LAMEEncoderAudioFormat.h"; path = "../../../juce/modules/juce_audio_formats/codecs/juce_LAMEEncoderAudioFormat.h"; sourceTree = "SOURCE_ROOT"; };
+		808B32EDF79E1139064ABB52 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_InputStream.cpp"; path = "../../../juce/modules/juce_core/streams/juce_InputStream.cpp"; sourceTree = "SOURCE_ROOT"; };
+		80BDEDE0C822A9AE504A2C38 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = KeyTouchFrame.h; path = ../../Source/TouchKeys/KeyTouchFrame.h; sourceTree = "SOURCE_ROOT"; };
+		80EBFCB95D6DF2DA4B645F20 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_DrawableComposite.cpp"; path = "../../../juce/modules/juce_gui_basics/drawables/juce_DrawableComposite.cpp"; sourceTree = "SOURCE_ROOT"; };
+		80FAE594348676A04BACD99F = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_CPlusPlusCodeTokeniserFunctions.h"; path = "../../../juce/modules/juce_gui_extra/code_editor/juce_CPlusPlusCodeTokeniserFunctions.h"; sourceTree = "SOURCE_ROOT"; };
+		8166153E2B41CAFEF2012166 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_FileSearchPathListComponent.h"; path = "../../../juce/modules/juce_gui_basics/filebrowser/juce_FileSearchPathListComponent.h"; sourceTree = "SOURCE_ROOT"; };
+		821F73F15E2DCF3438652D3B = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_ToneGeneratorAudioSource.h"; path = "../../../juce/modules/juce_audio_basics/sources/juce_ToneGeneratorAudioSource.h"; sourceTree = "SOURCE_ROOT"; };
+		83AE3F5E0B7AE5D956415BCC = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = PianoKeyCalibrator.cpp; path = ../../Source/TouchKeys/PianoKeyCalibrator.cpp; sourceTree = "SOURCE_ROOT"; };
+		83BB636195AA2F443ADEE961 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_FileBrowserComponent.cpp"; path = "../../../juce/modules/juce_gui_basics/filebrowser/juce_FileBrowserComponent.cpp"; sourceTree = "SOURCE_ROOT"; };
+		84329C0EAC5C8BCD99E03174 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_MemoryBlock.cpp"; path = "../../../juce/modules/juce_core/memory/juce_MemoryBlock.cpp"; sourceTree = "SOURCE_ROOT"; };
+		849B3D266CC18D432434AA9B = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = RawSensorDisplay.cpp; path = ../../Source/Display/RawSensorDisplay.cpp; sourceTree = "SOURCE_ROOT"; };
+		84AB0ED9ED35EC083E56C73B = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_RectanglePlacement.h"; path = "../../../juce/modules/juce_graphics/placement/juce_RectanglePlacement.h"; sourceTree = "SOURCE_ROOT"; };
+		84E1E20BD5B317A9C8AEB844 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_AttributedString.h"; path = "../../../juce/modules/juce_graphics/fonts/juce_AttributedString.h"; sourceTree = "SOURCE_ROOT"; };
+		84EBC2A31E96330B004B8ACD = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_JSON.h"; path = "../../../juce/modules/juce_core/javascript/juce_JSON.h"; sourceTree = "SOURCE_ROOT"; };
+		84FFC974D174B7938505FCE2 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_ButtonPropertyComponent.h"; path = "../../../juce/modules/juce_gui_basics/properties/juce_ButtonPropertyComponent.h"; sourceTree = "SOURCE_ROOT"; };
+		8517608903BDD585564A2BD9 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_mac_CarbonViewWrapperComponent.h"; path = "../../../juce/modules/juce_gui_extra/native/juce_mac_CarbonViewWrapperComponent.h"; sourceTree = "SOURCE_ROOT"; };
+		857081343554E08270399A7A = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_CallOutBox.cpp"; path = "../../../juce/modules/juce_gui_basics/windows/juce_CallOutBox.cpp"; sourceTree = "SOURCE_ROOT"; };
+		8577418313DD8B1192FDD61D = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = BinaryData.h; path = ../../JuceLibraryCode/BinaryData.h; sourceTree = "SOURCE_ROOT"; };
+		859ABCC7E6D33E942919BC4B = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_AffineTransform.cpp"; path = "../../../juce/modules/juce_graphics/geometry/juce_AffineTransform.cpp"; sourceTree = "SOURCE_ROOT"; };
+		8628FF7B8F05A904C8C59EC4 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_FileBasedDocument.h"; path = "../../../juce/modules/juce_gui_extra/documents/juce_FileBasedDocument.h"; sourceTree = "SOURCE_ROOT"; };
+		8688C08B789657B322528750 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_ResamplingAudioSource.h"; path = "../../../juce/modules/juce_audio_basics/sources/juce_ResamplingAudioSource.h"; sourceTree = "SOURCE_ROOT"; };
+		86A8F063C7880CC175FCB59A = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_DynamicObject.cpp"; path = "../../../juce/modules/juce_core/containers/juce_DynamicObject.cpp"; sourceTree = "SOURCE_ROOT"; };
+		86C14BDBEB5F070A2166E8E6 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_ApplicationProperties.h"; path = "../../../juce/modules/juce_data_structures/app_properties/juce_ApplicationProperties.h"; sourceTree = "SOURCE_ROOT"; };
+		87083E81CEAC3B0F8BE7AAAD = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_Point.h"; path = "../../../juce/modules/juce_graphics/geometry/juce_Point.h"; sourceTree = "SOURCE_ROOT"; };
+		8720C0E6620ACF2E555C1CB5 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_TextButton.h"; path = "../../../juce/modules/juce_gui_basics/buttons/juce_TextButton.h"; sourceTree = "SOURCE_ROOT"; };
+		8776329610ED3DF98A95ECD2 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = IIRFilter.cpp; path = ../../Source/Utility/IIRFilter.cpp; sourceTree = "SOURCE_ROOT"; };
+		879BA74FC54118852648722F = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = TouchkeyMultiFingerTriggerMappingFactory.cpp; path = ../../Source/Mappings/MultiFingerTrigger/TouchkeyMultiFingerTriggerMappingFactory.cpp; sourceTree = "SOURCE_ROOT"; };
+		88464C31A0A0F8F876BD5CA2 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_AudioFormatReaderSource.h"; path = "../../../juce/modules/juce_audio_formats/format/juce_AudioFormatReaderSource.h"; sourceTree = "SOURCE_ROOT"; };
+		884FF9165E861BE4F04646FC = { isa = PBXFileReference; lastKnownFileType = image.png; name = "tk-icon-512.png"; path = "../../Resources/tk-icon-512.png"; sourceTree = "SOURCE_ROOT"; };
+		88D0E8DA9A02D3ADB13FB753 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_WavAudioFormat.cpp"; path = "../../../juce/modules/juce_audio_formats/codecs/juce_WavAudioFormat.cpp"; sourceTree = "SOURCE_ROOT"; };
+		890B8EF2DC47885686DAAF94 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_CharPointer_ASCII.h"; path = "../../../juce/modules/juce_core/text/juce_CharPointer_ASCII.h"; sourceTree = "SOURCE_ROOT"; };
+		89130E3BE0652A8CAB11A2BE = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_osx_MessageQueue.h"; path = "../../../juce/modules/juce_events/native/juce_osx_MessageQueue.h"; sourceTree = "SOURCE_ROOT"; };
+		8990C39751CB1A719998A52D = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_TextDragAndDropTarget.h"; path = "../../../juce/modules/juce_gui_basics/mouse/juce_TextDragAndDropTarget.h"; sourceTree = "SOURCE_ROOT"; };
+		89EB02CBE5C658A77500E836 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_LassoComponent.h"; path = "../../../juce/modules/juce_gui_basics/mouse/juce_LassoComponent.h"; sourceTree = "SOURCE_ROOT"; };
+		8A02BB6161F2E74E22ABE635 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_ImageCache.h"; path = "../../../juce/modules/juce_graphics/images/juce_ImageCache.h"; sourceTree = "SOURCE_ROOT"; };
+		8AB62337AC1BB45D8DD8B43D = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_MarkerList.cpp"; path = "../../../juce/modules/juce_gui_basics/positioning/juce_MarkerList.cpp"; sourceTree = "SOURCE_ROOT"; };
+		8ADF834CD28E353B15D200C7 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = TouchkeyControlMappingFactory.cpp; path = ../../Source/Mappings/Control/TouchkeyControlMappingFactory.cpp; sourceTree = "SOURCE_ROOT"; };
+		8BA8A815340C0C952C6DAA46 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_ImageConvolutionKernel.cpp"; path = "../../../juce/modules/juce_graphics/images/juce_ImageConvolutionKernel.cpp"; sourceTree = "SOURCE_ROOT"; };
+		8BC0A410AADDAFD652051BDC = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_RectanglePlacement.cpp"; path = "../../../juce/modules/juce_graphics/placement/juce_RectanglePlacement.cpp"; sourceTree = "SOURCE_ROOT"; };
+		8BD94C1723C9174065B1CA57 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_Variant.h"; path = "../../../juce/modules/juce_core/containers/juce_Variant.h"; sourceTree = "SOURCE_ROOT"; };
+		8C46F1B244F115E716B37147 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_OpenGLFrameBuffer.cpp"; path = "../../../juce/modules/juce_opengl/opengl/juce_OpenGLFrameBuffer.cpp"; sourceTree = "SOURCE_ROOT"; };
+		8C9EEF956680F688D1C9E58B = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_DragAndDropContainer.h"; path = "../../../juce/modules/juce_gui_basics/mouse/juce_DragAndDropContainer.h"; sourceTree = "SOURCE_ROOT"; };
+		8CA21E32C250B212F23EA8AF = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; name = "juce_opengl.mm"; path = "../../../juce/modules/juce_opengl/juce_opengl.mm"; sourceTree = "SOURCE_ROOT"; };
+		8CBA2AD4897FAC4F6191FDB3 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_LowLevelGraphicsPostScriptRenderer.h"; path = "../../../juce/modules/juce_graphics/contexts/juce_LowLevelGraphicsPostScriptRenderer.h"; sourceTree = "SOURCE_ROOT"; };
+		8CF10E46C293D8E35324F60F = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = TouchkeyKeyDivisionMappingFactory.h; path = ../../Source/Mappings/KeyDivision/TouchkeyKeyDivisionMappingFactory.h; sourceTree = "SOURCE_ROOT"; };
+		8D3354A1E9C02DED513A4355 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; name = "juce_ios_UIViewComponentPeer.mm"; path = "../../../juce/modules/juce_gui_basics/native/juce_ios_UIViewComponentPeer.mm"; sourceTree = "SOURCE_ROOT"; };
+		8E169B97526DD9CCDE1BBA98 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_BubbleMessageComponent.cpp"; path = "../../../juce/modules/juce_gui_extra/misc/juce_BubbleMessageComponent.cpp"; sourceTree = "SOURCE_ROOT"; };
+		8E7AC08D09000F53F4C123CC = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_OpenGLGraphicsContext.cpp"; path = "../../../juce/modules/juce_opengl/opengl/juce_OpenGLGraphicsContext.cpp"; sourceTree = "SOURCE_ROOT"; };
+		8ED90DCD63DA37E5D56402E3 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_SplashScreen.h"; path = "../../../juce/modules/juce_gui_extra/misc/juce_SplashScreen.h"; sourceTree = "SOURCE_ROOT"; };
+		8ED952C01B726C1DBFACE2BD = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_AudioSourcePlayer.h"; path = "../../../juce/modules/juce_audio_devices/sources/juce_AudioSourcePlayer.h"; sourceTree = "SOURCE_ROOT"; };
+		8FCE773A5D594437EF46757A = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_LagrangeInterpolator.cpp"; path = "../../../juce/modules/juce_audio_basics/effects/juce_LagrangeInterpolator.cpp"; sourceTree = "SOURCE_ROOT"; };
+		8FEBDE824FD6FDFC09417C88 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_win32_Network.cpp"; path = "../../../juce/modules/juce_core/native/juce_win32_Network.cpp"; sourceTree = "SOURCE_ROOT"; };
+		9084178EF51E96AA8ACBB28D = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_TextEditorKeyMapper.h"; path = "../../../juce/modules/juce_gui_basics/keyboard/juce_TextEditorKeyMapper.h"; sourceTree = "SOURCE_ROOT"; };
+		90CFD9356A26573B6D0BBF3F = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_WindowsRegistry.h"; path = "../../../juce/modules/juce_core/misc/juce_WindowsRegistry.h"; sourceTree = "SOURCE_ROOT"; };
+		91157413471588AA9BBA75D7 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; name = "juce_graphics.mm"; path = "../../../juce/modules/juce_graphics/juce_graphics.mm"; sourceTree = "SOURCE_ROOT"; };
+		91AADDB641BD3F80E9011C65 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = TouchkeyReleaseAngleMappingFactory.cpp; path = ../../Source/Mappings/ReleaseAngle/TouchkeyReleaseAngleMappingFactory.cpp; sourceTree = "SOURCE_ROOT"; };
+		9210475F5E1B29B9C850ACDA = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_ImagePreviewComponent.cpp"; path = "../../../juce/modules/juce_gui_basics/filebrowser/juce_ImagePreviewComponent.cpp"; sourceTree = "SOURCE_ROOT"; };
+		92328D97A09C3864407862BC = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; name = "juce_mac_Fonts.mm"; path = "../../../juce/modules/juce_graphics/native/juce_mac_Fonts.mm"; sourceTree = "SOURCE_ROOT"; };
+		929C9770C346C51B352937B8 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_KeyListener.cpp"; path = "../../../juce/modules/juce_gui_basics/keyboard/juce_KeyListener.cpp"; sourceTree = "SOURCE_ROOT"; };
+		92C3C4A8F0677E70A2AAD5B9 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_Label.h"; path = "../../../juce/modules/juce_gui_basics/widgets/juce_Label.h"; sourceTree = "SOURCE_ROOT"; };
+		92D1EE5E3C477F4341709537 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_LowLevelGraphicsContext.h"; path = "../../../juce/modules/juce_graphics/contexts/juce_LowLevelGraphicsContext.h"; sourceTree = "SOURCE_ROOT"; };
+		92DEFC9A2AE2181DB9A0A8C2 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_Result.h"; path = "../../../juce/modules/juce_core/misc/juce_Result.h"; sourceTree = "SOURCE_ROOT"; };
+		9314190BB0D368F2A7C5E7C3 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_Path.h"; path = "../../../juce/modules/juce_graphics/geometry/juce_Path.h"; sourceTree = "SOURCE_ROOT"; };
+		93186997B68111C578D28993 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_Thread.h"; path = "../../../juce/modules/juce_core/threads/juce_Thread.h"; sourceTree = "SOURCE_ROOT"; };
+		93FFEE74D94C3D0D397CE80D = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_Colour.cpp"; path = "../../../juce/modules/juce_graphics/colour/juce_Colour.cpp"; sourceTree = "SOURCE_ROOT"; };
+		94652A427B7F10B3405366B5 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_AudioFormatManager.cpp"; path = "../../../juce/modules/juce_audio_formats/format/juce_AudioFormatManager.cpp"; sourceTree = "SOURCE_ROOT"; };
+		94873408944DC8C21437551E = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_ThreadWithProgressWindow.h"; path = "../../../juce/modules/juce_gui_basics/windows/juce_ThreadWithProgressWindow.h"; sourceTree = "SOURCE_ROOT"; };
+		962444B8A31102AAEA550124 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_GlyphArrangement.h"; path = "../../../juce/modules/juce_graphics/fonts/juce_GlyphArrangement.h"; sourceTree = "SOURCE_ROOT"; };
+		96C1E28B6083DF9CA08B8DA5 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_ChoicePropertyComponent.h"; path = "../../../juce/modules/juce_gui_basics/properties/juce_ChoicePropertyComponent.h"; sourceTree = "SOURCE_ROOT"; };
+		97101F61193525E59EE16454 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_CaretComponent.cpp"; path = "../../../juce/modules/juce_gui_basics/keyboard/juce_CaretComponent.cpp"; sourceTree = "SOURCE_ROOT"; };
+		972158C4F988264A6E5BC592 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_LookAndFeel_V3.cpp"; path = "../../../juce/modules/juce_gui_basics/lookandfeel/juce_LookAndFeel_V3.cpp"; sourceTree = "SOURCE_ROOT"; };
+		976AEB3279AAA64A0EF186FE = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_MemoryInputStream.h"; path = "../../../juce/modules/juce_core/streams/juce_MemoryInputStream.h"; sourceTree = "SOURCE_ROOT"; };
+		978BBCD3C704C06D407A95C0 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_PathIterator.cpp"; path = "../../../juce/modules/juce_graphics/geometry/juce_PathIterator.cpp"; sourceTree = "SOURCE_ROOT"; };
+		980132D50C058EB7B1B625BA = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_ArrowButton.cpp"; path = "../../../juce/modules/juce_gui_basics/buttons/juce_ArrowButton.cpp"; sourceTree = "SOURCE_ROOT"; };
+		980B7401EF270C8C3BF5921D = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_FloatVectorOperations.h"; path = "../../../juce/modules/juce_audio_basics/buffers/juce_FloatVectorOperations.h"; sourceTree = "SOURCE_ROOT"; };
+		984641E3DE573FF55DADB2AE = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_Typeface.cpp"; path = "../../../juce/modules/juce_graphics/fonts/juce_Typeface.cpp"; sourceTree = "SOURCE_ROOT"; };
+		984AEFF53886155F33D2F336 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; name = "juce_mac_MouseCursor.mm"; path = "../../../juce/modules/juce_gui_basics/native/juce_mac_MouseCursor.mm"; sourceTree = "SOURCE_ROOT"; };
+		987FA7FC98B320FAE747F2ED = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_ValueTree.h"; path = "../../../juce/modules/juce_data_structures/values/juce_ValueTree.h"; sourceTree = "SOURCE_ROOT"; };
+		99011DB2E425583086338097 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_MidiInput.h"; path = "../../../juce/modules/juce_audio_devices/midi_io/juce_MidiInput.h"; sourceTree = "SOURCE_ROOT"; };
+		9923EBA9D4A01541CAE90CF2 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_ScopedReadLock.h"; path = "../../../juce/modules/juce_core/threads/juce_ScopedReadLock.h"; sourceTree = "SOURCE_ROOT"; };
+		992F1A00CD771D7AA8E1E15D = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_AudioSubsectionReader.cpp"; path = "../../../juce/modules/juce_audio_formats/format/juce_AudioSubsectionReader.cpp"; sourceTree = "SOURCE_ROOT"; };
+		99AA5732A932BEF06D09AB22 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_MidiDataConcatenator.h"; path = "../../../juce/modules/juce_audio_devices/native/juce_MidiDataConcatenator.h"; sourceTree = "SOURCE_ROOT"; };
+		99C03C1C226C3BC10D3422BB = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_Sampler.h"; path = "../../../juce/modules/juce_audio_formats/sampler/juce_Sampler.h"; sourceTree = "SOURCE_ROOT"; };
+		99C3E2DED78CCBDBE436AE99 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_BorderSize.h"; path = "../../../juce/modules/juce_graphics/geometry/juce_BorderSize.h"; sourceTree = "SOURCE_ROOT"; };
+		9A42DC8DA886A985FC5A0862 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_LinkedListPointer.h"; path = "../../../juce/modules/juce_core/containers/juce_LinkedListPointer.h"; sourceTree = "SOURCE_ROOT"; };
+		9A4DFD3457C3E8A95F71F6E7 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_InputStream.h"; path = "../../../juce/modules/juce_core/streams/juce_InputStream.h"; sourceTree = "SOURCE_ROOT"; };
+		9A4FBA2DD18C825B15EE6D19 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_AudioSubsectionReader.h"; path = "../../../juce/modules/juce_audio_formats/format/juce_AudioSubsectionReader.h"; sourceTree = "SOURCE_ROOT"; };
+		9A639B808D54F5251C701002 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_GlyphArrangement.cpp"; path = "../../../juce/modules/juce_graphics/fonts/juce_GlyphArrangement.cpp"; sourceTree = "SOURCE_ROOT"; };
+		9A8C321C45F9E659F8676BA3 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; name = "juce_mac_AudioCDBurner.mm"; path = "../../../juce/modules/juce_audio_devices/native/juce_mac_AudioCDBurner.mm"; sourceTree = "SOURCE_ROOT"; };
+		9AB36BA5D331A32F6EBF2E4C = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_DropShadowEffect.h"; path = "../../../juce/modules/juce_graphics/effects/juce_DropShadowEffect.h"; sourceTree = "SOURCE_ROOT"; };
+		9AD89C058D91C6D880FF028E = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_MultiTimer.h"; path = "../../../juce/modules/juce_events/timers/juce_MultiTimer.h"; sourceTree = "SOURCE_ROOT"; };
+		9B8C279F7E8FF6BCE7EFFF64 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; name = "juce_mac_FileChooser.mm"; path = "../../../juce/modules/juce_gui_basics/native/juce_mac_FileChooser.mm"; sourceTree = "SOURCE_ROOT"; };
+		9B9B99EFAE12DE02397A87B4 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = Osc.h; path = ../../Source/TouchKeys/Osc.h; sourceTree = "SOURCE_ROOT"; };
+		9B9C26B87D15142FEBBDAD4C = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_InterprocessConnectionServer.cpp"; path = "../../../juce/modules/juce_events/interprocess/juce_InterprocessConnectionServer.cpp"; sourceTree = "SOURCE_ROOT"; };
+		9BE9BB5131F3D749A7717085 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_ScopedValueSetter.h"; path = "../../../juce/modules/juce_core/containers/juce_ScopedValueSetter.h"; sourceTree = "SOURCE_ROOT"; };
+		9C0B692CB270EFA9DB7FA4D7 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = PianoKeyboard.cpp; path = ../../Source/TouchKeys/PianoKeyboard.cpp; sourceTree = "SOURCE_ROOT"; };
+		9CAC7B4DFD297F29680FE739 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_CodeEditorComponent.h"; path = "../../../juce/modules/juce_gui_extra/code_editor/juce_CodeEditorComponent.h"; sourceTree = "SOURCE_ROOT"; };
+		9D044091881D52DC7354C266 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_CaretComponent.h"; path = "../../../juce/modules/juce_gui_basics/keyboard/juce_CaretComponent.h"; sourceTree = "SOURCE_ROOT"; };
+		9D0F29356400B8E025E5F524 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_ArrayAllocationBase.h"; path = "../../../juce/modules/juce_core/containers/juce_ArrayAllocationBase.h"; sourceTree = "SOURCE_ROOT"; };
+		9D2CA1387AFF642B2E0C06A3 = { isa = PBXFileReference; lastKnownFileType = file; name = "juce_module_info"; path = "../../../juce/modules/juce_audio_devices/juce_module_info"; sourceTree = "SOURCE_ROOT"; };
+		9D627B675B5F55FD5A203346 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = TimestampSynchronizer.cpp; path = ../../Source/Utility/TimestampSynchronizer.cpp; sourceTree = "SOURCE_ROOT"; };
+		9D633F4B9BBCF3F4D9331E00 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = MappingFactory.h; path = ../../Source/Mappings/MappingFactory.h; sourceTree = "SOURCE_ROOT"; };
+		9D744C0830CCA407EB41368E = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_TextEditor.cpp"; path = "../../../juce/modules/juce_gui_basics/widgets/juce_TextEditor.cpp"; sourceTree = "SOURCE_ROOT"; };
+		9D97B40DE97CFCF58CEA9DBD = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_FileTreeComponent.h"; path = "../../../juce/modules/juce_gui_basics/filebrowser/juce_FileTreeComponent.h"; sourceTree = "SOURCE_ROOT"; };
+		9DEC36437E061C38E07BAC75 = { isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreAudio.framework; path = System/Library/Frameworks/CoreAudio.framework; sourceTree = SDKROOT; };
+		9E1100E08D019514168470B6 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_CharPointer_UTF8.h"; path = "../../../juce/modules/juce_core/text/juce_CharPointer_UTF8.h"; sourceTree = "SOURCE_ROOT"; };
+		9E51136900384B3DBAF5D60E = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_WavAudioFormat.h"; path = "../../../juce/modules/juce_audio_formats/codecs/juce_WavAudioFormat.h"; sourceTree = "SOURCE_ROOT"; };
+		9E80DB194A5D094EE64958AB = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_ComponentAnimator.h"; path = "../../../juce/modules/juce_gui_basics/layout/juce_ComponentAnimator.h"; sourceTree = "SOURCE_ROOT"; };
+		9E815E59EEB3FBAED99918C2 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_PathStrokeType.cpp"; path = "../../../juce/modules/juce_graphics/geometry/juce_PathStrokeType.cpp"; sourceTree = "SOURCE_ROOT"; };
+		9EA1E13123758B126A76513B = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_linux_FileChooser.cpp"; path = "../../../juce/modules/juce_gui_basics/native/juce_linux_FileChooser.cpp"; sourceTree = "SOURCE_ROOT"; };
+		9EFEE9C78BF40448C92E4EA3 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_IPAddress.cpp"; path = "../../../juce/modules/juce_core/network/juce_IPAddress.cpp"; sourceTree = "SOURCE_ROOT"; };
+		9F455251CF84921306543B93 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_DirectoryContentsList.cpp"; path = "../../../juce/modules/juce_gui_basics/filebrowser/juce_DirectoryContentsList.cpp"; sourceTree = "SOURCE_ROOT"; };
+		9F64E66E6497FC3516119144 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_KeyPress.cpp"; path = "../../../juce/modules/juce_gui_basics/keyboard/juce_KeyPress.cpp"; sourceTree = "SOURCE_ROOT"; };
+		9F843264E37B0F16987CDA48 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = Types.h; path = ../../Source/Utility/Types.h; sourceTree = "SOURCE_ROOT"; };
+		9FA50DF5D707E8075BE42429 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_TabbedComponent.cpp"; path = "../../../juce/modules/juce_gui_basics/layout/juce_TabbedComponent.cpp"; sourceTree = "SOURCE_ROOT"; };
+		A01CEE27EC9F82523164E137 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_ApplicationCommandTarget.h"; path = "../../../juce/modules/juce_gui_basics/commands/juce_ApplicationCommandTarget.h"; sourceTree = "SOURCE_ROOT"; };
+		A01E2825335AD2156D93D462 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_StretchableLayoutManager.cpp"; path = "../../../juce/modules/juce_gui_basics/layout/juce_StretchableLayoutManager.cpp"; sourceTree = "SOURCE_ROOT"; };
+		A024211A62CC0D9A646A7676 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_NamedPipe.h"; path = "../../../juce/modules/juce_core/network/juce_NamedPipe.h"; sourceTree = "SOURCE_ROOT"; };
+		A041486CD4E6540A8D189C15 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_HyperlinkButton.h"; path = "../../../juce/modules/juce_gui_basics/buttons/juce_HyperlinkButton.h"; sourceTree = "SOURCE_ROOT"; };
+		A08C368697313473C377CE60 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_RelativePoint.cpp"; path = "../../../juce/modules/juce_gui_basics/positioning/juce_RelativePoint.cpp"; sourceTree = "SOURCE_ROOT"; };
+		A1723FE218F435C322FB9AE0 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_Rectangle.h"; path = "../../../juce/modules/juce_graphics/geometry/juce_Rectangle.h"; sourceTree = "SOURCE_ROOT"; };
+		A1C6CCE346DC8551574C4108 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_AudioTransportSource.h"; path = "../../../juce/modules/juce_audio_devices/sources/juce_AudioTransportSource.h"; sourceTree = "SOURCE_ROOT"; };
+		A2120F8127E3DC96FD65D613 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_MouseInactivityDetector.h"; path = "../../../juce/modules/juce_gui_basics/mouse/juce_MouseInactivityDetector.h"; sourceTree = "SOURCE_ROOT"; };
+		A237BDE5342960289E3E6302 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_AnimatedPositionBehaviours.h"; path = "../../../juce/modules/juce_gui_basics/layout/juce_AnimatedPositionBehaviours.h"; sourceTree = "SOURCE_ROOT"; };
+		A266DE5D4412DD7BA69F8CF8 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_ScrollBar.h"; path = "../../../juce/modules/juce_gui_basics/layout/juce_ScrollBar.h"; sourceTree = "SOURCE_ROOT"; };
+		A28DB7EF92412F8484B88A07 = { isa = PBXFileReference; lastKnownFileType = file; name = "juce_module_info"; path = "../../../juce/modules/juce_gui_extra/juce_module_info"; sourceTree = "SOURCE_ROOT"; };
+		A29D19C2D6EEEE8A3C5D63B7 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_FilenameComponent.h"; path = "../../../juce/modules/juce_gui_basics/filebrowser/juce_FilenameComponent.h"; sourceTree = "SOURCE_ROOT"; };
+		A2B676B239E39A8977B5EB5E = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_UndoableAction.h"; path = "../../../juce/modules/juce_data_structures/undomanager/juce_UndoableAction.h"; sourceTree = "SOURCE_ROOT"; };
+		A4317D5AEB8EDF9E2F0BEFF5 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_ComponentDragger.cpp"; path = "../../../juce/modules/juce_gui_basics/mouse/juce_ComponentDragger.cpp"; sourceTree = "SOURCE_ROOT"; };
+		A50534690CFC8F765EEBCACC = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_ApplicationBase.h"; path = "../../../juce/modules/juce_events/messages/juce_ApplicationBase.h"; sourceTree = "SOURCE_ROOT"; };
+		A58766D5C2AD6E9FC08EBD74 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_ImageFileFormat.h"; path = "../../../juce/modules/juce_graphics/images/juce_ImageFileFormat.h"; sourceTree = "SOURCE_ROOT"; };
+		A68FFCC7544F6D566C62402E = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_MouseInactivityDetector.cpp"; path = "../../../juce/modules/juce_gui_basics/mouse/juce_MouseInactivityDetector.cpp"; sourceTree = "SOURCE_ROOT"; };
+		A6B7D7B713F01F521411F2D3 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = Mapping.cpp; path = ../../Source/Mappings/Mapping.cpp; sourceTree = "SOURCE_ROOT"; };
+		A6D093637F71601C70B1DA16 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_Result.cpp"; path = "../../../juce/modules/juce_core/misc/juce_Result.cpp"; sourceTree = "SOURCE_ROOT"; };
+		A760FABA720E873DCAD04DAF = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_MixerAudioSource.h"; path = "../../../juce/modules/juce_audio_basics/sources/juce_MixerAudioSource.h"; sourceTree = "SOURCE_ROOT"; };
+		A79D9F98051857EFFD8C2B88 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_Viewport.h"; path = "../../../juce/modules/juce_gui_basics/layout/juce_Viewport.h"; sourceTree = "SOURCE_ROOT"; };
+		A7CB749C845499A420598F30 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_UnitTest.cpp"; path = "../../../juce/modules/juce_core/unit_tests/juce_UnitTest.cpp"; sourceTree = "SOURCE_ROOT"; };
+		A7E3F42645F33A7460F4D770 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_MemoryInputStream.cpp"; path = "../../../juce/modules/juce_core/streams/juce_MemoryInputStream.cpp"; sourceTree = "SOURCE_ROOT"; };
+		A84AFB90B3F99E4469CF5FDF = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_ToolbarItemFactory.h"; path = "../../../juce/modules/juce_gui_basics/widgets/juce_ToolbarItemFactory.h"; sourceTree = "SOURCE_ROOT"; };
+		A84C4AE3D9E536D74C379F0F = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_InterprocessConnectionServer.h"; path = "../../../juce/modules/juce_events/interprocess/juce_InterprocessConnectionServer.h"; sourceTree = "SOURCE_ROOT"; };
+		A8AF4B0BFECD9F39E1FE4E09 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_AppleRemote.h"; path = "../../../juce/modules/juce_gui_extra/misc/juce_AppleRemote.h"; sourceTree = "SOURCE_ROOT"; };
+		A8BD97DF1D8C19D42CF0715C = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_PositionableAudioSource.h"; path = "../../../juce/modules/juce_audio_basics/sources/juce_PositionableAudioSource.h"; sourceTree = "SOURCE_ROOT"; };
+		A9184C4BCBA196795CCBCD98 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_FileChooserDialogBox.h"; path = "../../../juce/modules/juce_gui_basics/filebrowser/juce_FileChooserDialogBox.h"; sourceTree = "SOURCE_ROOT"; };
+		A9483F780B954919D59F1178 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_MixerAudioSource.cpp"; path = "../../../juce/modules/juce_audio_basics/sources/juce_MixerAudioSource.cpp"; sourceTree = "SOURCE_ROOT"; };
+		A9832F86A2E7CA1675C64C47 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_TextEditor.h"; path = "../../../juce/modules/juce_gui_basics/widgets/juce_TextEditor.h"; sourceTree = "SOURCE_ROOT"; };
+		A9F0CD5B5FBAE5D4589A8C10 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_DirectoryIterator.h"; path = "../../../juce/modules/juce_core/files/juce_DirectoryIterator.h"; sourceTree = "SOURCE_ROOT"; };
+		AAB51A5E4451980FD9D77FF7 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_android_OpenSL.cpp"; path = "../../../juce/modules/juce_audio_devices/native/juce_android_OpenSL.cpp"; sourceTree = "SOURCE_ROOT"; };
+		AAF9D7469BCE537A1FC75ABC = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_Value.cpp"; path = "../../../juce/modules/juce_data_structures/values/juce_Value.cpp"; sourceTree = "SOURCE_ROOT"; };
+		ABF842ECF835C33322E2F21A = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_PNGLoader.cpp"; path = "../../../juce/modules/juce_graphics/image_formats/juce_PNGLoader.cpp"; sourceTree = "SOURCE_ROOT"; };
+		AC5E7E9B31701A5A7B4E3ABB = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_BufferingAudioFormatReader.cpp"; path = "../../../juce/modules/juce_audio_formats/format/juce_BufferingAudioFormatReader.cpp"; sourceTree = "SOURCE_ROOT"; };
+		AC80872B6544527C71AB2B0B = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_TableHeaderComponent.h"; path = "../../../juce/modules/juce_gui_basics/widgets/juce_TableHeaderComponent.h"; sourceTree = "SOURCE_ROOT"; };
+		AC9C0675772B6211213D774F = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_LagrangeInterpolator.h"; path = "../../../juce/modules/juce_audio_basics/effects/juce_LagrangeInterpolator.h"; sourceTree = "SOURCE_ROOT"; };
+		ACA460FC9207D68260CD11CA = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_LookAndFeel.h"; path = "../../../juce/modules/juce_gui_basics/lookandfeel/juce_LookAndFeel.h"; sourceTree = "SOURCE_ROOT"; };
+		ACAD7720D728A3A8C9DB3949 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_ToolbarItemPalette.h"; path = "../../../juce/modules/juce_gui_basics/widgets/juce_ToolbarItemPalette.h"; sourceTree = "SOURCE_ROOT"; };
+		ACB27E148164D4F5D3C36FCB = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_linux_SystemStats.cpp"; path = "../../../juce/modules/juce_core/native/juce_linux_SystemStats.cpp"; sourceTree = "SOURCE_ROOT"; };
+		ACC97E565EDDD42DA4C1CB2E = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_win32_DirectWriteTypeLayout.cpp"; path = "../../../juce/modules/juce_graphics/native/juce_win32_DirectWriteTypeLayout.cpp"; sourceTree = "SOURCE_ROOT"; };
+		ACDF26585BEEE33D70E19A3C = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_mac_SystemTrayIcon.cpp"; path = "../../../juce/modules/juce_gui_extra/native/juce_mac_SystemTrayIcon.cpp"; sourceTree = "SOURCE_ROOT"; };
+		AD32973FF9774A2CF194C3CB = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_URL.cpp"; path = "../../../juce/modules/juce_core/network/juce_URL.cpp"; sourceTree = "SOURCE_ROOT"; };
+		AD95293E5C296F4C23A4F94B = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_ChildProcess.h"; path = "../../../juce/modules/juce_core/threads/juce_ChildProcess.h"; sourceTree = "SOURCE_ROOT"; };
+		ADC1E27313862E1559D41C3A = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_core.h"; path = "../../../juce/modules/juce_core/juce_core.h"; sourceTree = "SOURCE_ROOT"; };
+		ADCF2FE48BE727BF37B3C817 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_OpenGLGraphicsContext.h"; path = "../../../juce/modules/juce_opengl/opengl/juce_OpenGLGraphicsContext.h"; sourceTree = "SOURCE_ROOT"; };
+		AE32A61D504EC34FE186FDD1 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_CustomTypeface.h"; path = "../../../juce/modules/juce_graphics/fonts/juce_CustomTypeface.h"; sourceTree = "SOURCE_ROOT"; };
+		AE876C6FF1860B4CC97EDF07 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_FileChooserDialogBox.cpp"; path = "../../../juce/modules/juce_gui_basics/filebrowser/juce_FileChooserDialogBox.cpp"; sourceTree = "SOURCE_ROOT"; };
+		AE954565B99BE415F7966F4A = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_Random.cpp"; path = "../../../juce/modules/juce_core/maths/juce_Random.cpp"; sourceTree = "SOURCE_ROOT"; };
+		AE98A52AB3BC4D803B54380C = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_MidiKeyboardState.h"; path = "../../../juce/modules/juce_audio_basics/midi/juce_MidiKeyboardState.h"; sourceTree = "SOURCE_ROOT"; };
+		AF10BCD18ABC5EC330CE3B1B = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_win32_ASIO.cpp"; path = "../../../juce/modules/juce_audio_devices/native/juce_win32_ASIO.cpp"; sourceTree = "SOURCE_ROOT"; };
+		AF1275CD971968AF3CB13BAD = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_FileTreeComponent.cpp"; path = "../../../juce/modules/juce_gui_basics/filebrowser/juce_FileTreeComponent.cpp"; sourceTree = "SOURCE_ROOT"; };
+		AF7CC57D767DFC1F4B121270 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = IIRFilter.h; path = ../../Source/Utility/IIRFilter.h; sourceTree = "SOURCE_ROOT"; };
+		AF87CD32C1189D997A5520B9 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_Memory.h"; path = "../../../juce/modules/juce_core/memory/juce_Memory.h"; sourceTree = "SOURCE_ROOT"; };
+		AF8EFA9540E8757E68922E40 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = MappingScheduler.cpp; path = ../../Source/Mappings/MappingScheduler.cpp; sourceTree = "SOURCE_ROOT"; };
+		AFA3C397FE16ECC3DE0A5155 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_StretchableLayoutManager.h"; path = "../../../juce/modules/juce_gui_basics/layout/juce_StretchableLayoutManager.h"; sourceTree = "SOURCE_ROOT"; };
+		B0A4E57EEDD13E204E723A92 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = TouchkeyOnsetAngleMappingFactory.h; path = ../../Source/Mappings/OnsetAngle/TouchkeyOnsetAngleMappingFactory.h; sourceTree = "SOURCE_ROOT"; };
+		B0EAE043C24041BFA266B6EE = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_SystemStats.cpp"; path = "../../../juce/modules/juce_core/system/juce_SystemStats.cpp"; sourceTree = "SOURCE_ROOT"; };
+		B169A76946B512C0D34D1807 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_NativeMessageBox.h"; path = "../../../juce/modules/juce_gui_basics/windows/juce_NativeMessageBox.h"; sourceTree = "SOURCE_ROOT"; };
+		B1D86EC91F8BDE1D1BF29728 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_win32_HiddenMessageWindow.h"; path = "../../../juce/modules/juce_events/native/juce_win32_HiddenMessageWindow.h"; sourceTree = "SOURCE_ROOT"; };
+		B1E1FC7301A83D9FC8C320F9 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_DrawableButton.h"; path = "../../../juce/modules/juce_gui_basics/buttons/juce_DrawableButton.h"; sourceTree = "SOURCE_ROOT"; };
+		B24051ADEFBD03E1C7CB8DC3 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = GraphicsDisplayWindow.h; path = ../../Source/GUI/GraphicsDisplayWindow.h; sourceTree = "SOURCE_ROOT"; };
+		B2575F7C163D92DA04ABDC7B = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_win32_Midi.cpp"; path = "../../../juce/modules/juce_audio_devices/native/juce_win32_Midi.cpp"; sourceTree = "SOURCE_ROOT"; };
+		B25BC7CD4D7D11A764A0FF19 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_LAMEEncoderAudioFormat.cpp"; path = "../../../juce/modules/juce_audio_formats/codecs/juce_LAMEEncoderAudioFormat.cpp"; sourceTree = "SOURCE_ROOT"; };
+		B2C2E0F7737F05D71DC9CB1B = { isa = PBXFileReference; lastKnownFileType = file; name = "juce_module_info"; path = "../../../juce/modules/juce_opengl/juce_module_info"; sourceTree = "SOURCE_ROOT"; };
+		B2D53D597D6709806BB7239A = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_TextLayout.cpp"; path = "../../../juce/modules/juce_graphics/fonts/juce_TextLayout.cpp"; sourceTree = "SOURCE_ROOT"; };
+		B303EB3E070BBF9A30F07301 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_AudioCDReader.h"; path = "../../../juce/modules/juce_audio_devices/audio_cd/juce_AudioCDReader.h"; sourceTree = "SOURCE_ROOT"; };
+		B3414ED8780ED544999D2B1C = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_mac_CoreGraphicsHelpers.h"; path = "../../../juce/modules/juce_graphics/native/juce_mac_CoreGraphicsHelpers.h"; sourceTree = "SOURCE_ROOT"; };
+		B38844D2CDF1EC2E6110BB56 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_ImageComponent.h"; path = "../../../juce/modules/juce_gui_basics/widgets/juce_ImageComponent.h"; sourceTree = "SOURCE_ROOT"; };
+		B39D1F9C2A6556C7E73F7B0A = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_DirectoryIterator.cpp"; path = "../../../juce/modules/juce_core/files/juce_DirectoryIterator.cpp"; sourceTree = "SOURCE_ROOT"; };
+		B3F47C8187CE05809A994637 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_OutputStream.h"; path = "../../../juce/modules/juce_core/streams/juce_OutputStream.h"; sourceTree = "SOURCE_ROOT"; };
+		B464F76711F6B9CBB66DA3EF = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_QuickTimeAudioFormat.cpp"; path = "../../../juce/modules/juce_audio_formats/codecs/juce_QuickTimeAudioFormat.cpp"; sourceTree = "SOURCE_ROOT"; };
+		B5053EFFA070EFE91763D1CF = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_GZIPDecompressorInputStream.h"; path = "../../../juce/modules/juce_core/zip/juce_GZIPDecompressorInputStream.h"; sourceTree = "SOURCE_ROOT"; };
+		B50647653535F8801D7FF83D = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_RelativeCoordinatePositioner.h"; path = "../../../juce/modules/juce_gui_basics/positioning/juce_RelativeCoordinatePositioner.h"; sourceTree = "SOURCE_ROOT"; };
+		B676B0C4543E418E31B2607F = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_Message.h"; path = "../../../juce/modules/juce_events/messages/juce_Message.h"; sourceTree = "SOURCE_ROOT"; };
+		B67D221133A97DC1C2C83C6F = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_OpenGLImage.h"; path = "../../../juce/modules/juce_opengl/opengl/juce_OpenGLImage.h"; sourceTree = "SOURCE_ROOT"; };
+		B6DCD7E8C9C5C9EA58C5EF9C = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = MainWindow.h; path = ../../Source/GUI/MainWindow.h; sourceTree = "SOURCE_ROOT"; };
+		B6E00FB0259FB8560B8EB8B2 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_MessageManager.cpp"; path = "../../../juce/modules/juce_events/messages/juce_MessageManager.cpp"; sourceTree = "SOURCE_ROOT"; };
+		B747A4B93EAA22A264B3D24C = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_FlacAudioFormat.cpp"; path = "../../../juce/modules/juce_audio_formats/codecs/juce_FlacAudioFormat.cpp"; sourceTree = "SOURCE_ROOT"; };
+		B7497C02BCD8C01280DA32B2 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = KeyPositionTracker.cpp; path = ../../Source/TouchKeys/KeyPositionTracker.cpp; sourceTree = "SOURCE_ROOT"; };
+		B76B16D0041E8AF65ECBA8D2 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_IIRFilter.cpp"; path = "../../../juce/modules/juce_audio_basics/effects/juce_IIRFilter.cpp"; sourceTree = "SOURCE_ROOT"; };
+		B785A355901939FBFC107581 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_Vector3D.h"; path = "../../../juce/modules/juce_opengl/opengl/juce_Vector3D.h"; sourceTree = "SOURCE_ROOT"; };
+		B78F0674BC78DF9DAE4FEB7D = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_MouseEvent.h"; path = "../../../juce/modules/juce_gui_basics/mouse/juce_MouseEvent.h"; sourceTree = "SOURCE_ROOT"; };
+		B8CB4838AFDE0B20C61A7248 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_LookAndFeel.cpp"; path = "../../../juce/modules/juce_gui_basics/lookandfeel/juce_LookAndFeel.cpp"; sourceTree = "SOURCE_ROOT"; };
+		B8DF81CC9D4E0BE71C3AF558 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; name = "juce_mac_WebBrowserComponent.mm"; path = "../../../juce/modules/juce_gui_extra/native/juce_mac_WebBrowserComponent.mm"; sourceTree = "SOURCE_ROOT"; };
+		B8F2014DEA90E3849192C12D = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_XmlElement.h"; path = "../../../juce/modules/juce_core/xml/juce_XmlElement.h"; sourceTree = "SOURCE_ROOT"; };
+		B928D73E2A712404DA6A9D39 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_PopupMenu.h"; path = "../../../juce/modules/juce_gui_basics/menus/juce_PopupMenu.h"; sourceTree = "SOURCE_ROOT"; };
+		B954FAFC45D231B388305CBB = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_OggVorbisAudioFormat.cpp"; path = "../../../juce/modules/juce_audio_formats/codecs/juce_OggVorbisAudioFormat.cpp"; sourceTree = "SOURCE_ROOT"; };
+		B967DAA2B052D328B12C7DE8 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_Synthesiser.cpp"; path = "../../../juce/modules/juce_audio_basics/synthesisers/juce_Synthesiser.cpp"; sourceTree = "SOURCE_ROOT"; };
+		B9683DFC5C73C8EA097C0C87 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_RelativeCoordinate.h"; path = "../../../juce/modules/juce_gui_basics/positioning/juce_RelativeCoordinate.h"; sourceTree = "SOURCE_ROOT"; };
+		B97766EAEBB55185F51ED7B4 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_SparseSet.h"; path = "../../../juce/modules/juce_core/containers/juce_SparseSet.h"; sourceTree = "SOURCE_ROOT"; };
+		B9A42558DE8A295EAF05E9A4 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_ScopedPointer.h"; path = "../../../juce/modules/juce_core/memory/juce_ScopedPointer.h"; sourceTree = "SOURCE_ROOT"; };
+		B9E1B02E2D61CD5F0C3BA92E = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_Identifier.h"; path = "../../../juce/modules/juce_core/text/juce_Identifier.h"; sourceTree = "SOURCE_ROOT"; };
+		BA13CD1B8B5B44B8E675F743 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_TemporaryFile.cpp"; path = "../../../juce/modules/juce_core/files/juce_TemporaryFile.cpp"; sourceTree = "SOURCE_ROOT"; };
+		BA2C6578AE3F6F2B82656B4F = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = MappingListComponent.cpp; path = ../../Source/GUI/MappingListComponent.cpp; sourceTree = "SOURCE_ROOT"; };
+		BA5449FDC0526DDC94874CC7 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = TouchkeyControlMappingFactory.h; path = ../../Source/Mappings/Control/TouchkeyControlMappingFactory.h; sourceTree = "SOURCE_ROOT"; };
+		BA641413905F4396597FDF78 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; name = "juce_audio_devices.mm"; path = "../../../juce/modules/juce_audio_devices/juce_audio_devices.mm"; sourceTree = "SOURCE_ROOT"; };
+		BA8405E8F85F9A3F9EB253FD = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_UnitTest.h"; path = "../../../juce/modules/juce_core/unit_tests/juce_UnitTest.h"; sourceTree = "SOURCE_ROOT"; };
+		BA84C1198E0DEF50764C7D0F = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_BooleanPropertyComponent.h"; path = "../../../juce/modules/juce_gui_basics/properties/juce_BooleanPropertyComponent.h"; sourceTree = "SOURCE_ROOT"; };
+		BAC6DC0A9324D08F4675165D = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_linux_Network.cpp"; path = "../../../juce/modules/juce_core/native/juce_linux_Network.cpp"; sourceTree = "SOURCE_ROOT"; };
+		BB571C0DB322805E8CC5FAFC = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_PreferencesPanel.h"; path = "../../../juce/modules/juce_gui_extra/misc/juce_PreferencesPanel.h"; sourceTree = "SOURCE_ROOT"; };
+		BB8F5735542B54CA9DE6F353 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_ActiveXControlComponent.h"; path = "../../../juce/modules/juce_gui_extra/embedding/juce_ActiveXControlComponent.h"; sourceTree = "SOURCE_ROOT"; };
+		BBF99FBA82BF98E0CDA1F0C0 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_GIFLoader.cpp"; path = "../../../juce/modules/juce_graphics/image_formats/juce_GIFLoader.cpp"; sourceTree = "SOURCE_ROOT"; };
+		BC0A05397FE514F1AE6B3436 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = AppConfig.h; path = ../../JuceLibraryCode/AppConfig.h; sourceTree = "SOURCE_ROOT"; };
+		BC52700DC6BCBBA42635510D = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_Variant.cpp"; path = "../../../juce/modules/juce_core/containers/juce_Variant.cpp"; sourceTree = "SOURCE_ROOT"; };
+		BC64F550F0D3B5C2DD546574 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_data_structures.h"; path = "../../../juce/modules/juce_data_structures/juce_data_structures.h"; sourceTree = "SOURCE_ROOT"; };
+		BD20E908DEABE97BC3CC4F07 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_WaitableEvent.h"; path = "../../../juce/modules/juce_core/threads/juce_WaitableEvent.h"; sourceTree = "SOURCE_ROOT"; };
+		BD8254A682953545FA36F552 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = TouchkeyOnsetAngleMapping.h; path = ../../Source/Mappings/OnsetAngle/TouchkeyOnsetAngleMapping.h; sourceTree = "SOURCE_ROOT"; };
+		BDA6E687818B0F2DC22337CB = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_KeyMappingEditorComponent.cpp"; path = "../../../juce/modules/juce_gui_extra/misc/juce_KeyMappingEditorComponent.cpp"; sourceTree = "SOURCE_ROOT"; };
+		BDB6848D33CE535EBCA3B9AD = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = MappingFactorySplitter.h; path = ../../Source/Mappings/MappingFactorySplitter.h; sourceTree = "SOURCE_ROOT"; };
+		BDBFE5792BFFFEC6DD9C51AD = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_Expression.h"; path = "../../../juce/modules/juce_core/maths/juce_Expression.h"; sourceTree = "SOURCE_ROOT"; };
+		BDD4F9A4B8D15A1515F87F53 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_WildcardFileFilter.cpp"; path = "../../../juce/modules/juce_gui_basics/filebrowser/juce_WildcardFileFilter.cpp"; sourceTree = "SOURCE_ROOT"; };
+		BDDEA8330D6309B2B97A55A6 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_MouseCursor.h"; path = "../../../juce/modules/juce_gui_basics/mouse/juce_MouseCursor.h"; sourceTree = "SOURCE_ROOT"; };
+		BE2ACC20FF51D7702D22D3B4 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_freetype_Fonts.cpp"; path = "../../../juce/modules/juce_graphics/native/juce_freetype_Fonts.cpp"; sourceTree = "SOURCE_ROOT"; };
+		BE91DB4CF2406F37AF7E7D67 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = ControlWindowMainComponent.h; path = ../../Source/GUI/ControlWindowMainComponent.h; sourceTree = "SOURCE_ROOT"; };
+		BF01A2533D2222C856D3DB3A = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = MRPMapping.h; path = ../../Source/Mappings/MRPMapping.h; sourceTree = "SOURCE_ROOT"; };
+		BF7B348D8CDF033289AA95AD = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_StandardHeader.h"; path = "../../../juce/modules/juce_core/system/juce_StandardHeader.h"; sourceTree = "SOURCE_ROOT"; };
+		BFBC57AA124FBB8C1C51CB59 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = PianoTypes.h; path = ../../Source/TouchKeys/PianoTypes.h; sourceTree = "SOURCE_ROOT"; };
+		BFD6F76FF80DE8CB6D83D18D = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_FileOutputStream.h"; path = "../../../juce/modules/juce_core/files/juce_FileOutputStream.h"; sourceTree = "SOURCE_ROOT"; };
+		C18D5C01F2C345BC7775FABD = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_win32_Fonts.cpp"; path = "../../../juce/modules/juce_graphics/native/juce_win32_Fonts.cpp"; sourceTree = "SOURCE_ROOT"; };
+		C18FB4B3120557AA4EAFF405 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_CodeDocument.cpp"; path = "../../../juce/modules/juce_gui_extra/code_editor/juce_CodeDocument.cpp"; sourceTree = "SOURCE_ROOT"; };
+		C213BD9455B5E42E62AF260A = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_MouseInputSource.cpp"; path = "../../../juce/modules/juce_gui_basics/mouse/juce_MouseInputSource.cpp"; sourceTree = "SOURCE_ROOT"; };
+		C25315D4D50046938BDE18D8 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_ImageConvolutionKernel.h"; path = "../../../juce/modules/juce_graphics/images/juce_ImageConvolutionKernel.h"; sourceTree = "SOURCE_ROOT"; };
+		C266C85403BF3646B729144D = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = OpenGLDisplayBase.h; path = ../../Source/Display/OpenGLDisplayBase.h; sourceTree = "SOURCE_ROOT"; };
+		C2673A6A3D247F3CDCF66FF0 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_ScopedWriteLock.h"; path = "../../../juce/modules/juce_core/threads/juce_ScopedWriteLock.h"; sourceTree = "SOURCE_ROOT"; };
+		C26E6685AAEC528FC835CA5C = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_ComponentBuilder.h"; path = "../../../juce/modules/juce_gui_basics/layout/juce_ComponentBuilder.h"; sourceTree = "SOURCE_ROOT"; };
+		C2762F0E696E257A507595A5 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = MRPMapping.cpp; path = ../../Source/Mappings/MRPMapping.cpp; sourceTree = "SOURCE_ROOT"; };
+		C2D442FDEECC11C9BD433379 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = KeyboardDisplay.cpp; path = ../../Source/Display/KeyboardDisplay.cpp; sourceTree = "SOURCE_ROOT"; };
+		C31F902A006BD5C9BD9259B3 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_AudioIODeviceType.h"; path = "../../../juce/modules/juce_audio_devices/audio_io/juce_AudioIODeviceType.h"; sourceTree = "SOURCE_ROOT"; };
+		C3AAD3FBAACDA8F0B7BB8FF9 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_BubbleMessageComponent.h"; path = "../../../juce/modules/juce_gui_extra/misc/juce_BubbleMessageComponent.h"; sourceTree = "SOURCE_ROOT"; };
+		C469456ED7E4039CF8BCB26F = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_MidiBuffer.h"; path = "../../../juce/modules/juce_audio_basics/midi/juce_MidiBuffer.h"; sourceTree = "SOURCE_ROOT"; };
+		C4ADD63DE594AD3A82825DD4 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_Initialisation.h"; path = "../../../juce/modules/juce_events/messages/juce_Initialisation.h"; sourceTree = "SOURCE_ROOT"; };
+		C4D3105688518F04E8D6D591 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_LookAndFeel_V1.h"; path = "../../../juce/modules/juce_gui_basics/lookandfeel/juce_LookAndFeel_V1.h"; sourceTree = "SOURCE_ROOT"; };
+		C5032AA8AFA0CF6414D26DDE = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; name = "juce_mac_Threads.mm"; path = "../../../juce/modules/juce_core/native/juce_mac_Threads.mm"; sourceTree = "SOURCE_ROOT"; };
+		C548DA4DD5135F3EDFF7589D = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_events.h"; path = "../../../juce/modules/juce_events/juce_events.h"; sourceTree = "SOURCE_ROOT"; };
+		C72E037FB7147B081510B13E = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_ActionListener.h"; path = "../../../juce/modules/juce_events/broadcasters/juce_ActionListener.h"; sourceTree = "SOURCE_ROOT"; };
+		C75FB456CF13DFDEEF28C277 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_Toolbar.h"; path = "../../../juce/modules/juce_gui_basics/widgets/juce_Toolbar.h"; sourceTree = "SOURCE_ROOT"; };
+		C804C201CC2FADE94FA73FAD = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_Expression.cpp"; path = "../../../juce/modules/juce_core/maths/juce_Expression.cpp"; sourceTree = "SOURCE_ROOT"; };
+		C8204B6A7B83338403077010 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_DocumentWindow.cpp"; path = "../../../juce/modules/juce_gui_basics/windows/juce_DocumentWindow.cpp"; sourceTree = "SOURCE_ROOT"; };
+		C8288E63BC4A9120BE71066E = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_DrawableButton.cpp"; path = "../../../juce/modules/juce_gui_basics/buttons/juce_DrawableButton.cpp"; sourceTree = "SOURCE_ROOT"; };
+		C830A46F2626904CBD8E74CF = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_StringPool.h"; path = "../../../juce/modules/juce_core/text/juce_StringPool.h"; sourceTree = "SOURCE_ROOT"; };
+		C87D1CD195A69D0FC2BD3F33 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_win32_DragAndDrop.cpp"; path = "../../../juce/modules/juce_gui_basics/native/juce_win32_DragAndDrop.cpp"; sourceTree = "SOURCE_ROOT"; };
+		C89CDA180CC916AB5EEEE534 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_DrawableShape.h"; path = "../../../juce/modules/juce_gui_basics/drawables/juce_DrawableShape.h"; sourceTree = "SOURCE_ROOT"; };
+		C8C2FA6F235AB6AA44027FAE = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_OpenGL_android.h"; path = "../../../juce/modules/juce_opengl/native/juce_OpenGL_android.h"; sourceTree = "SOURCE_ROOT"; };
+		C8CEE210EF8AA5594AFB7C6F = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_FileInputSource.cpp"; path = "../../../juce/modules/juce_core/streams/juce_FileInputSource.cpp"; sourceTree = "SOURCE_ROOT"; };
+		C9A5595CCCAF91E88CA4C4B4 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_RelativeRectangle.cpp"; path = "../../../juce/modules/juce_gui_basics/positioning/juce_RelativeRectangle.cpp"; sourceTree = "SOURCE_ROOT"; };
+		CA08ADDF1800B59374067EBE = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_FilenameComponent.cpp"; path = "../../../juce/modules/juce_gui_basics/filebrowser/juce_FilenameComponent.cpp"; sourceTree = "SOURCE_ROOT"; };
+		CB26855B4D41689AC3C18F85 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_ApplicationProperties.cpp"; path = "../../../juce/modules/juce_data_structures/app_properties/juce_ApplicationProperties.cpp"; sourceTree = "SOURCE_ROOT"; };
+		CBB0FB63AD659D3F838318AB = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_DrawableComposite.h"; path = "../../../juce/modules/juce_gui_basics/drawables/juce_DrawableComposite.h"; sourceTree = "SOURCE_ROOT"; };
+		CC0D3E1F3FAAF5B18C3FFDDF = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_BooleanPropertyComponent.cpp"; path = "../../../juce/modules/juce_gui_basics/properties/juce_BooleanPropertyComponent.cpp"; sourceTree = "SOURCE_ROOT"; };
+		CC659F7C5C7BBB1483D22927 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_android_WebBrowserComponent.cpp"; path = "../../../juce/modules/juce_gui_extra/native/juce_android_WebBrowserComponent.cpp"; sourceTree = "SOURCE_ROOT"; };
+		CCDD506916477B1ECDA100D1 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_android_GraphicsContext.cpp"; path = "../../../juce/modules/juce_graphics/native/juce_android_GraphicsContext.cpp"; sourceTree = "SOURCE_ROOT"; };
+		CD1916C626761395E7729958 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_ChannelRemappingAudioSource.h"; path = "../../../juce/modules/juce_audio_basics/sources/juce_ChannelRemappingAudioSource.h"; sourceTree = "SOURCE_ROOT"; };
+		CD5A70F1BEBD963AA994F53E = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_RelativeCoordinatePositioner.cpp"; path = "../../../juce/modules/juce_gui_basics/positioning/juce_RelativeCoordinatePositioner.cpp"; sourceTree = "SOURCE_ROOT"; };
+		CDCE769E18A546EC7FE65FE0 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_ThreadWithProgressWindow.cpp"; path = "../../../juce/modules/juce_gui_basics/windows/juce_ThreadWithProgressWindow.cpp"; sourceTree = "SOURCE_ROOT"; };
+		CE4297701F6471B19A8DB984 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_ApplicationCommandManager.h"; path = "../../../juce/modules/juce_gui_basics/commands/juce_ApplicationCommandManager.h"; sourceTree = "SOURCE_ROOT"; };
+		CE4BCD27BDF96500D2F89936 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_Uuid.h"; path = "../../../juce/modules/juce_core/misc/juce_Uuid.h"; sourceTree = "SOURCE_ROOT"; };
+		CE4F2F551165086D73459E43 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_HighResolutionTimer.cpp"; path = "../../../juce/modules/juce_core/threads/juce_HighResolutionTimer.cpp"; sourceTree = "SOURCE_ROOT"; };
+		CE5071065642A70419650EB3 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_MultiTimer.cpp"; path = "../../../juce/modules/juce_events/timers/juce_MultiTimer.cpp"; sourceTree = "SOURCE_ROOT"; };
+		CE6B0F39A803962ACD879D85 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_JPEGLoader.cpp"; path = "../../../juce/modules/juce_graphics/image_formats/juce_JPEGLoader.cpp"; sourceTree = "SOURCE_ROOT"; };
+		CED1159B6F39600DE0520C82 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_OggVorbisAudioFormat.h"; path = "../../../juce/modules/juce_audio_formats/codecs/juce_OggVorbisAudioFormat.h"; sourceTree = "SOURCE_ROOT"; };
+		CEE095838CD8EDB961A05ACA = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_MidiKeyboardState.cpp"; path = "../../../juce/modules/juce_audio_basics/midi/juce_MidiKeyboardState.cpp"; sourceTree = "SOURCE_ROOT"; };
+		CF1E1CAA10DC6E090133245E = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_audio_devices.h"; path = "../../../juce/modules/juce_audio_devices/juce_audio_devices.h"; sourceTree = "SOURCE_ROOT"; };
+		CF273B4AE1819D5344866606 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_TargetPlatform.h"; path = "../../../juce/modules/juce_core/system/juce_TargetPlatform.h"; sourceTree = "SOURCE_ROOT"; };
+		D000F79ED732A055BC94D285 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_AffineTransform.h"; path = "../../../juce/modules/juce_graphics/geometry/juce_AffineTransform.h"; sourceTree = "SOURCE_ROOT"; };
+		D00A8C703C2C0055DD2B790C = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_CachedComponentImage.h"; path = "../../../juce/modules/juce_gui_basics/components/juce_CachedComponentImage.h"; sourceTree = "SOURCE_ROOT"; };
+		D0868A6AA9193202A16F7B5D = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; name = "juce_mac_MainMenu.mm"; path = "../../../juce/modules/juce_gui_basics/native/juce_mac_MainMenu.mm"; sourceTree = "SOURCE_ROOT"; };
+		D16ADF8AF26851DFFF4AE731 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_EdgeTable.cpp"; path = "../../../juce/modules/juce_graphics/geometry/juce_EdgeTable.cpp"; sourceTree = "SOURCE_ROOT"; };
+		D2C3D422A9FFB418D71B1FCA = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_TimeSliceThread.cpp"; path = "../../../juce/modules/juce_core/threads/juce_TimeSliceThread.cpp"; sourceTree = "SOURCE_ROOT"; };
+		D2CE1FE912067925F5ACA619 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_ModifierKeys.cpp"; path = "../../../juce/modules/juce_gui_basics/keyboard/juce_ModifierKeys.cpp"; sourceTree = "SOURCE_ROOT"; };
+		D30002F0500DBE972234F381 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_RelativePointPath.cpp"; path = "../../../juce/modules/juce_gui_basics/positioning/juce_RelativePointPath.cpp"; sourceTree = "SOURCE_ROOT"; };
+		D3AB19332A0AE6C6A82612EF = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_SelectedItemSet.h"; path = "../../../juce/modules/juce_gui_basics/mouse/juce_SelectedItemSet.h"; sourceTree = "SOURCE_ROOT"; };
+		D3C368F0138FE8F3F0B7540F = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_IPAddress.h"; path = "../../../juce/modules/juce_core/network/juce_IPAddress.h"; sourceTree = "SOURCE_ROOT"; };
+		D3C722814093C96312625553 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_StringPairArray.cpp"; path = "../../../juce/modules/juce_core/text/juce_StringPairArray.cpp"; sourceTree = "SOURCE_ROOT"; };
+		D3FE20DFB95B39D949C6B842 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_LowLevelGraphicsPostScriptRenderer.cpp"; path = "../../../juce/modules/juce_graphics/contexts/juce_LowLevelGraphicsPostScriptRenderer.cpp"; sourceTree = "SOURCE_ROOT"; };
+		D42A52316423A9F531FFC765 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_android_Network.cpp"; path = "../../../juce/modules/juce_core/native/juce_android_Network.cpp"; sourceTree = "SOURCE_ROOT"; };
+		D4D1E893C5144CF41D381E0E = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; name = "juce_mac_AppleRemote.mm"; path = "../../../juce/modules/juce_gui_extra/native/juce_mac_AppleRemote.mm"; sourceTree = "SOURCE_ROOT"; };
+		D5B0943362643EC0EE867701 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_KeyboardFocusTraverser.h"; path = "../../../juce/modules/juce_gui_basics/keyboard/juce_KeyboardFocusTraverser.h"; sourceTree = "SOURCE_ROOT"; };
+		D6272EF2E6816B659ABFBDA6 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_linux_WebBrowserComponent.cpp"; path = "../../../juce/modules/juce_gui_extra/native/juce_linux_WebBrowserComponent.cpp"; sourceTree = "SOURCE_ROOT"; };
+		D6A25A64B90CA2125D9D31B8 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_win32_SystemTrayIcon.cpp"; path = "../../../juce/modules/juce_gui_extra/native/juce_win32_SystemTrayIcon.cpp"; sourceTree = "SOURCE_ROOT"; };
+		D6C4019A40A11C1B95FF07BB = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_DrawableShape.cpp"; path = "../../../juce/modules/juce_gui_basics/drawables/juce_DrawableShape.cpp"; sourceTree = "SOURCE_ROOT"; };
+		D6D6D17FF1925E76CE1F24E4 = { isa = PBXFileReference; lastKnownFileType = file; name = "juce_module_info"; path = "../../../juce/modules/juce_graphics/juce_module_info"; sourceTree = "SOURCE_ROOT"; };
+		D74EA156B75DFD28AB6BD8D0 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_Thread.cpp"; path = "../../../juce/modules/juce_core/threads/juce_Thread.cpp"; sourceTree = "SOURCE_ROOT"; };
+		D7752214A0701D59E34DC038 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_WindowsMediaAudioFormat.h"; path = "../../../juce/modules/juce_audio_formats/codecs/juce_WindowsMediaAudioFormat.h"; sourceTree = "SOURCE_ROOT"; };
+		D850695B102730FCFF698E4D = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_RelativeRectangle.h"; path = "../../../juce/modules/juce_gui_basics/positioning/juce_RelativeRectangle.h"; sourceTree = "SOURCE_ROOT"; };
+		D85AA9748C07062C4898B172 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_ZipFile.h"; path = "../../../juce/modules/juce_core/zip/juce_ZipFile.h"; sourceTree = "SOURCE_ROOT"; };
+		D867F0F9AE014AC4873F4093 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_XmlDocument.h"; path = "../../../juce/modules/juce_core/xml/juce_XmlDocument.h"; sourceTree = "SOURCE_ROOT"; };
+		D967F911BFDE7E5B482F5A5F = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_PropertyPanel.cpp"; path = "../../../juce/modules/juce_gui_basics/properties/juce_PropertyPanel.cpp"; sourceTree = "SOURCE_ROOT"; };
+		D999C1E99812B5D0C9F7519E = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_RelativePoint.h"; path = "../../../juce/modules/juce_gui_basics/positioning/juce_RelativePoint.h"; sourceTree = "SOURCE_ROOT"; };
+		D9FD13866909F3134B4AB740 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = Mapping.h; path = ../../Source/Mappings/Mapping.h; sourceTree = "SOURCE_ROOT"; };
+		DA596B63735FF691A714D004 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_graphics.h"; path = "../../../juce/modules/juce_graphics/juce_graphics.h"; sourceTree = "SOURCE_ROOT"; };
+		DA76EEB0BD9183E2006CE9EB = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_LookAndFeel_V2.cpp"; path = "../../../juce/modules/juce_gui_basics/lookandfeel/juce_LookAndFeel_V2.cpp"; sourceTree = "SOURCE_ROOT"; };
+		DB09B4EA350E52FB74BAAD9D = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_Component.cpp"; path = "../../../juce/modules/juce_gui_basics/components/juce_Component.cpp"; sourceTree = "SOURCE_ROOT"; };
+		DBD3326F4F7F5F41D3B7EA99 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; name = "juce_mac_Files.mm"; path = "../../../juce/modules/juce_core/native/juce_mac_Files.mm"; sourceTree = "SOURCE_ROOT"; };
+		DBDB2CBDB7DD3D7B9713D4C5 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = Scheduler.cpp; path = ../../Source/Utility/Scheduler.cpp; sourceTree = "SOURCE_ROOT"; };
+		DDF271285760C5642E3D3346 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; name = "juce_ios_Windowing.mm"; path = "../../../juce/modules/juce_gui_basics/native/juce_ios_Windowing.mm"; sourceTree = "SOURCE_ROOT"; };
+		DF870F58DC21D8A032AE4D03 = { isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Carbon.framework; path = System/Library/Frameworks/Carbon.framework; sourceTree = SDKROOT; };
+		E263014FE404722FDDC437C8 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_CoreAudioFormat.h"; path = "../../../juce/modules/juce_audio_formats/codecs/juce_CoreAudioFormat.h"; sourceTree = "SOURCE_ROOT"; };
+		E9E267650C0230141C461A4B = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = ControlWindowMainComponent.cpp; path = ../../Source/GUI/ControlWindowMainComponent.cpp; sourceTree = "SOURCE_ROOT"; };
+		F07FDD832AD269D84A40DAF1 = { isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; };
+		90E8A67FBC9B5B91FEB780F5 = { isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TouchKeys.app; sourceTree = "BUILT_PRODUCTS_DIR"; };
+		D2F76A9A564C9C39C9110C7E = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_ComponentBuilder.cpp"; path = "../../../juce/modules/juce_gui_basics/layout/juce_ComponentBuilder.cpp"; sourceTree = "SOURCE_ROOT"; };
+		D3D34D4A8674E01CCE92CE65 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_AudioFormatWriter.h"; path = "../../../juce/modules/juce_audio_formats/format/juce_AudioFormatWriter.h"; sourceTree = "SOURCE_ROOT"; };
+		D3F247C3C568453665FD300D = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = LineSegment.h; path = ../../Source/Utility/LineSegment.h; sourceTree = "SOURCE_ROOT"; };
+		D70B19E3DE0323FFA2EFCBBF = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_ComponentMovementWatcher.h"; path = "../../../juce/modules/juce_gui_basics/layout/juce_ComponentMovementWatcher.h"; sourceTree = "SOURCE_ROOT"; };
+		D75E1147AF76C62DC23E7B18 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_KeyPress.h"; path = "../../../juce/modules/juce_gui_basics/keyboard/juce_KeyPress.h"; sourceTree = "SOURCE_ROOT"; };
+		DBC98946C2E19791899587BD = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_ToggleButton.h"; path = "../../../juce/modules/juce_gui_basics/buttons/juce_ToggleButton.h"; sourceTree = "SOURCE_ROOT"; };
+		DBE3D6F70DF4558463C24395 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_DrawableText.cpp"; path = "../../../juce/modules/juce_gui_basics/drawables/juce_DrawableText.cpp"; sourceTree = "SOURCE_ROOT"; };
+		DC7333AE4FD5C16D3B49EE77 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_win32_ActiveXComponent.cpp"; path = "../../../juce/modules/juce_gui_extra/native/juce_win32_ActiveXComponent.cpp"; sourceTree = "SOURCE_ROOT"; };
+		DD18A8B0C1A5F8BDA30BAA50 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = OscMidiConverter.h; path = ../../Source/TouchKeys/OscMidiConverter.h; sourceTree = "SOURCE_ROOT"; };
+		DDC862F5672CFD8794EA65BC = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_MissingGLDefinitions.h"; path = "../../../juce/modules/juce_opengl/native/juce_MissingGLDefinitions.h"; sourceTree = "SOURCE_ROOT"; };
+		DE68B78DB869AC78BCBD1214 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_DrawableText.h"; path = "../../../juce/modules/juce_gui_basics/drawables/juce_DrawableText.h"; sourceTree = "SOURCE_ROOT"; };
+		DE6A8EF9DD39C0260ADD27B0 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = TouchkeyVibratoMapping.cpp; path = ../../Source/Mappings/Vibrato/TouchkeyVibratoMapping.cpp; sourceTree = "SOURCE_ROOT"; };
+		DE7280B3F30E33133756C534 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_linux_Messaging.cpp"; path = "../../../juce/modules/juce_events/native/juce_linux_Messaging.cpp"; sourceTree = "SOURCE_ROOT"; };
+		DF0B3A4D5C217DA0283223BF = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_ColourSelector.h"; path = "../../../juce/modules/juce_gui_extra/misc/juce_ColourSelector.h"; sourceTree = "SOURCE_ROOT"; };
+		DF22553364728350DAFA6267 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_MenuBarComponent.h"; path = "../../../juce/modules/juce_gui_basics/menus/juce_MenuBarComponent.h"; sourceTree = "SOURCE_ROOT"; };
+		DF2B5B4B8D7948C9EF4882A7 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = MidiInputController.h; path = ../../Source/TouchKeys/MidiInputController.h; sourceTree = "SOURCE_ROOT"; };
+		DF35E5C3BC9627D808F927E6 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_MouseEvent.cpp"; path = "../../../juce/modules/juce_gui_basics/mouse/juce_MouseEvent.cpp"; sourceTree = "SOURCE_ROOT"; };
+		E0FB878B8DB0FE5B526BB950 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_ShapeButton.h"; path = "../../../juce/modules/juce_gui_basics/buttons/juce_ShapeButton.h"; sourceTree = "SOURCE_ROOT"; };
+		E185507E8E49A513D5E6894A = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_HighResolutionTimer.h"; path = "../../../juce/modules/juce_core/threads/juce_HighResolutionTimer.h"; sourceTree = "SOURCE_ROOT"; };
+		E1B8E23E7491C09D6708018C = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; name = "juce_mac_NSViewComponentPeer.mm"; path = "../../../juce/modules/juce_gui_basics/native/juce_mac_NSViewComponentPeer.mm"; sourceTree = "SOURCE_ROOT"; };
+		E1D304ED9044C0CE62C7B3AD = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_WildcardFileFilter.h"; path = "../../../juce/modules/juce_gui_basics/filebrowser/juce_WildcardFileFilter.h"; sourceTree = "SOURCE_ROOT"; };
+		E2322B995208819DF175E332 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_mac_CoreGraphicsContext.h"; path = "../../../juce/modules/juce_graphics/native/juce_mac_CoreGraphicsContext.h"; sourceTree = "SOURCE_ROOT"; };
+		E2EE80FAFA0DADF6D8AD8EA0 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_OpenGL_win32.h"; path = "../../../juce/modules/juce_opengl/native/juce_OpenGL_win32.h"; sourceTree = "SOURCE_ROOT"; };
+		E2F713FF46DF610A87C64265 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_ButtonPropertyComponent.cpp"; path = "../../../juce/modules/juce_gui_basics/properties/juce_ButtonPropertyComponent.cpp"; sourceTree = "SOURCE_ROOT"; };
+		E32397CB64E42DA5119CC3E8 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_FillType.h"; path = "../../../juce/modules/juce_graphics/colour/juce_FillType.h"; sourceTree = "SOURCE_ROOT"; };
+		E327C96B7AB06A125C9764FC = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_AbstractFifo.cpp"; path = "../../../juce/modules/juce_core/containers/juce_AbstractFifo.cpp"; sourceTree = "SOURCE_ROOT"; };
+		E389B103581AD847C754E978 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_Line.h"; path = "../../../juce/modules/juce_graphics/geometry/juce_Line.h"; sourceTree = "SOURCE_ROOT"; };
+		E38A23A31D7FB8E86851666B = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = MidiInputController.cpp; path = ../../Source/TouchKeys/MidiInputController.cpp; sourceTree = "SOURCE_ROOT"; };
+		E392DA2568D4143CE19A9A2F = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_Quaternion.h"; path = "../../../juce/modules/juce_opengl/opengl/juce_Quaternion.h"; sourceTree = "SOURCE_ROOT"; };
+		E393747AE555F946712380D5 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_FileSearchPathListComponent.cpp"; path = "../../../juce/modules/juce_gui_basics/filebrowser/juce_FileSearchPathListComponent.cpp"; sourceTree = "SOURCE_ROOT"; };
+		E3DA2BE88F2738CCDEDDF3AD = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_Timer.h"; path = "../../../juce/modules/juce_events/timers/juce_Timer.h"; sourceTree = "SOURCE_ROOT"; };
+		E46A2AA59E72BCA0D0891C27 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_TooltipClient.h"; path = "../../../juce/modules/juce_gui_basics/mouse/juce_TooltipClient.h"; sourceTree = "SOURCE_ROOT"; };
+		E4F3ACBA5181C0AF7021EE38 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_AudioSourcePlayer.cpp"; path = "../../../juce/modules/juce_audio_devices/sources/juce_AudioSourcePlayer.cpp"; sourceTree = "SOURCE_ROOT"; };
+		E56B94B61B89BBBD6510D713 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_TextDiff.h"; path = "../../../juce/modules/juce_core/text/juce_TextDiff.h"; sourceTree = "SOURCE_ROOT"; };
+		E583018026996A67748B9630 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_GZIPCompressorOutputStream.cpp"; path = "../../../juce/modules/juce_core/zip/juce_GZIPCompressorOutputStream.cpp"; sourceTree = "SOURCE_ROOT"; };
+		E5F79D13171E0F28729529DD = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = BinaryData.cpp; path = ../../JuceLibraryCode/BinaryData.cpp; sourceTree = "SOURCE_ROOT"; };
+		E62E694F0D19A252A6B9EAA1 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_HeapBlock.h"; path = "../../../juce/modules/juce_core/memory/juce_HeapBlock.h"; sourceTree = "SOURCE_ROOT"; };
+		E68F5154CAF362F281F55A1A = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_MidiMessage.cpp"; path = "../../../juce/modules/juce_audio_basics/midi/juce_MidiMessage.cpp"; sourceTree = "SOURCE_ROOT"; };
+		E6908DE88A1A409B5E35CC37 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_ResizableBorderComponent.cpp"; path = "../../../juce/modules/juce_gui_basics/layout/juce_ResizableBorderComponent.cpp"; sourceTree = "SOURCE_ROOT"; };
+		E6B9CFC25E8B762EB17568ED = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_win32_SystemStats.cpp"; path = "../../../juce/modules/juce_core/native/juce_win32_SystemStats.cpp"; sourceTree = "SOURCE_ROOT"; };
+		E7043CF11AB43042A6725FDC = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_SliderPropertyComponent.cpp"; path = "../../../juce/modules/juce_gui_basics/properties/juce_SliderPropertyComponent.cpp"; sourceTree = "SOURCE_ROOT"; };
+		E73EB83A03FED6C1036C85A6 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_ColourGradient.h"; path = "../../../juce/modules/juce_graphics/colour/juce_ColourGradient.h"; sourceTree = "SOURCE_ROOT"; };
+		E7AF7E2ECFDAFF3CB186986D = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_KeyMappingEditorComponent.h"; path = "../../../juce/modules/juce_gui_extra/misc/juce_KeyMappingEditorComponent.h"; sourceTree = "SOURCE_ROOT"; };
+		E7B6750E6B2616D84773B7D0 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = MidiKeyboardSegment.h; path = ../../Source/TouchKeys/MidiKeyboardSegment.h; sourceTree = "SOURCE_ROOT"; };
+		E7F83771B734D5CCAC7AD391 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_linux_Threads.cpp"; path = "../../../juce/modules/juce_core/native/juce_linux_Threads.cpp"; sourceTree = "SOURCE_ROOT"; };
+		E830ACDAC41A38A04A16E2CD = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_OpenGLRenderer.h"; path = "../../../juce/modules/juce_opengl/opengl/juce_OpenGLRenderer.h"; sourceTree = "SOURCE_ROOT"; };
+		E88D828B769B0DB0DC1D624B = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_ContainerDeletePolicy.h"; path = "../../../juce/modules/juce_core/memory/juce_ContainerDeletePolicy.h"; sourceTree = "SOURCE_ROOT"; };
+		E8D2B3CBDF5DED9EF0E0C63B = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_ToneGeneratorAudioSource.cpp"; path = "../../../juce/modules/juce_audio_basics/sources/juce_ToneGeneratorAudioSource.cpp"; sourceTree = "SOURCE_ROOT"; };
+		E8EDACD332FC3BDFC3F0E6E1 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = Recorder.h; path = ../../Source/Utility/Recorder.h; sourceTree = "SOURCE_ROOT"; };
+		E91EEAE6F09A108DBF5E9BC5 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_win32_DirectSound.cpp"; path = "../../../juce/modules/juce_audio_devices/native/juce_win32_DirectSound.cpp"; sourceTree = "SOURCE_ROOT"; };
+		E9350A4235E42DBC9987725A = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_FileBrowserListener.h"; path = "../../../juce/modules/juce_gui_basics/filebrowser/juce_FileBrowserListener.h"; sourceTree = "SOURCE_ROOT"; };
+		E93D98571147B0AD0B0D42C1 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_StringArray.h"; path = "../../../juce/modules/juce_core/text/juce_StringArray.h"; sourceTree = "SOURCE_ROOT"; };
+		E9E29024302023AEC889C4E1 = { isa = PBXFileReference; lastKnownFileType = file; name = "juce_module_info"; path = "../../../juce/modules/juce_core/juce_module_info"; sourceTree = "SOURCE_ROOT"; };
+		EA085C2056A44FA5A7EE2AFE = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_Slider.h"; path = "../../../juce/modules/juce_gui_basics/widgets/juce_Slider.h"; sourceTree = "SOURCE_ROOT"; };
+		EA23F0162B24A0A0FD58B0B2 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_Reverb.h"; path = "../../../juce/modules/juce_audio_basics/effects/juce_Reverb.h"; sourceTree = "SOURCE_ROOT"; };
+		EA47BDE992F39919F3EF53EC = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = PianoPedal.h; path = ../../Source/TouchKeys/PianoPedal.h; sourceTree = "SOURCE_ROOT"; };
+		EA983D5CAA5DB46A4EA83E19 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_NotificationType.h"; path = "../../../juce/modules/juce_events/messages/juce_NotificationType.h"; sourceTree = "SOURCE_ROOT"; };
+		EAC2B53916B041FEC59E050C = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_OpenGLImage.cpp"; path = "../../../juce/modules/juce_opengl/opengl/juce_OpenGLImage.cpp"; sourceTree = "SOURCE_ROOT"; };
+		EAEB8DD999D027A9F9C02FC0 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_Slider.cpp"; path = "../../../juce/modules/juce_gui_basics/widgets/juce_Slider.cpp"; sourceTree = "SOURCE_ROOT"; };
+		EB414D42BBBF2486ECE698D8 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_win32_WASAPI.cpp"; path = "../../../juce/modules/juce_audio_devices/native/juce_win32_WASAPI.cpp"; sourceTree = "SOURCE_ROOT"; };
+		EB977BE1676B103BE15C730C = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = TouchkeyReleaseAngleMapping.h; path = ../../Source/Mappings/ReleaseAngle/TouchkeyReleaseAngleMapping.h; sourceTree = "SOURCE_ROOT"; };
+		EB9EE29BC6E0F654EBDA140F = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_AudioFormatManager.h"; path = "../../../juce/modules/juce_audio_formats/format/juce_AudioFormatManager.h"; sourceTree = "SOURCE_ROOT"; };
+		EBA91A460FFAAFBC3EBEC36E = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_TabbedButtonBar.cpp"; path = "../../../juce/modules/juce_gui_basics/layout/juce_TabbedButtonBar.cpp"; sourceTree = "SOURCE_ROOT"; };
+		EBBA13C5C542DF0F8EFB826D = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_win32_Windowing.cpp"; path = "../../../juce/modules/juce_gui_basics/native/juce_win32_Windowing.cpp"; sourceTree = "SOURCE_ROOT"; };
+		EC3966AA0A0C2A80EBB36B82 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_GraphicsContext.h"; path = "../../../juce/modules/juce_graphics/contexts/juce_GraphicsContext.h"; sourceTree = "SOURCE_ROOT"; };
+		ECAB1F5B50EACFEBDFC11A40 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_win32_FileChooser.cpp"; path = "../../../juce/modules/juce_gui_basics/native/juce_win32_FileChooser.cpp"; sourceTree = "SOURCE_ROOT"; };
+		ED331E878A92AC3986F5885B = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_FileLogger.cpp"; path = "../../../juce/modules/juce_core/logging/juce_FileLogger.cpp"; sourceTree = "SOURCE_ROOT"; };
+		EE373F66DCFC442DBAADCF74 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_ios_Audio.cpp"; path = "../../../juce/modules/juce_audio_devices/native/juce_ios_Audio.cpp"; sourceTree = "SOURCE_ROOT"; };
+		EECB8BFB50475C0564D37EAD = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_PropertyPanel.h"; path = "../../../juce/modules/juce_gui_basics/properties/juce_PropertyPanel.h"; sourceTree = "SOURCE_ROOT"; };
+		EF092B37816526FF234B11F9 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_ComponentDragger.h"; path = "../../../juce/modules/juce_gui_basics/mouse/juce_ComponentDragger.h"; sourceTree = "SOURCE_ROOT"; };
+		EF60BF8888F0B0EA48E9FF09 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_UIViewComponent.h"; path = "../../../juce/modules/juce_gui_extra/embedding/juce_UIViewComponent.h"; sourceTree = "SOURCE_ROOT"; };
+		EF7D4F4824E5775A2D3801E5 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_GlowEffect.h"; path = "../../../juce/modules/juce_graphics/effects/juce_GlowEffect.h"; sourceTree = "SOURCE_ROOT"; };
+		EF8E10BB5A52F89097F763D9 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_ApplicationBase.cpp"; path = "../../../juce/modules/juce_events/messages/juce_ApplicationBase.cpp"; sourceTree = "SOURCE_ROOT"; };
+		EF96A2B04F061E2DDCDD45D7 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_linux_Clipboard.cpp"; path = "../../../juce/modules/juce_gui_basics/native/juce_linux_Clipboard.cpp"; sourceTree = "SOURCE_ROOT"; };
+		EFA7C36D71A52C06681FCDBA = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_DrawablePath.h"; path = "../../../juce/modules/juce_gui_basics/drawables/juce_DrawablePath.h"; sourceTree = "SOURCE_ROOT"; };
+		EFD6A6B89F68D70C31D5D167 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_ReadWriteLock.cpp"; path = "../../../juce/modules/juce_core/threads/juce_ReadWriteLock.cpp"; sourceTree = "SOURCE_ROOT"; };
+		F01030FB262F40E8E52922DE = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_URL.h"; path = "../../../juce/modules/juce_core/network/juce_URL.h"; sourceTree = "SOURCE_ROOT"; };
+		F03FCD9223E01482CF16FE9A = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_PropertySet.cpp"; path = "../../../juce/modules/juce_core/containers/juce_PropertySet.cpp"; sourceTree = "SOURCE_ROOT"; };
+		F076F25ECB0B7D2073F6478E = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_MarkerList.h"; path = "../../../juce/modules/juce_gui_basics/positioning/juce_MarkerList.h"; sourceTree = "SOURCE_ROOT"; };
+		F0B30E3238044CF573DFF44D = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = TouchkeyOnsetAngleMappingFactory.cpp; path = ../../Source/Mappings/OnsetAngle/TouchkeyOnsetAngleMappingFactory.cpp; sourceTree = "SOURCE_ROOT"; };
+		F0C1BF45B5321D9A128DC034 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_Logger.h"; path = "../../../juce/modules/juce_core/logging/juce_Logger.h"; sourceTree = "SOURCE_ROOT"; };
+		F0F03FAE0B2065470B1815B7 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_PerformanceCounter.cpp"; path = "../../../juce/modules/juce_core/time/juce_PerformanceCounter.cpp"; sourceTree = "SOURCE_ROOT"; };
+		F0F457D2A8E7EC3DE1CADC28 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = Osc.cpp; path = ../../Source/TouchKeys/Osc.cpp; sourceTree = "SOURCE_ROOT"; };
+		F17AD2FF61E5ECA564467092 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_win32_DirectWriteTypeface.cpp"; path = "../../../juce/modules/juce_graphics/native/juce_win32_DirectWriteTypeface.cpp"; sourceTree = "SOURCE_ROOT"; };
+		F1A1127D8FADC377892165D6 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_DirectoryContentsDisplayComponent.h"; path = "../../../juce/modules/juce_gui_basics/filebrowser/juce_DirectoryContentsDisplayComponent.h"; sourceTree = "SOURCE_ROOT"; };
+		F1DF08252E945645AE2B4E98 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_RelativeCoordinate.cpp"; path = "../../../juce/modules/juce_gui_basics/positioning/juce_RelativeCoordinate.cpp"; sourceTree = "SOURCE_ROOT"; };
+		F1F5AB75B6F813F0BAE8ACB6 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_CPlusPlusCodeTokeniser.cpp"; path = "../../../juce/modules/juce_gui_extra/code_editor/juce_CPlusPlusCodeTokeniser.cpp"; sourceTree = "SOURCE_ROOT"; };
+		F22AC4DF46EFEB83C6243426 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_ResizableBorderComponent.h"; path = "../../../juce/modules/juce_gui_basics/layout/juce_ResizableBorderComponent.h"; sourceTree = "SOURCE_ROOT"; };
+		F24B0FB4FEE7764F3EC91EE5 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_BubbleComponent.h"; path = "../../../juce/modules/juce_gui_basics/misc/juce_BubbleComponent.h"; sourceTree = "SOURCE_ROOT"; };
+		F2AFB80DCF056F88546FD65A = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_File.cpp"; path = "../../../juce/modules/juce_core/files/juce_File.cpp"; sourceTree = "SOURCE_ROOT"; };
+		F3125A1E89944D2040CA8DDE = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_PropertiesFile.h"; path = "../../../juce/modules/juce_data_structures/app_properties/juce_PropertiesFile.h"; sourceTree = "SOURCE_ROOT"; };
+		F3B5E38E7731B299DBDBE164 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = MappingEditorComponent.h; path = ../../Source/GUI/MappingEditorComponent.h; sourceTree = "SOURCE_ROOT"; };
+		F408D8EEB8B3503EC6442162 = { isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Info.plist; sourceTree = "SOURCE_ROOT"; };
+		F430F75983EA8B5A31B686E8 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_IIRFilter.h"; path = "../../../juce/modules/juce_audio_basics/effects/juce_IIRFilter.h"; sourceTree = "SOURCE_ROOT"; };
+		F47D3518C1FDA9C65BDFE088 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_DialogWindow.h"; path = "../../../juce/modules/juce_gui_basics/windows/juce_DialogWindow.h"; sourceTree = "SOURCE_ROOT"; };
+		F4A7F22576E33E2A5E841B7D = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_CharacterFunctions.cpp"; path = "../../../juce/modules/juce_core/text/juce_CharacterFunctions.cpp"; sourceTree = "SOURCE_ROOT"; };
+		F4DDE0DD51C64223CC8C9B62 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = MappingListComponent.h; path = ../../Source/GUI/MappingListComponent.h; sourceTree = "SOURCE_ROOT"; };
+		F55D37A9CFDBAA406A95E743 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = TouchkeyBaseMapping.cpp; path = ../../Source/Mappings/TouchkeyBaseMapping.cpp; sourceTree = "SOURCE_ROOT"; };
+		F55EF8461AE8552E4FD9A0BF = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_StretchableObjectResizer.h"; path = "../../../juce/modules/juce_gui_basics/layout/juce_StretchableObjectResizer.h"; sourceTree = "SOURCE_ROOT"; };
+		F63EB3D2F75EC36B2FBFCDBA = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = TouchkeyOnsetAngleMapping.cpp; path = ../../Source/Mappings/OnsetAngle/TouchkeyOnsetAngleMapping.cpp; sourceTree = "SOURCE_ROOT"; };
+		F66F5ACB0EF8ABFD47CD2630 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_Socket.cpp"; path = "../../../juce/modules/juce_core/network/juce_Socket.cpp"; sourceTree = "SOURCE_ROOT"; };
+		F70C14B43A5C65BB3E1C27A8 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = TouchkeyVibratoMappingFactory.h; path = ../../Source/Mappings/Vibrato/TouchkeyVibratoMappingFactory.h; sourceTree = "SOURCE_ROOT"; };
+		F7CFBA6425CEA669DAEF5F44 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_DrawablePath.cpp"; path = "../../../juce/modules/juce_gui_basics/drawables/juce_DrawablePath.cpp"; sourceTree = "SOURCE_ROOT"; };
+		F7F083D64EF53598854CF637 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_AudioFormatReader.h"; path = "../../../juce/modules/juce_audio_formats/format/juce_AudioFormatReader.h"; sourceTree = "SOURCE_ROOT"; };
+		F7F0A21852B09E8081C1DD93 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_RecentlyOpenedFilesList.h"; path = "../../../juce/modules/juce_gui_extra/misc/juce_RecentlyOpenedFilesList.h"; sourceTree = "SOURCE_ROOT"; };
+		F81F313202CDF4BE59B56B25 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_ModifierKeys.h"; path = "../../../juce/modules/juce_gui_basics/keyboard/juce_ModifierKeys.h"; sourceTree = "SOURCE_ROOT"; };
+		F85B20313230BD88D984FC3A = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_Singleton.h"; path = "../../../juce/modules/juce_core/memory/juce_Singleton.h"; sourceTree = "SOURCE_ROOT"; };
+		F89BC7CFD2C4BC07D31DFA04 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = TouchkeyPitchBendMapping.cpp; path = ../../Source/Mappings/PitchBend/TouchkeyPitchBendMapping.cpp; sourceTree = "SOURCE_ROOT"; };
+		F8F8849DF5261E350DF33690 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_ChoicePropertyComponent.cpp"; path = "../../../juce/modules/juce_gui_basics/properties/juce_ChoicePropertyComponent.cpp"; sourceTree = "SOURCE_ROOT"; };
+		F950FB55905754598EA9B3B5 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_ResizableEdgeComponent.cpp"; path = "../../../juce/modules/juce_gui_basics/layout/juce_ResizableEdgeComponent.cpp"; sourceTree = "SOURCE_ROOT"; };
+		F9CBC71C9E3A22EC667F0B93 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_FileSearchPath.cpp"; path = "../../../juce/modules/juce_core/files/juce_FileSearchPath.cpp"; sourceTree = "SOURCE_ROOT"; };
+		FA09A507AA15C65C617AEAE2 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_RelativeTime.cpp"; path = "../../../juce/modules/juce_core/time/juce_RelativeTime.cpp"; sourceTree = "SOURCE_ROOT"; };
+		FA23D0A59A1BE884D7308CC8 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_MidiOutput.cpp"; path = "../../../juce/modules/juce_audio_devices/midi_io/juce_MidiOutput.cpp"; sourceTree = "SOURCE_ROOT"; };
+		FA32A230622BE3CA42ED90FB = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_ApplicationCommandInfo.cpp"; path = "../../../juce/modules/juce_gui_basics/commands/juce_ApplicationCommandInfo.cpp"; sourceTree = "SOURCE_ROOT"; };
+		FA442E5370C59BC201129D2D = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_OpenGLExtensions.h"; path = "../../../juce/modules/juce_opengl/native/juce_OpenGLExtensions.h"; sourceTree = "SOURCE_ROOT"; };
+		FA7129964EDAC6BCFB6012BB = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_CriticalSection.h"; path = "../../../juce/modules/juce_core/threads/juce_CriticalSection.h"; sourceTree = "SOURCE_ROOT"; };
+		FB95EEFCC0A2A933309EAEAE = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_Javascript.h"; path = "../../../juce/modules/juce_core/javascript/juce_Javascript.h"; sourceTree = "SOURCE_ROOT"; };
+		FBA117D9E3AD465CFA9DDA51 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_ImageCache.cpp"; path = "../../../juce/modules/juce_graphics/images/juce_ImageCache.cpp"; sourceTree = "SOURCE_ROOT"; };
+		FBD0A4CCB4013E8B8AA653E2 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; name = "juce_audio_basics.mm"; path = "../../../juce/modules/juce_audio_basics/juce_audio_basics.mm"; sourceTree = "SOURCE_ROOT"; };
+		FC5EAA6165A109AD70EDDCA8 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_AudioIODevice.h"; path = "../../../juce/modules/juce_audio_devices/audio_io/juce_AudioIODevice.h"; sourceTree = "SOURCE_ROOT"; };
+		FC931CB64045178BDB71E95D = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_Colour.h"; path = "../../../juce/modules/juce_graphics/colour/juce_Colour.h"; sourceTree = "SOURCE_ROOT"; };
+		FCCF27C93502E80A0EBDDAC7 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_mac_CoreAudio.cpp"; path = "../../../juce/modules/juce_audio_devices/native/juce_mac_CoreAudio.cpp"; sourceTree = "SOURCE_ROOT"; };
+		FD093D026A05354DA8D1B111 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_StretchableLayoutResizerBar.cpp"; path = "../../../juce/modules/juce_gui_basics/layout/juce_StretchableLayoutResizerBar.cpp"; sourceTree = "SOURCE_ROOT"; };
+		FD7BF71F32457EFD941519E0 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = MidiOutputController.cpp; path = ../../Source/TouchKeys/MidiOutputController.cpp; sourceTree = "SOURCE_ROOT"; };
+		FDA89D8C60D78D85531C8BC0 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_FileChooser.h"; path = "../../../juce/modules/juce_gui_basics/filebrowser/juce_FileChooser.h"; sourceTree = "SOURCE_ROOT"; };
+		FDCAF8B0857EC94103B91E9E = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_OpenGLContext.h"; path = "../../../juce/modules/juce_opengl/opengl/juce_OpenGLContext.h"; sourceTree = "SOURCE_ROOT"; };
+		FE27CB5DB6ECB67339BFE39D = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_ToolbarButton.cpp"; path = "../../../juce/modules/juce_gui_basics/buttons/juce_ToolbarButton.cpp"; sourceTree = "SOURCE_ROOT"; };
+		FE59EEF1AB35791BB7600489 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_InterProcessLock.h"; path = "../../../juce/modules/juce_core/threads/juce_InterProcessLock.h"; sourceTree = "SOURCE_ROOT"; };
+		FE6FCFD3E4E2689302C29B50 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = PianoKeyboard.h; path = ../../Source/TouchKeys/PianoKeyboard.h; sourceTree = "SOURCE_ROOT"; };
+		FEB3A41EEF9A3958E3FD1BDB = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = TouchkeyControlMappingShortEditor.cpp; path = ../../Source/Mappings/Control/TouchkeyControlMappingShortEditor.cpp; sourceTree = "SOURCE_ROOT"; };
+		FECFF08C2CECDF120F9F1454 = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_ConcertinaPanel.cpp"; path = "../../../juce/modules/juce_gui_basics/layout/juce_ConcertinaPanel.cpp"; sourceTree = "SOURCE_ROOT"; };
+		FF28767B560C00EABF1D1C73 = { isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "juce_OpenGL_osx.h"; path = "../../../juce/modules/juce_opengl/native/juce_OpenGL_osx.h"; sourceTree = "SOURCE_ROOT"; };
+		FFB3A6545B8B46F2043EEE1C = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_FileFilter.cpp"; path = "../../../juce/modules/juce_gui_basics/filebrowser/juce_FileFilter.cpp"; sourceTree = "SOURCE_ROOT"; };
+		FFBCB888FCE740B6BFE2835B = { isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = "juce_OpenGLTexture.cpp"; path = "../../../juce/modules/juce_opengl/opengl/juce_OpenGLTexture.cpp"; sourceTree = "SOURCE_ROOT"; };
+		278F1D36FE45600F19FDFA91 = { isa = PBXGroup; children = (
+				7B9FCDB57204606F4A7FDAD4,
+				B6DCD7E8C9C5C9EA58C5EF9C,
+				0C9554C2C80A409B9486F101,
+				3FBF5C96BBAA8C88CB37943C,
+				E9E267650C0230141C461A4B,
+				BE91DB4CF2406F37AF7E7D67,
+				B24051ADEFBD03E1C7CB8DC3,
+				BA2C6578AE3F6F2B82656B4F,
+				F4DDE0DD51C64223CC8C9B62,
+				01A3C0D5D0FDDA38CE02C685,
+				0A486C6A586B60525C9BAB59,
+				F3B5E38E7731B299DBDBE164 ); name = GUI; sourceTree = "<group>"; };
+		9B41786D34A867FF8F1341EF = { isa = PBXGroup; children = (
+				119E49FA10A84C0AED360605,
+				692CFED8D5A615CCE73120FB,
+				DE6A8EF9DD39C0260ADD27B0,
+				4025F5D575B9A5A511EF8F30,
+				001418B9F0352A2205CDAA96,
+				F70C14B43A5C65BB3E1C27A8 ); name = Vibrato; sourceTree = "<group>"; };
+		72E20DA6718B4572688E63D7 = { isa = PBXGroup; children = (
+				5DE5675862714BFEFF231027,
+				EB977BE1676B103BE15C730C,
+				91AADDB641BD3F80E9011C65,
+				3BA1D64E8B635B5EFBD8C416 ); name = ReleaseAngle; sourceTree = "<group>"; };
+		AB366B7760234A8957CC46C3 = { isa = PBXGroup; children = (
+				25875D01B837C34F01EF8C2F,
+				1DCDF6FC01C7DBDDA7D7AFEA,
+				F89BC7CFD2C4BC07D31DFA04,
+				0B15F842C077C9765284446A,
+				77D10D2C57D62DF18D3CE862,
+				7B299BDFEBA4DABD680B7116 ); name = PitchBend; sourceTree = "<group>"; };
+		CE924A2E99C34B5C0C1749CA = { isa = PBXGroup; children = (
+				F63EB3D2F75EC36B2FBFCDBA,
+				BD8254A682953545FA36F552,
+				F0B30E3238044CF573DFF44D,
+				B0A4E57EEDD13E204E723A92 ); name = OnsetAngle; sourceTree = "<group>"; };
+		428A1A8AE78D576DDA139D6F = { isa = PBXGroup; children = (
+				0A767ED26F1C6854875AC0DB,
+				0A982DF57CE775D8F05AD78B,
+				879BA74FC54118852648722F,
+				07F3126C9F1842B069672882 ); name = MultiFingerTrigger; sourceTree = "<group>"; };
+		EC83555D1B3E0BF39064D703 = { isa = PBXGroup; children = (
+				77BA61409E24E072AF1E5493,
+				724ABFBF59674B3C9B5C2437,
+				56EF3900C63C00BED0E574D0,
+				8CF10E46C293D8E35324F60F ); name = KeyDivision; sourceTree = "<group>"; };
+		D390B7A00A5496FC36CA9328 = { isa = PBXGroup; children = (
+				FEB3A41EEF9A3958E3FD1BDB,
+				0A5EA54B877D84C939B6F6D7,
+				0D2D03F562749971564F7773,
+				293AA70C67056ED558221FCD,
+				8ADF834CD28E353B15D200C7,
+				BA5449FDC0526DDC94874CC7 ); name = Control; sourceTree = "<group>"; };
+		2F055548C29B16BA107E346B = { isa = PBXGroup; children = (
+				9B41786D34A867FF8F1341EF,
+				72E20DA6718B4572688E63D7,
+				AB366B7760234A8957CC46C3,
+				CE924A2E99C34B5C0C1749CA,
+				428A1A8AE78D576DDA139D6F,
+				EC83555D1B3E0BF39064D703,
+				D390B7A00A5496FC36CA9328,
+				AF8EFA9540E8757E68922E40,
+				606F2538F89C451EC8BB479D,
+				F55D37A9CFDBAA406A95E743,
+				2AB205503EDC14D07B0CDFA7,
+				5C085DEDBB1A8F755F800922,
+				A6B7D7B713F01F521411F2D3,
+				D9FD13866909F3134B4AB740,
+				9D633F4B9BBCF3F4D9331E00,
+				7A002562A60140BEE1434ECF,
+				BDB6848D33CE535EBCA3B9AD,
+				61A7ED9BEB8538FEE656E782,
+				7EDCB02E405529C4B4A60DA9,
+				C2762F0E696E257A507595A5,
+				BF01A2533D2222C856D3DB3A ); name = Mappings; sourceTree = "<group>"; };
+		D6E28C861AC5D4069E6655A7 = { isa = PBXGroup; children = (
+				C2D442FDEECC11C9BD433379,
+				3D220516F82AD82C888C6765,
+				3FDF3206CD46EA7C0063B295,
+				439AF7EB00EE96C9B8E9B480,
+				C266C85403BF3646B729144D,
+				42312AF39D46367A8CCAB92F,
+				849B3D266CC18D432434AA9B,
+				4AF53AC364CD4D92FA9C66B1 ); name = Display; sourceTree = "<group>"; };
+		15BBDF1D59F35F4EC6C82651 = { isa = PBXGroup; children = (
+				7512B86BFA154BD74FC8AC07,
+				8776329610ED3DF98A95ECD2,
+				AF7CC57D767DFC1F4B121270,
+				D3F247C3C568453665FD300D,
+				7103BB75D00938B3DEF6F943,
+				E8EDACD332FC3BDFC3F0E6E1,
+				DBDB2CBDB7DD3D7B9713D4C5,
+				4B5B59C3EB40E81B0EB3FF2A,
+				32302BE7297F75C489B19CED,
+				4AD0E0592C453AF63D48A792,
+				9D627B675B5F55FD5A203346,
+				1EDD89AA0A5497A9AC685E41,
+				4BFF669E2B855A7A576E8CE2,
+				0955F3A0CD0B38AA81A04149,
+				9F843264E37B0F16987CDA48 ); name = Utility; sourceTree = "<group>"; };
+		E7C04CC2AA6E16B51566C9B6 = { isa = PBXGroup; children = (
+				68A9E6D3AA5B1E80308A5400,
+				E7B6750E6B2616D84773B7D0,
+				05A3090F3BD2DAA8D6DF24B6,
+				4C27098692905309308ADA65,
+				B7497C02BCD8C01280DA32B2,
+				725AA29F72430257825A0E8B,
+				80BDEDE0C822A9AE504A2C38,
+				5A7A1022A19D6DD162029AA6,
+				6D7B40AF156B7244E6D37851,
+				E38A23A31D7FB8E86851666B,
+				DF2B5B4B8D7948C9EF4882A7,
+				FD7BF71F32457EFD941519E0,
+				42DE2C4C5791303F70375058,
+				F0F457D2A8E7EC3DE1CADC28,
+				9B9B99EFAE12DE02397A87B4,
+				178A386F6FE9CCEAD2ACEA3A,
+				DD18A8B0C1A5F8BDA30BAA50,
+				183D81D9E023859A3E499243,
+				5CD67ECB4B41375D889C1A2F,
+				9C0B692CB270EFA9DB7FA4D7,
+				FE6FCFD3E4E2689302C29B50,
+				83AE3F5E0B7AE5D956415BCC,
+				55E99E81FC362EA304FEFC7D,
+				67B1EA88315E60BDDEF78A9B,
+				EA47BDE992F39919F3EF53EC,
+				BFBC57AA124FBB8C1C51CB59,
+				76824FE677CBBE997E6B95AC,
+				0E25A6A37156A0D0F5063494 ); name = TouchKeys; sourceTree = "<group>"; };
+		1583CC1C5B912AD3F1695E12 = { isa = PBXGroup; children = (
+				278F1D36FE45600F19FDFA91,
+				2F055548C29B16BA107E346B,
+				D6E28C861AC5D4069E6655A7,
+				15BBDF1D59F35F4EC6C82651,
+				E7C04CC2AA6E16B51566C9B6,
+				26646EAE80CBDF47DADEE278,
+				091527B6BC312B2295FA565A,
+				638AC9A213FFAFC1647D3C78 ); name = Source; sourceTree = "<group>"; };
+		BB858467B2CAC483468A8AA8 = { isa = PBXGroup; children = (
+				1596880D3FEA7FDC94B7D71A,
+				4262B169F606570751DE5855,
+				884FF9165E861BE4F04646FC,
+				1583CC1C5B912AD3F1695E12 ); name = TouchKeys; sourceTree = "<group>"; };
+		402FB34DB3166C76A94D4F83 = { isa = PBXGroup; children = (
+				486B2D4FE311DCB82E37DF60,
+				7CF528A9CF4D2D0C77AECA41,
+				11E71A3ED179AF068E6D8A9D,
+				22C2B16BCB6F4CA405233C04,
+				2C8F38028F686154B4760DC4,
+				980B7401EF270C8C3BF5921D ); name = buffers; sourceTree = "<group>"; };
+		C7E3A3464460973F3CA31423 = { isa = PBXGroup; children = (
+				688D8F39D56A47170508B993,
+				C469456ED7E4039CF8BCB26F,
+				02AFF71371B8781BB9914E14,
+				4E7E0F0BE092AB6743B03D43,
+				CEE095838CD8EDB961A05ACA,
+				AE98A52AB3BC4D803B54380C,
+				E68F5154CAF362F281F55A1A,
+				70DF91DDFADBF8A69040904F,
+				29057C0D0A47537D39F20C9C,
+				764F6B4DF0E407661F9594C1 ); name = midi; sourceTree = "<group>"; };
+		1957CB7441950FD771657845 = { isa = PBXGroup; children = (
+				6395794A858E7036D062579C,
+				B76B16D0041E8AF65ECBA8D2,
+				F430F75983EA8B5A31B686E8,
+				8FCE773A5D594437EF46757A,
+				AC9C0675772B6211213D774F,
+				EA23F0162B24A0A0FD58B0B2 ); name = effects; sourceTree = "<group>"; };
+		3646B9929E133CA941A2E1EA = { isa = PBXGroup; children = (
+				0241560E0F43D8F64C26A397,
+				25E54DE680924E94F31F7727,
+				47C9F191ACA30DA8866F8162,
+				0436257DE1C3D66483C147A4,
+				CD1916C626761395E7729958,
+				1C69D05010E094E31BACC6AA,
+				2DB8DD2C8970470DD6C94355,
+				A9483F780B954919D59F1178,
+				A760FABA720E873DCAD04DAF,
+				A8BD97DF1D8C19D42CF0715C,
+				09DB2381F553CE3AE1190F3C,
+				8688C08B789657B322528750,
+				4BA89F5127922FC8A0D88602,
+				79EBCB683DF7BD707B29F835,
+				E8D2B3CBDF5DED9EF0E0C63B,
+				821F73F15E2DCF3438652D3B ); name = sources; sourceTree = "<group>"; };
+		FCAAB1680ABFC736AAC1AD98 = { isa = PBXGroup; children = (
+				B967DAA2B052D328B12C7DE8,
+				57E7CFA95F5E9A5C31E0706A ); name = synthesisers; sourceTree = "<group>"; };
+		D461C49014F6BD834CCB7E79 = { isa = PBXGroup; children = (
+				402FB34DB3166C76A94D4F83,
+				C7E3A3464460973F3CA31423,
+				1957CB7441950FD771657845,
+				3646B9929E133CA941A2E1EA,
+				FCAAB1680ABFC736AAC1AD98,
+				7A2A67299D56679990772E1A,
+				102054A65E6A2C9E170F6C29 ); name = "juce_audio_basics"; sourceTree = "<group>"; };
+		C4577B7B6BFE3EA6C9604677 = { isa = PBXGroup; children = (
+				6DF85AFF4681188E85089BF5,
+				6F9A5F79F66D8B56E1B62B9B,
+				27034337E8AAB0ED01CA45E9,
+				FC5EAA6165A109AD70EDDCA8,
+				369F9740183012CD9E400C49,
+				C31F902A006BD5C9BD9259B3,
+				206A731B98F5606ED6B6F951 ); name = "audio_io"; sourceTree = "<group>"; };
+		68E9A92265B816B5CF13942B = { isa = PBXGroup; children = (
+				99011DB2E425583086338097,
+				354D2DA37EA34EFC3121E523,
+				0FF0FEC5D686372FEF413FF4,
+				FA23D0A59A1BE884D7308CC8,
+				396B15B57E8411B6131561B1 ); name = "midi_io"; sourceTree = "<group>"; };
+		048CF2E63BDE15CE2F974A86 = { isa = PBXGroup; children = (
+				E4F3ACBA5181C0AF7021EE38,
+				8ED952C01B726C1DBFACE2BD,
+				7CB06A483D91B0345A110791,
+				A1C6CCE346DC8551574C4108 ); name = sources; sourceTree = "<group>"; };
+		D8C9E7CEB1FDBFA62BA03154 = { isa = PBXGroup; children = (
+				5B1EA934207F64017DAEB18F,
+				3DB49071D4E78F56A380391D,
+				B303EB3E070BBF9A30F07301 ); name = "audio_cd"; sourceTree = "<group>"; };
+		061D61C2D1E6F813599269CB = { isa = PBXGroup; children = (
+				70D03CB09823072EB0FECD50,
+				80474D6CC77F91AADA740AC3,
+				AAB51A5E4451980FD9D77FF7,
+				EE373F66DCFC442DBAADCF74,
+				5F9AD4578F56B8BEC2E311C3,
+				4218E86125D6E9C9ACB58741,
+				62A053A579D81CD2C6171B4E,
+				53C317A2BC085C625C1ABB49,
+				9A8C321C45F9E659F8676BA3,
+				6F7A2A1DF08F35D772F72DA9,
+				FCCF27C93502E80A0EBDDAC7,
+				64C0F5A52B61A2F5A8689840,
+				99AA5732A932BEF06D09AB22,
+				AF10BCD18ABC5EC330CE3B1B,
+				36F8B4676EACEAFCED73F5A0,
+				53297CAD72BBB338874C593F,
+				E91EEAE6F09A108DBF5E9BC5,
+				B2575F7C163D92DA04ABDC7B,
+				EB414D42BBBF2486ECE698D8 ); name = native; sourceTree = "<group>"; };
+		B0535668713BC274381E1916 = { isa = PBXGroup; children = (
+				C4577B7B6BFE3EA6C9604677,
+				68E9A92265B816B5CF13942B,
+				048CF2E63BDE15CE2F974A86,
+				D8C9E7CEB1FDBFA62BA03154,
+				061D61C2D1E6F813599269CB,
+				9D2CA1387AFF642B2E0C06A3,
+				CF1E1CAA10DC6E090133245E ); name = "juce_audio_devices"; sourceTree = "<group>"; };
+		69DAA09EE6CFFE4106A71F9C = { isa = PBXGroup; children = (
+				10674C5BE3CBDC1812E82AFF,
+				172BE377D36CE8F6915DECC0,
+				94652A427B7F10B3405366B5,
+				EB9EE29BC6E0F654EBDA140F,
+				09E18B170D35E65AABCC3C90,
+				F7F083D64EF53598854CF637,
+				70E7379DBBD8C42D6DF053AE,
+				88464C31A0A0F8F876BD5CA2,
+				176B62DE2BBD7453ADE87482,
+				D3D34D4A8674E01CCE92CE65,
+				992F1A00CD771D7AA8E1E15D,
+				9A4FBA2DD18C825B15EE6D19,
+				AC5E7E9B31701A5A7B4E3ABB,
+				6C1F7220FB3D51C3E401670D,
+				6BBD376C70ED0BF7DB547474 ); name = format; sourceTree = "<group>"; };
+		DDBEA4594FA71E3BFE54F19B = { isa = PBXGroup; children = (
+				13F3F140177E3986BCBC12EB,
+				1DCB093C0AA6B07F8846CE5F,
+				3206DFDE463E6BD093541953,
+				E263014FE404722FDDC437C8,
+				B747A4B93EAA22A264B3D24C,
+				68F90A1AC81BD36F4AFCA49D,
+				B25BC7CD4D7D11A764A0FF19,
+				80878F914A6A514CE5FEDD3C,
+				4EF55CBE453A2F06A5880E6C,
+				33486E83204368CF282A73F8,
+				B954FAFC45D231B388305CBB,
+				CED1159B6F39600DE0520C82,
+				B464F76711F6B9CBB66DA3EF,
+				65851006CAF520CAAC0F81B7,
+				88D0E8DA9A02D3ADB13FB753,
+				9E51136900384B3DBAF5D60E,
+				16BA68E8565D501ED6085968,
+				D7752214A0701D59E34DC038 ); name = codecs; sourceTree = "<group>"; };
+		B0652D3130CBE5A5C14F13B7 = { isa = PBXGroup; children = (
+				1B21A0523192F8E570190A8D,
+				99C03C1C226C3BC10D3422BB ); name = sampler; sourceTree = "<group>"; };
+		0DE4341F24550CC6009036DC = { isa = PBXGroup; children = (
+				69DAA09EE6CFFE4106A71F9C,
+				DDBEA4594FA71E3BFE54F19B,
+				B0652D3130CBE5A5C14F13B7,
+				673187B7CDEE9090F0503F45,
+				65D27B3F8F17B07DCC171F1C ); name = "juce_audio_formats"; sourceTree = "<group>"; };
+		23FE544BDF21655E69832411 = { isa = PBXGroup; children = (
+				F4A7F22576E33E2A5E841B7D,
+				37D9F61856238A58FBAF151C,
+				890B8EF2DC47885686DAAF94,
+				50FBD249C6C5C1B4DBA459E6,
+				2DF4F5F337BF6C45F00C2921,
+				9E1100E08D019514168470B6,
+				02F82B7A43C1B002A55C8645,
+				B9E1B02E2D61CD5F0C3BA92E,
+				022C3AEC95B6F5C85673E1B9,
+				79EC7F603DD271F787FE2EC9,
+				24BBD2EEE5685F7BF08524DC,
+				3721BA53A6BAB64AFCE77C35,
+				4693A834FF8117902FFF4D3F,
+				3882417E73E282C25A526368,
+				E93D98571147B0AD0B0D42C1,
+				D3C722814093C96312625553,
+				547C4147F20B76FD1596DF3B,
+				3F044B1232BF1AD76CF25C05,
+				C830A46F2626904CBD8E74CF,
+				4B9F0E9662F5974FEB6D3EA6,
+				35660D5C79544657DACE648E,
+				E56B94B61B89BBBD6510D713 ); name = text; sourceTree = "<group>"; };
+		351644949FDC57BA4E44D3D4 = { isa = PBXGroup; children = (
+				22A640D49DAB8EC000C48C37,
+				3E03094D5D1AC333A39F4B7A,
+				C804C201CC2FADE94FA73FAD,
+				BDBFE5792BFFFEC6DD9C51AD,
+				5A557C5800B7FD7879B89A9A,
+				AE954565B99BE415F7966F4A,
+				52503B83C3EB95429DBCF2B8,
+				5AA900E1C54219C4371B3907 ); name = maths; sourceTree = "<group>"; };
+		1744D2795680DE549C3DE675 = { isa = PBXGroup; children = (
+				435F555EAB5F88A7DE95C8FB,
+				19241B0EBAE0BB8B2B78C861,
+				E88D828B769B0DB0DC1D624B,
+				E62E694F0D19A252A6B9EAA1,
+				158BA823A17ACCB4E6826026,
+				AF87CD32C1189D997A5520B9,
+				84329C0EAC5C8BCD99E03174,
+				5BDFD343F8067BB49E2C2125,
+				12EF0EF35B5F1C68F6576FBF,
+				5CFC24DCE2459CF5A80FD766,
+				B9A42558DE8A295EAF05E9A4,
+				F85B20313230BD88D984FC3A,
+				63E32F29B1D3528BDDE0B72A ); name = memory; sourceTree = "<group>"; };
+		2F64BBE827F299C15CC19C87 = { isa = PBXGroup; children = (
+				E327C96B7AB06A125C9764FC,
+				1DD5318B2EC76DCE6B3F715C,
+				1C3B338D3F36E6ADB417204D,
+				9D0F29356400B8E025E5F524,
+				86A8F063C7880CC175FCB59A,
+				5E21284BD5A78CCF9533A7CB,
+				2ED84C912889F7D0DA71BC31,
+				608D7036802547CACA0C8EB9,
+				9A42DC8DA886A985FC5A0862,
+				026C6BCB121C0BB163DE9F06,
+				021AF0A3F7822EA031EE86A1,
+				68B7DB3155ECA7DF6A79D5EC,
+				F03FCD9223E01482CF16FE9A,
+				27829CDA49F7FB509A7B5A1C,
+				3738D2B9C5D2ACBA23938BD6,
+				9BE9BB5131F3D749A7717085,
+				4E88EE61D4328B092B1F0BB2,
+				B97766EAEBB55185F51ED7B4,
+				BC52700DC6BCBBA42635510D,
+				8BD94C1723C9174065B1CA57 ); name = containers; sourceTree = "<group>"; };
+		3AEC149409FA3F1F9A5D0605 = { isa = PBXGroup; children = (
+				4722FD5DDEA12074E23626F6,
+				AD95293E5C296F4C23A4F94B,
+				FA7129964EDAC6BCFB6012BB,
+				0738BD96A6B0D207B2656964,
+				CE4F2F551165086D73459E43,
+				E185507E8E49A513D5E6894A,
+				FE59EEF1AB35791BB7600489,
+				3D9C5122C7D61F188F36817E,
+				EFD6A6B89F68D70C31D5D167,
+				30787E67674DB9065149BCF9,
+				3F12742B404F1BCE9FD1182F,
+				9923EBA9D4A01541CAE90CF2,
+				C2673A6A3D247F3CDCF66FF0,
+				440B6F250CB865AA74386018,
+				D74EA156B75DFD28AB6BD8D0,
+				93186997B68111C578D28993,
+				5B851A07868E688F0C765CA1,
+				1B47C271A0750D61976A387B,
+				75CA737B306C29F574308F8E,
+				D2C3D422A9FFB418D71B1FCA,
+				106C5E90696E6F1CC04EF896,
+				BD20E908DEABE97BC3CC4F07 ); name = threads; sourceTree = "<group>"; };
+		3C05FC3B5BA734DF3EA8398F = { isa = PBXGroup; children = (
+				F0F03FAE0B2065470B1815B7,
+				2F5F168DE166CF81749EDE32,
+				FA09A507AA15C65C617AEAE2,
+				47AFF0730B6F48B44D9AF21F,
+				133CEEB518DC3E0387DDDAAA,
+				4F0FBB923DC40FD7859CA9B6 ); name = time; sourceTree = "<group>"; };
+		6E38183E98F91312140B2F58 = { isa = PBXGroup; children = (
+				B39D1F9C2A6556C7E73F7B0A,
+				A9F0CD5B5FBAE5D4589A8C10,
+				F2AFB80DCF056F88546FD65A,
+				765EE2C3B48F47F94E607FBF,
+				2C07FA9C85BA4477E7E732BA,
+				4BF12D55902ABB1FC8FC63AF,
+				17C5876FBE7CBA0393D71B51,
+				BFD6F76FF80DE8CB6D83D18D,
+				F9CBC71C9E3A22EC667F0B93,
+				2C65A282C235450315BFA7B7,
+				72D026BC20AD5DA4743D6A68,
+				BA13CD1B8B5B44B8E675F743,
+				48D74A7F2C32E5ED9838F5FE ); name = files; sourceTree = "<group>"; };
+		EB96B9A9B78F5E70729A5B25 = { isa = PBXGroup; children = (
+				9EFEE9C78BF40448C92E4EA3,
+				D3C368F0138FE8F3F0B7540F,
+				2710E4CE033A62C80F13B7F2,
+				2E8E49CCC7727B5DBE866711,
+				7D8C65927B21E027450B23F9,
+				A024211A62CC0D9A646A7676,
+				F66F5ACB0EF8ABFD47CD2630,
+				2EFF4ABBAACFB3611A738940,
+				AD32973FF9774A2CF194C3CB,
+				F01030FB262F40E8E52922DE ); name = network; sourceTree = "<group>"; };
+		85FCA1F68636EBDFD80CCD3B = { isa = PBXGroup; children = (
+				2A64318B395562B54E8B978D,
+				3B0FB247B6CA70098D56B522,
+				C8CEE210EF8AA5594AFB7C6F,
+				62A0804DA5C8D8F076538D7D,
+				0A9A3C2E3F533AB2FD618DD9,
+				808B32EDF79E1139064ABB52,
+				9A4DFD3457C3E8A95F71F6E7,
+				A7E3F42645F33A7460F4D770,
+				976AEB3279AAA64A0EF186FE,
+				11DF49143B2E40D5E8AB074D,
+				390F98D3E43D2BC741B08D5B,
+				070312CF9497593454CB10C1,
+				B3F47C8187CE05809A994637,
+				601CBE152587954A406ED25F,
+				1BAF3D8502C6D43B2CA5CF8A ); name = streams; sourceTree = "<group>"; };
+		2284CA6682785C1A82A11DC9 = { isa = PBXGroup; children = (
+				ED331E878A92AC3986F5885B,
+				6C119011A8A9B7A6F092F10D,
+				4EA0B74126E80DFAA6075D51,
+				F0C1BF45B5321D9A128DC034 ); name = logging; sourceTree = "<group>"; };
+		E709133C2C49CBD3A3F8295C = { isa = PBXGroup; children = (
+				32C7614FDFD477BFC45C7A05,
+				BF7B348D8CDF033289AA95AD,
+				B0EAE043C24041BFA266B6EE,
+				7DE576785D066AA5B72476F5,
+				CF273B4AE1819D5344866606 ); name = system; sourceTree = "<group>"; };
+		3A94655CA811747F076D5DE6 = { isa = PBXGroup; children = (
+				1F101BE64CA15979764736A3,
+				D867F0F9AE014AC4873F4093,
+				2B3A1D8D1A77B39FFBA39EC8,
+				B8F2014DEA90E3849192C12D ); name = xml; sourceTree = "<group>"; };
+		3BF800D92630CD6B7CEF6ADC = { isa = PBXGroup; children = (
+				6A59FD6927F7863EA627B22B,
+				FB95EEFCC0A2A933309EAEAE,
+				1D41477949C13C4A9C7F0536,
+				84EBC2A31E96330B004B8ACD ); name = javascript; sourceTree = "<group>"; };
+		625475C9A5BF37B3641D16E8 = { isa = PBXGroup; children = (
+				E583018026996A67748B9630,
+				684832C67BAC9A135AD0F420,
+				39A2849DABF6CBB1F554501D,
+				B5053EFFA070EFE91763D1CF,
+				564725F265FA251EA1E85EFE,
+				D85AA9748C07062C4898B172 ); name = zip; sourceTree = "<group>"; };
+		FB2DC1F86FD22B3FC6741FE2 = { isa = PBXGroup; children = (
+				A7CB749C845499A420598F30,
+				BA8405E8F85F9A3F9EB253FD ); name = "unit_tests"; sourceTree = "<group>"; };
+		D1B0B826075013DD1A0227EC = { isa = PBXGroup; children = (
+				A6D093637F71601C70B1DA16,
+				92DEFC9A2AE2181DB9A0A8C2,
+				0F78459420EA5BD1915A0A8A,
+				CE4BCD27BDF96500D2F89936,
+				90CFD9356A26573B6D0BBF3F ); name = misc; sourceTree = "<group>"; };
+		BB218DAC73F21FF21FAE6C63 = { isa = PBXGroup; children = (
+				35C231F40757E0C7B7BE74A4,
+				72972DBFFD3B26C1A933CD0F,
+				58E550A1B386C2A56A4E5205,
+				D42A52316423A9F531FFC765,
+				5092DF5BA82449512AAA7D7B,
+				1D633C390EEF3C24FE44FF27,
+				560BA4677563ADBFEAA4A9A8,
+				4881596CF3AA3B6809FD95FB,
+				035E2239C42EE699D96A082C,
+				BAC6DC0A9324D08F4675165D,
+				ACB27E148164D4F5D3C36FCB,
+				E7F83771B734D5CCAC7AD391,
+				DBD3326F4F7F5F41D3B7EA99,
+				586FCB50DD095947B527E462,
+				776A2D8E1CDE4CBEFFA042B0,
+				7640817A6F712E83E8127229,
+				C5032AA8AFA0CF6414D26DDE,
+				2DA07ABEBAE78CBA17AD496C,
+				06111D0738FEE481A9A0212B,
+				707B322BFF21635B171475AE,
+				77E88D86D01AF16261B9E3AC,
+				6604B7074C60D16C2958058B,
+				8FEBDE824FD6FDFC09417C88,
+				277CDB2F2319FD585A04F00D,
+				E6B9CFC25E8B762EB17568ED,
+				7B945396F869A8F9750F3F45 ); name = native; sourceTree = "<group>"; };
+		F20C13E9ABA55C434579CDEE = { isa = PBXGroup; children = (
+				23FE544BDF21655E69832411,
+				351644949FDC57BA4E44D3D4,
+				1744D2795680DE549C3DE675,
+				2F64BBE827F299C15CC19C87,
+				3AEC149409FA3F1F9A5D0605,
+				3C05FC3B5BA734DF3EA8398F,
+				6E38183E98F91312140B2F58,
+				EB96B9A9B78F5E70729A5B25,
+				85FCA1F68636EBDFD80CCD3B,
+				2284CA6682785C1A82A11DC9,
+				E709133C2C49CBD3A3F8295C,
+				3A94655CA811747F076D5DE6,
+				3BF800D92630CD6B7CEF6ADC,
+				625475C9A5BF37B3641D16E8,
+				FB2DC1F86FD22B3FC6741FE2,
+				D1B0B826075013DD1A0227EC,
+				BB218DAC73F21FF21FAE6C63,
+				E9E29024302023AEC889C4E1,
+				ADC1E27313862E1559D41C3A ); name = "juce_core"; sourceTree = "<group>"; };
+		AF50D24168F417177DB7370F = { isa = PBXGroup; children = (
+				AAF9D7469BCE537A1FC75ABC,
+				40B64B6C8BD06AD2930DA9F0,
+				63271E5A2866BBC8014C76F6,
+				987FA7FC98B320FAE747F2ED ); name = values; sourceTree = "<group>"; };
+		70490EADE714A7B309AC6274 = { isa = PBXGroup; children = (
+				A2B676B239E39A8977B5EB5E,
+				5B03F93B4E808E5B42C5F984,
+				30B72EA5CC760FDC1D68A1D5 ); name = undomanager; sourceTree = "<group>"; };
+		FA848916740767602A5B6385 = { isa = PBXGroup; children = (
+				CB26855B4D41689AC3C18F85,
+				86C14BDBEB5F070A2166E8E6,
+				226A0BD39983B2132DDBD732,
+				F3125A1E89944D2040CA8DDE ); name = "app_properties"; sourceTree = "<group>"; };
+		BECD79B159F94076CCF77238 = { isa = PBXGroup; children = (
+				AF50D24168F417177DB7370F,
+				70490EADE714A7B309AC6274,
+				FA848916740767602A5B6385,
+				42C6DE63A9107E8D5B543F73,
+				BC64F550F0D3B5C2DD546574 ); name = "juce_data_structures"; sourceTree = "<group>"; };
+		3AA44EDFDA4BC1C6DCE59475 = { isa = PBXGroup; children = (
+				EF8E10BB5A52F89097F763D9,
+				A50534690CFC8F765EEBCACC,
+				1F76B83EE0CFAA4761604B94,
+				610013306039A681BE00CDF4,
+				5A740A5459309CC1144016FD,
+				C4ADD63DE594AD3A82825DD4,
+				B676B0C4543E418E31B2607F,
+				74A7CB1DEB75A34CDD0CD4C4,
+				5D1750119D2E86324ED67585,
+				B6E00FB0259FB8560B8EB8B2,
+				0FD7CCA5B5517C3CF7C2CCA9,
+				EA983D5CAA5DB46A4EA83E19 ); name = messages; sourceTree = "<group>"; };
+		542941430579B611BD5E2588 = { isa = PBXGroup; children = (
+				CE5071065642A70419650EB3,
+				9AD89C058D91C6D880FF028E,
+				482B36C5234DB4CED73506B4,
+				E3DA2BE88F2738CCDEDDF3AD ); name = timers; sourceTree = "<group>"; };
+		59F782E0DB9A51FD365302D2 = { isa = PBXGroup; children = (
+				4953D82D7A84A83E3E7572A3,
+				2F46807285895EC6D5DDA8E3,
+				C72E037FB7147B081510B13E,
+				3F55433E0CFB772D4CEC69EA,
+				09995800CE4E0E3EF8C11EF8,
+				63FB3D8BC053ED771331E303,
+				451D1DA2175F20466C358449,
+				595D9603C966DD7A96601770,
+				79ED04A9195FA283CCBF6492 ); name = broadcasters; sourceTree = "<group>"; };
+		14EF5B2E7780CD35F595E7EF = { isa = PBXGroup; children = (
+				3C49A71B859640FC4A39B8F5,
+				5E4F05C1C0E47ABAD690E924,
+				9B9C26B87D15142FEBBDAD4C,
+				A84C4AE3D9E536D74C379F0F ); name = interprocess; sourceTree = "<group>"; };
+		B811157C7EAF4978CF8ED535 = { isa = PBXGroup; children = (
+				6A58296B30F95486340DFD85,
+				6EF7B06580A10399CCD7BD5F,
+				DE7280B3F30E33133756C534,
+				4D6A4F688249F5B876849207,
+				89130E3BE0652A8CAB11A2BE,
+				179FD2EA9B129C845CB4DCA7,
+				B1D86EC91F8BDE1D1BF29728,
+				0F654BA2689F1C8D04DE1864 ); name = native; sourceTree = "<group>"; };
+		1E588EC96A4D32BF0C19F169 = { isa = PBXGroup; children = (
+				3AA44EDFDA4BC1C6DCE59475,
+				542941430579B611BD5E2588,
+				59F782E0DB9A51FD365302D2,
+				14EF5B2E7780CD35F595E7EF,
+				B811157C7EAF4978CF8ED535,
+				7ED45DC66FA713C394B2EA1F,
+				C548DA4DD5135F3EDFF7589D ); name = "juce_events"; sourceTree = "<group>"; };
+		7154A3B59065B4F5BFBEAD90 = { isa = PBXGroup; children = (
+				93FFEE74D94C3D0D397CE80D,
+				FC931CB64045178BDB71E95D,
+				5AA0C6F1D0EEB64916D79A1F,
+				E73EB83A03FED6C1036C85A6,
+				74367CCDFA812324602A8232,
+				012940D53AFFB34E7515BA0D,
+				5BDDEB249D4D04F0DAAA97B6,
+				E32397CB64E42DA5119CC3E8,
+				641DCDEE4366C19C302BCDC7 ); name = colour; sourceTree = "<group>"; };
+		1142603A91CA5F35115D029F = { isa = PBXGroup; children = (
+				71D4D37B1DBDF821E57F68F3,
+				EC3966AA0A0C2A80EBB36B82,
+				92D1EE5E3C477F4341709537,
+				D3FE20DFB95B39D949C6B842,
+				8CBA2AD4897FAC4F6191FDB3,
+				6B53EFC11817616594E2D9BB,
+				638FEE22F7BC4DEE41443DC3 ); name = contexts; sourceTree = "<group>"; };
+		C03D946F0D17FDD49DB86985 = { isa = PBXGroup; children = (
+				57370DDA62616114166E89B2,
+				0A106C01DDEECF61E2D7F51E,
+				FBA117D9E3AD465CFA9DDA51,
+				8A02BB6161F2E74E22ABE635,
+				8BA8A815340C0C952C6DAA46,
+				C25315D4D50046938BDE18D8,
+				639B0783D7C0F63CACBC8616,
+				A58766D5C2AD6E9FC08EBD74 ); name = images; sourceTree = "<group>"; };
+		A1D76F0186CFBD4F87D59464 = { isa = PBXGroup; children = (
+				BBF99FBA82BF98E0CDA1F0C0,
+				CE6B0F39A803962ACD879D85,
+				ABF842ECF835C33322E2F21A ); name = "image_formats"; sourceTree = "<group>"; };
+		F3CD2A2CA1AB7FD343BD5E07 = { isa = PBXGroup; children = (
+				859ABCC7E6D33E942919BC4B,
+				D000F79ED732A055BC94D285,
+				99C3E2DED78CCBDBE436AE99,
+				D16ADF8AF26851DFFF4AE731,
+				1291BBAFB29453DAF847D561,
+				E389B103581AD847C754E978,
+				17FC0AB961883C8A5AB58D30,
+				9314190BB0D368F2A7C5E7C3,
+				978BBCD3C704C06D407A95C0,
+				3D8CB5CFD9B2031C3E016A0B,
+				9E815E59EEB3FBAED99918C2,
+				13C0DA6E480188AAA99E4283,
+				87083E81CEAC3B0F8BE7AAAD,
+				A1723FE218F435C322FB9AE0,
+				724CC1B7FF7ECEC8F25400AF ); name = geometry; sourceTree = "<group>"; };
+		DC6E3C5C4ACC4EDAC51A567E = { isa = PBXGroup; children = (
+				006F973FB5A7E243D7293AA6,
+				8BC0A410AADDAFD652051BDC,
+				84AB0ED9ED35EC083E56C73B ); name = placement; sourceTree = "<group>"; };
+		269C7F1C35A082EA46BBFC4C = { isa = PBXGroup; children = (
+				21761A1F87F6E65A6DDF9AF3,
+				84E1E20BD5B317A9C8AEB844,
+				0FF3EA4E9EBFF0FE96DF62E2,
+				AE32A61D504EC34FE186FDD1,
+				55614439CEA2AA4C3C83960C,
+				51824FD31518891951469CBA,
+				9A639B808D54F5251C701002,
+				962444B8A31102AAEA550124,
+				B2D53D597D6709806BB7239A,
+				558764F1D4A196E74A8B4D47,
+				984641E3DE573FF55DADB2AE,
+				70007C0A1E082ED1349FD91B ); name = fonts; sourceTree = "<group>"; };
+		F951AC35DF6A20E9D19E1097 = { isa = PBXGroup; children = (
+				2CEB8C23A0AE5989D02CA488,
+				9AB36BA5D331A32F6EBF2E4C,
+				1FA09EB888ECFBF0424174B2,
+				EF7D4F4824E5775A2D3801E5,
+				1CD21A58F0E15076E1027B2E ); name = effects; sourceTree = "<group>"; };
+		72CC318C6877D1D65674FE3B = { isa = PBXGroup; children = (
+				596161CC0A4A8DD05A368B32,
+				CCDD506916477B1ECDA100D1,
+				BE2ACC20FF51D7702D22D3B4,
+				588647ADF4EEADF6DD8C573F,
+				E2322B995208819DF175E332,
+				6CD813F180C163518C623E8C,
+				B3414ED8780ED544999D2B1C,
+				92328D97A09C3864407862BC,
+				54C9DB3C192D2E3C4BB796F6,
+				6A14D41C2C4B1022DFCFDEC9,
+				F17AD2FF61E5ECA564467092,
+				ACC97E565EDDD42DA4C1CB2E,
+				C18D5C01F2C345BC7775FABD ); name = native; sourceTree = "<group>"; };
+		50E4113B846C3F50715436BE = { isa = PBXGroup; children = (
+				7154A3B59065B4F5BFBEAD90,
+				1142603A91CA5F35115D029F,
+				C03D946F0D17FDD49DB86985,
+				A1D76F0186CFBD4F87D59464,
+				F3CD2A2CA1AB7FD343BD5E07,
+				DC6E3C5C4ACC4EDAC51A567E,
+				269C7F1C35A082EA46BBFC4C,
+				F951AC35DF6A20E9D19E1097,
+				72CC318C6877D1D65674FE3B,
+				D6D6D17FF1925E76CE1F24E4,
+				DA596B63735FF691A714D004 ); name = "juce_graphics"; sourceTree = "<group>"; };
+		1114A6CEDF1F72662105AFC7 = { isa = PBXGroup; children = (
+				D00A8C703C2C0055DD2B790C,
+				DB09B4EA350E52FB74BAAD9D,
+				196DBB540F88035040706D73,
+				189E1AEC6F95E5DF992C910D,
+				797AB794731D226BD260BA38,
+				1FADCBF1B7451DE704A9E5DE,
+				290AE3B1231EFE4B43011C38,
+				3CC9C766A7CA3465A75C1264,
+				19ECE65352DCC21F539ADFDE ); name = components; sourceTree = "<group>"; };
+		D23B6D10C1D76B5F36065122 = { isa = PBXGroup; children = (
+				A4317D5AEB8EDF9E2F0BEFF5,
+				EF092B37816526FF234B11F9,
+				2846850D6D318CBE5662505E,
+				8C9EEF956680F688D1C9E58B,
+				2B545CB34532967184BB979C,
+				7A05F75D7C08D0921F4DBA0E,
+				89EB02CBE5C658A77500E836,
+				601196F609721F1C314F9F50,
+				BDDEA8330D6309B2B97A55A6,
+				DF35E5C3BC9627D808F927E6,
+				B78F0674BC78DF9DAE4FEB7D,
+				A68FFCC7544F6D566C62402E,
+				A2120F8127E3DC96FD65D613,
+				C213BD9455B5E42E62AF260A,
+				7AD3466889EF224CEB709119,
+				143C31FDEB6ACD8A02F6F861,
+				62F2282824CD10B6F82A703F,
+				D3AB19332A0AE6C6A82612EF,
+				8990C39751CB1A719998A52D,
+				E46A2AA59E72BCA0D0891C27 ); name = mouse; sourceTree = "<group>"; };
+		63EF1E5C383925B43D88E623 = { isa = PBXGroup; children = (
+				97101F61193525E59EE16454,
+				9D044091881D52DC7354C266,
+				6CF3B1703B8D1E75091098B0,
+				D5B0943362643EC0EE867701,
+				929C9770C346C51B352937B8,
+				1256FA69F41BA1090B5C1A16,
+				9F64E66E6497FC3516119144,
+				D75E1147AF76C62DC23E7B18,
+				D2CE1FE912067925F5ACA619,
+				F81F313202CDF4BE59B56B25,
+				76ACD5EA5060FE1084BE7407,
+				9084178EF51E96AA8ACBB28D,
+				07D76602E68244DA48A496C3 ); name = keyboard; sourceTree = "<group>"; };
+		16BDAFE366F09A32579D877C = { isa = PBXGroup; children = (
+				7EB4672F9008FB273FB0E5A0,
+				6B502138661E9AB628F374F6,
+				778872C42C72FA6949A2536E,
+				B38844D2CDF1EC2E6110BB56,
+				3D2DF36E0CC698317233B864,
+				92C3C4A8F0677E70A2AAD5B9,
+				099F9D0711B4409A70F97168,
+				1F4C487096EFF581BE969D75,
+				4A75AF43CD5BD452E2B322A0,
+				42C95CE7D85568838409D2CF,
+				EAEB8DD999D027A9F9C02FC0,
+				EA085C2056A44FA5A7EE2AFE,
+				34F26056D1BF4026D3CA3DFB,
+				AC80872B6544527C71AB2B0B,
+				09AFFC227A610CE469F29590,
+				5D9DBA8EC90E8161918F12E0,
+				9D744C0830CCA407EB41368E,
+				A9832F86A2E7CA1675C64C47,
+				34F6C441AE3E69C11C988F58,
+				C75FB456CF13DFDEEF28C277,
+				542B60D386C30F602E8F37C7,
+				28A31552383A162DC607EAA0,
+				A84AFB90B3F99E4469CF5FDF,
+				50B10AEABDF8CFDB4C1F5198,
+				ACAD7720D728A3A8C9DB3949,
+				5F46502A5D1A44BD738467D4,
+				1C1E6DBAE366BBF6B93C6231 ); name = widgets; sourceTree = "<group>"; };
+		1BA54372572881E7B0D0C803 = { isa = PBXGroup; children = (
+				6A2C76C28ADEBBEB65637ED4,
+				6099FF0A5378073D75F68649,
+				857081343554E08270399A7A,
+				4A2C4C9905C7DBE7BD7A0A7D,
+				420CD52BA81E8FA04E73B0A1,
+				4E546FBB3A7626EFD573D41D,
+				2C144159C82D78DDD70C4105,
+				F47D3518C1FDA9C65BDFE088,
+				C8204B6A7B83338403077010,
+				6B6237A52DA8FB843E47CE0A,
+				B169A76946B512C0D34D1807,
+				6CA0CAA72433DDE5E6E2BA99,
+				3851FE5AD54A9688682B21E9,
+				CDCE769E18A546EC7FE65FE0,
+				94873408944DC8C21437551E,
+				2B4B4811F717681D3CEA3C06,
+				2F6C87AE0D32BF2600B951B2,
+				1A67F2C4654C988FD919A612,
+				265BDA429218A7AE3E5608CD ); name = windows; sourceTree = "<group>"; };
+		40E7FDCF5675A7C9DBE4A370 = { isa = PBXGroup; children = (
+				4B6DD49A71F451207F114891,
+				DF22553364728350DAFA6267,
+				2E2351F96F9FEDFB06036B45,
+				67FBBBE888B8FF071E9A6E9B,
+				04B1699E31A404BB6C7E4D93,
+				B928D73E2A712404DA6A9D39 ); name = menus; sourceTree = "<group>"; };
+		78E0AEA42C73EDD79029FAAD = { isa = PBXGroup; children = (
+				5D5300842AF71C49D1E5EDF5,
+				A237BDE5342960289E3E6302,
+				68EC729C279BCAD684AEBF9C,
+				9E80DB194A5D094EE64958AB,
+				0390578A9F961786A2E91266,
+				077F55BB50ECFDEA65F71DF6,
+				D2F76A9A564C9C39C9110C7E,
+				C26E6685AAEC528FC835CA5C,
+				286E94AA7893E151671EFACC,
+				D70B19E3DE0323FFA2EFCBBF,
+				FECFF08C2CECDF120F9F1454,
+				3D29826EB4A5B899C3624500,
+				5535E1D2AB0D896D5EA1528C,
+				399F9880BC2270EF7FD9BFA1,
+				7362738354A361AC4913C252,
+				7036D606B54801EF8B582B14,
+				E6908DE88A1A409B5E35CC37,
+				F22AC4DF46EFEB83C6243426,
+				03FD289F5F76E8E1644CE8AB,
+				6B4AAA40F2D016E163663316,
+				F950FB55905754598EA9B3B5,
+				121CF611E14695D8AC8D4AB3,
+				79FB5A147DA4950BDB10B97B,
+				A266DE5D4412DD7BA69F8CF8,
+				A01E2825335AD2156D93D462,
+				AFA3C397FE16ECC3DE0A5155,
+				FD093D026A05354DA8D1B111,
+				4F63CAE984068E8493AB1C59,
+				091FB91B90B6E8C215BB309B,
+				F55EF8461AE8552E4FD9A0BF,
+				EBA91A460FFAAFBC3EBEC36E,
+				801654FBD686CBFC671446FB,
+				9FA50DF5D707E8075BE42429,
+				63BF081465ACDD86B21207F5,
+				500B161A0A390BE6A7AC6E67,
+				A79D9F98051857EFFD8C2B88 ); name = layout; sourceTree = "<group>"; };
+		1064D4C7FF463C437CE721B1 = { isa = PBXGroup; children = (
+				980132D50C058EB7B1B625BA,
+				4D3486477C93D169F9841EBD,
+				4AA95F2942C46275D0CC1206,
+				4FAEFEF7315CD4697A3A4A5F,
+				C8288E63BC4A9120BE71066E,
+				B1E1FC7301A83D9FC8C320F9,
+				2C14D0A7927411AE5E37293F,
+				A041486CD4E6540A8D189C15,
+				350CAF816F5658A6F048333B,
+				7D4128713E8FB000F14BF646,
+				1167AC1646F727991030443D,
+				E0FB878B8DB0FE5B526BB950,
+				0D5355C05137A13E0F9C3577,
+				8720C0E6620ACF2E555C1CB5,
+				182845A3D1FBA30CEF9E0DA2,
+				DBC98946C2E19791899587BD,
+				FE27CB5DB6ECB67339BFE39D,
+				018F96DE10ED9ABDBBE82873 ); name = buttons; sourceTree = "<group>"; };
+		4166E01E2CADE6032D40685B = { isa = PBXGroup; children = (
+				8AB62337AC1BB45D8DD8B43D,
+				F076F25ECB0B7D2073F6478E,
+				F1DF08252E945645AE2B4E98,
+				B9683DFC5C73C8EA097C0C87,
+				CD5A70F1BEBD963AA994F53E,
+				B50647653535F8801D7FF83D,
+				5B83D18A4AF71418DA5F54F4,
+				4CAF6AD5590B3C8C9D2DCA10,
+				A08C368697313473C377CE60,
+				D999C1E99812B5D0C9F7519E,
+				D30002F0500DBE972234F381,
+				1FB429A8B8F85E403CA12650,
+				C9A5595CCCAF91E88CA4C4B4,
+				D850695B102730FCFF698E4D ); name = positioning; sourceTree = "<group>"; };
+		8E98E4F1FA2DB3BEB880470D = { isa = PBXGroup; children = (
+				1C2CDFD33D4679F91D0FBE57,
+				52FC4A84B75D864E89E3AB5B,
+				80EBFCB95D6DF2DA4B645F20,
+				CBB0FB63AD659D3F838318AB,
+				158AE747154A3811549CECBB,
+				4C8A9DE41142589CE8A66D22,
+				F7CFBA6425CEA669DAEF5F44,
+				EFA7C36D71A52C06681FCDBA,
+				5AAE89A72D91459EB9548A5D,
+				7756797D74D0D9D929AA9A80,
+				D6C4019A40A11C1B95FF07BB,
+				C89CDA180CC916AB5EEEE534,
+				DBE3D6F70DF4558463C24395,
+				DE68B78DB869AC78BCBD1214,
+				6EE034FF03FFA7308A5766AC ); name = drawables; sourceTree = "<group>"; };
+		A1E2976CE853750FF33BD8F2 = { isa = PBXGroup; children = (
+				CC0D3E1F3FAAF5B18C3FFDDF,
+				BA84C1198E0DEF50764C7D0F,
+				E2F713FF46DF610A87C64265,
+				84FFC974D174B7938505FCE2,
+				F8F8849DF5261E350DF33690,
+				96C1E28B6083DF9CA08B8DA5,
+				6B4D53A3F41AB62FA997F22E,
+				44820B2991A9F69F106501EB,
+				D967F911BFDE7E5B482F5A5F,
+				EECB8BFB50475C0564D37EAD,
+				E7043CF11AB43042A6725FDC,
+				6500D22CD08FD736625BA949,
+				726E5869DFBD1AE70FB39109,
+				2FEDA73E284BA8DD022BD7E0 ); name = properties; sourceTree = "<group>"; };
+		8A570D9D51DBC95AED850EE6 = { isa = PBXGroup; children = (
+				B8CB4838AFDE0B20C61A7248,
+				ACA460FC9207D68260CD11CA,
+				3014C4D84D91D187C834D4D9,
+				C4D3105688518F04E8D6D591,
+				DA76EEB0BD9183E2006CE9EB,
+				213F98F1913ED639AE895474,
+				972158C4F988264A6E5BC592,
+				41D8CD12402ACE94C06C12CF ); name = lookandfeel; sourceTree = "<group>"; };
+		17482FACCDAB45936C284B3B = { isa = PBXGroup; children = (
+				30BABDF73CAF7CC000817364,
+				F1A1127D8FADC377892165D6,
+				9F455251CF84921306543B93,
+				0E1393D568B97F822EC620E2,
+				83BB636195AA2F443ADEE961,
+				7828BC7ED00966FBBE2A90CB,
+				E9350A4235E42DBC9987725A,
+				5DFC9E79432F0E8B0245B05E,
+				FDA89D8C60D78D85531C8BC0,
+				AE876C6FF1860B4CC97EDF07,
+				A9184C4BCBA196795CCBCD98,
+				FFB3A6545B8B46F2043EEE1C,
+				7EE90399313A416997604AAE,
+				7688BA989669160877676209,
+				647A3C5CF4D3DE9C169861D1,
+				CA08ADDF1800B59374067EBE,
+				A29D19C2D6EEEE8A3C5D63B7,
+				0DD2016AB1A5661593E69C5A,
+				E393747AE555F946712380D5,
+				8166153E2B41CAFEF2012166,
+				AF1275CD971968AF3CB13BAD,
+				9D97B40DE97CFCF58CEA9DBD,
+				9210475F5E1B29B9C850ACDA,
+				5AC453F4B30AB8DA51B8B9B0,
+				BDD4F9A4B8D15A1515F87F53,
+				E1D304ED9044C0CE62C7B3AD ); name = filebrowser; sourceTree = "<group>"; };
+		CA85555DF7856DBC586ED01E = { isa = PBXGroup; children = (
+				1A4837FC0BE8B165FAFD999A,
+				FA32A230622BE3CA42ED90FB,
+				72DEAE1CFA0E4495953E162B,
+				6610029938CB08266FAD5120,
+				CE4297701F6471B19A8DB984,
+				10E1A1A46BECA9BD86A43829,
+				A01CEE27EC9F82523164E137,
+				1144B3B90A0E8142CCBC8097,
+				577688014EC968E5BB50E11B ); name = commands; sourceTree = "<group>"; };
+		223074080D125D810EB5A020 = { isa = PBXGroup; children = (
+				1A96C45EFEBAAD6DA351F6E7,
+				F24B0FB4FEE7764F3EC91EE5,
+				7172E38F5AA96EB0063A67AC,
+				585AE1E2582CD19A8F9FE2D2 ); name = misc; sourceTree = "<group>"; };
+		6E78DDAE9D7F453F48DBC404 = { isa = PBXGroup; children = (
+				0CCFFE7CFFC883C81D7D9441,
+				4A136908B5FF7712EBAD9981 ); name = application; sourceTree = "<group>"; };
+		5D2B9D64AC52724E6229C9DF = { isa = PBXGroup; children = (
+				350BF12B579608D2FBAD6AFC,
+				0900F84A5A94029BABD4F3D0,
+				8D3354A1E9C02DED513A4355,
+				DDF271285760C5642E3D3346,
+				EF96A2B04F061E2DDCDD45D7,
+				9EA1E13123758B126A76513B,
+				5E9303619B553ED5D8014075,
+				9B8C279F7E8FF6BCE7EFFF64,
+				D0868A6AA9193202A16F7B5D,
+				984AEFF53886155F33D2F336,
+				E1B8E23E7491C09D6708018C,
+				49745E79E63C5B9BC9A57AB1,
+				021EAA7FB2AF6D2F19996A6D,
+				C87D1CD195A69D0FC2BD3F33,
+				ECAB1F5B50EACFEBDFC11A40,
+				EBBA13C5C542DF0F8EFB826D ); name = native; sourceTree = "<group>"; };
+		FB598CA779C7221F5B504316 = { isa = PBXGroup; children = (
+				1114A6CEDF1F72662105AFC7,
+				D23B6D10C1D76B5F36065122,
+				63EF1E5C383925B43D88E623,
+				16BDAFE366F09A32579D877C,
+				1BA54372572881E7B0D0C803,
+				40E7FDCF5675A7C9DBE4A370,
+				78E0AEA42C73EDD79029FAAD,
+				1064D4C7FF463C437CE721B1,
+				4166E01E2CADE6032D40685B,
+				8E98E4F1FA2DB3BEB880470D,
+				A1E2976CE853750FF33BD8F2,
+				8A570D9D51DBC95AED850EE6,
+				17482FACCDAB45936C284B3B,
+				CA85555DF7856DBC586ED01E,
+				223074080D125D810EB5A020,
+				6E78DDAE9D7F453F48DBC404,
+				5D2B9D64AC52724E6229C9DF,
+				336105F72E57EF2412E2D362,
+				79892EC92519C81D7E02BDE1 ); name = "juce_gui_basics"; sourceTree = "<group>"; };
+		E2C64F09882744C8505B35EA = { isa = PBXGroup; children = (
+				C18FB4B3120557AA4EAFF405,
+				335BCF6E40FD20E31CFFCCE4,
+				6ABB704327024442CCD18ADD,
+				9CAC7B4DFD297F29680FE739,
+				2CFC336DBA57275297F8966D,
+				F1F5AB75B6F813F0BAE8ACB6,
+				4FF0EE53340B306B09230CAB,
+				80FAE594348676A04BACD99F ); name = "code_editor"; sourceTree = "<group>"; };
+		5DD3537008E371C199E90778 = { isa = PBXGroup; children = (
+				394A564D17F7FC26D45DD047,
+				8628FF7B8F05A904C8C59EC4 ); name = documents; sourceTree = "<group>"; };
+		86087062EF4A53F0DBC78299 = { isa = PBXGroup; children = (
+				BB8F5735542B54CA9DE6F353,
+				06462D0D2425BAF020472691,
+				EF60BF8888F0B0EA48E9FF09 ); name = embedding; sourceTree = "<group>"; };
+		16C95E981AC2D3FD64DE013E = { isa = PBXGroup; children = (
+				A8AF4B0BFECD9F39E1FE4E09,
+				8E169B97526DD9CCDE1BBA98,
+				C3AAD3FBAACDA8F0B7BB8FF9,
+				0838AE8A8B614755B5F31595,
+				DF0B3A4D5C217DA0283223BF,
+				BDA6E687818B0F2DC22337CB,
+				E7AF7E2ECFDAFF3CB186986D,
+				0C516DEBD52810652C1B4F87,
+				BB571C0DB322805E8CC5FAFC,
+				5E2C8035BF527680811D1CF5,
+				F7F0A21852B09E8081C1DD93,
+				76758FB3B866000523F0B144,
+				8ED90DCD63DA37E5D56402E3,
+				216C43FE64E807DFDDE137BB,
+				66F5603A6C984CEFD02530D3,
+				2D8CAD385E5572C8E790B98E ); name = misc; sourceTree = "<group>"; };
+		79B97A440791D2F786E3AA4D = { isa = PBXGroup; children = (
+				CC659F7C5C7BBB1483D22927,
+				7EE4DAABDC4CF9F13A3B5541,
+				31F03E9FBCC8C55504B03502,
+				D6272EF2E6816B659ABFBDA6,
+				D4D1E893C5144CF41D381E0E,
+				8517608903BDD585564A2BD9,
+				0754AE37A2269C502075E46B,
+				ACDF26585BEEE33D70E19A3C,
+				B8DF81CC9D4E0BE71C3AF558,
+				DC7333AE4FD5C16D3B49EE77,
+				D6A25A64B90CA2125D9D31B8,
+				3513EF2F4FDDA4BB173BA3D7 ); name = native; sourceTree = "<group>"; };
+		4ECC2091D369DED12DF6F35B = { isa = PBXGroup; children = (
+				E2C64F09882744C8505B35EA,
+				5DD3537008E371C199E90778,
+				86087062EF4A53F0DBC78299,
+				16C95E981AC2D3FD64DE013E,
+				79B97A440791D2F786E3AA4D,
+				A28DB7EF92412F8484B88A07,
+				2CBEEB3DDE7452AFAA496A53 ); name = "juce_gui_extra"; sourceTree = "<group>"; };
+		8ECD177FC31E505CF1234162 = { isa = PBXGroup; children = (
+				752E69ECB400EFBC66DB8081,
+				7D4A55601B7F8AB486B0AF21,
+				6CF6DF68577964FD68EFB132,
+				FDCAF8B0857EC94103B91E9E,
+				8C46F1B244F115E716B37147,
+				6348A0AF83CD2B08A0B0F867,
+				8E7AC08D09000F53F4C123CC,
+				ADCF2FE48BE727BF37B3C817,
+				4A0BF982E2E874D2FEC6073A,
+				3F8F763B8A07A2EE8D42C0C2,
+				EAC2B53916B041FEC59E050C,
+				B67D221133A97DC1C2C83C6F,
+				1F61F541D6F0256136E4F37B,
+				267AFF098D4880F1941B5201,
+				E830ACDAC41A38A04A16E2CD,
+				39A9EBDF9FF3E480CD2B43EB,
+				3FA6827CE8208A2676F7D89D,
+				FFBCB888FCE740B6BFE2835B,
+				11E656746B2425A8E99C2B1F,
+				E392DA2568D4143CE19A9A2F,
+				B785A355901939FBFC107581 ); name = opengl; sourceTree = "<group>"; };
+		0379222A65FDF46332F51183 = { isa = PBXGroup; children = (
+				DDC862F5672CFD8794EA65BC,
+				C8C2FA6F235AB6AA44027FAE,
+				03944A3451E95129394471C3,
+				6D365652129D06390251A09D,
+				FF28767B560C00EABF1D1C73,
+				E2EE80FAFA0DADF6D8AD8EA0,
+				FA442E5370C59BC201129D2D ); name = native; sourceTree = "<group>"; };
+		D5D115C9893CF02BF14D5AC0 = { isa = PBXGroup; children = (
+				8ECD177FC31E505CF1234162,
+				0379222A65FDF46332F51183,
+				B2C2E0F7737F05D71DC9CB1B,
+				7B799825EC1230F6618EC6FA ); name = "juce_opengl"; sourceTree = "<group>"; };
+		B6518018E1268AD0E306E052 = { isa = PBXGroup; children = (
+				D461C49014F6BD834CCB7E79,
+				B0535668713BC274381E1916,
+				0DE4341F24550CC6009036DC,
+				F20C13E9ABA55C434579CDEE,
+				BECD79B159F94076CCF77238,
+				1E588EC96A4D32BF0C19F169,
+				50E4113B846C3F50715436BE,
+				FB598CA779C7221F5B504316,
+				4ECC2091D369DED12DF6F35B,
+				D5D115C9893CF02BF14D5AC0 ); name = "Juce Modules"; sourceTree = "<group>"; };
+		C05820C87EF15292AA9DF609 = { isa = PBXGroup; children = (
+				BC0A05397FE514F1AE6B3436,
+				E5F79D13171E0F28729529DD,
+				8577418313DD8B1192FDD61D,
+				FBD0A4CCB4013E8B8AA653E2,
+				BA641413905F4396597FDF78,
+				23E81037EDC6EBA8A4145A66,
+				75A33066D1965DA1A60D41B0,
+				5D2E40E321F1A2340FB42A05,
+				5FBE2E2D25174AF84CAB065C,
+				91157413471588AA9BBA75D7,
+				007AEF97B8A9D76A0A2D83FC,
+				76566BC742E2FB1F6FDC34F5,
+				8CA21E32C250B212F23EA8AF,
+				743847CB7587F825B89E6FE8 ); name = "Juce Library Code"; sourceTree = "<group>"; };
+		1E79F78D63E48231E8EE6A98 = { isa = PBXGroup; children = (
+				F408D8EEB8B3503EC6442162,
+				283FF67DF916C041CE17E244,
+				55010ADB4310C9FF2CD11D52 ); name = Resources; sourceTree = "<group>"; };
+		13114A8A06EA75A50EE19833 = { isa = PBXGroup; children = (
+				1A9B74287470FD1944123E26,
+				7964EC4FD4E5860CF85469A6,
+				DF870F58DC21D8A032AE4D03,
+				5BC3C1BA534425DE943BCA22,
+				9DEC36437E061C38E07BAC75,
+				20BA5BC9BB93D1041D8F4C73,
+				3DFDF8135971D71B8889E84B,
+				6606A45FBF92643F83F78021,
+				7BBF33364D3B65730CEAD5F1,
+				F07FDD832AD269D84A40DAF1,
+				5EDDEE17AD20B0C75DF6DF12 ); name = Frameworks; sourceTree = "<group>"; };
+		6C350903DFBBB0A3EBFB29CC = { isa = PBXGroup; children = (
+				90E8A67FBC9B5B91FEB780F5 ); name = Products; sourceTree = "<group>"; };
+		A688FB8FEAB02AF9705DCC1C = { isa = PBXGroup; children = (
+				BB858467B2CAC483468A8AA8,
+				B6518018E1268AD0E306E052,
+				C05820C87EF15292AA9DF609,
+				1E79F78D63E48231E8EE6A98,
+				13114A8A06EA75A50EE19833,
+				6C350903DFBBB0A3EBFB29CC ); name = Source; sourceTree = "<group>"; };
+		D064BAE7941DCA67638B6779 = { isa = XCBuildConfiguration; buildSettings = {
+				ARCHS = "$(ARCHS_STANDARD_64_BIT)";
+				HEADER_SEARCH_PATHS = "../../JuceLibraryCode ../../../juce/modules $(inherited)";
+				GCC_OPTIMIZATION_LEVEL = 0;
+				INFOPLIST_FILE = Info.plist;
+				INSTALL_PATH = "$(HOME)/Applications";
+				CONFIGURATION_BUILD_DIR = "$(PROJECT_DIR)/build/$(CONFIGURATION)";
+				MACOSX_DEPLOYMENT_TARGET_ppc = 10.4;
+				SDKROOT_ppc = macosx10.5;
+				GCC_VERSION = com.apple.compilers.llvm.clang.1_0;
+				CLANG_CXX_LANGUAGE_STANDARD = "c++0x";
+				CLANG_LINK_OBJC_RUNTIME = NO;
+				COMBINE_HIDPI_IMAGES = YES;
+				OTHER_LDFLAGS = "/usr/local/lib/liblo.a";
+				COPY_PHASE_STRIP = NO;
+				GCC_DYNAMIC_NO_PIC = NO;
+				GCC_PREPROCESSOR_DEFINITIONS = (
+				"_DEBUG=1",
+				"DEBUG=1",
+				"JUCER_XCODE_MAC_F6D2F4CF=1"); }; name = Debug; };
+		33B8957D355F89111F5FF5AE = { isa = XCBuildConfiguration; buildSettings = {
+				ARCHS = "$(ARCHS_STANDARD_64_BIT)";
+				HEADER_SEARCH_PATHS = "../../JuceLibraryCode ../../../juce/modules $(inherited)";
+				GCC_OPTIMIZATION_LEVEL = s;
+				INFOPLIST_FILE = Info.plist;
+				INSTALL_PATH = "$(HOME)/Applications";
+				CONFIGURATION_BUILD_DIR = "$(PROJECT_DIR)/build/$(CONFIGURATION)";
+				MACOSX_DEPLOYMENT_TARGET_ppc = 10.4;
+				SDKROOT_ppc = macosx10.5;
+				GCC_VERSION = com.apple.compilers.llvm.clang.1_0;
+				CLANG_CXX_LANGUAGE_STANDARD = "c++0x";
+				CLANG_LINK_OBJC_RUNTIME = NO;
+				COMBINE_HIDPI_IMAGES = YES;
+				OTHER_LDFLAGS = "/usr/local/lib/liblo.a";
+				GCC_GENERATE_DEBUGGING_SYMBOLS = NO;
+				GCC_SYMBOLS_PRIVATE_EXTERN = YES;
+				DEAD_CODE_STRIPPING = YES;
+				GCC_PREPROCESSOR_DEFINITIONS = (
+				"_NDEBUG=1",
+				"NDEBUG=1",
+				"JUCER_XCODE_MAC_F6D2F4CF=1"); }; name = Release; };
+		208CA00FEE6920E1D9A18E62 = { isa = XCBuildConfiguration; buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				GCC_C_LANGUAGE_STANDARD = c99;
+				GCC_WARN_ABOUT_RETURN_TYPE = YES;
+				GCC_WARN_CHECK_SWITCH_STATEMENTS = YES;
+				GCC_WARN_UNUSED_VARIABLE = YES;
+				GCC_WARN_MISSING_PARENTHESES = YES;
+				GCC_WARN_NON_VIRTUAL_DESTRUCTOR = YES;
+				GCC_WARN_TYPECHECK_CALLS_TO_PRINTF = YES;
+				WARNING_CFLAGS = -Wreorder;
+				GCC_MODEL_TUNING = G5;
+				GCC_INLINES_ARE_PRIVATE_EXTERN = YES;
+				ZERO_LINK = NO;
+				DEBUG_INFORMATION_FORMAT = "dwarf";
+				PRODUCT_NAME = "TouchKeys"; }; name = Debug; };
+		581B2A3779A20392FD055C39 = { isa = XCBuildConfiguration; buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				GCC_C_LANGUAGE_STANDARD = c99;
+				GCC_WARN_ABOUT_RETURN_TYPE = YES;
+				GCC_WARN_CHECK_SWITCH_STATEMENTS = YES;
+				GCC_WARN_UNUSED_VARIABLE = YES;
+				GCC_WARN_MISSING_PARENTHESES = YES;
+				GCC_WARN_NON_VIRTUAL_DESTRUCTOR = YES;
+				GCC_WARN_TYPECHECK_CALLS_TO_PRINTF = YES;
+				WARNING_CFLAGS = -Wreorder;
+				GCC_MODEL_TUNING = G5;
+				GCC_INLINES_ARE_PRIVATE_EXTERN = YES;
+				ZERO_LINK = NO;
+				DEBUG_INFORMATION_FORMAT = "dwarf";
+				PRODUCT_NAME = "TouchKeys"; }; name = Release; };
+		5CD567AF6D8BF6EDE3603F6C = { isa = XCConfigurationList; buildConfigurations = (
+				208CA00FEE6920E1D9A18E62,
+				581B2A3779A20392FD055C39 ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Debug; };
+		2AA133E6B7020EDFA692D765 = { isa = XCConfigurationList; buildConfigurations = (
+				D064BAE7941DCA67638B6779,
+				33B8957D355F89111F5FF5AE ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Debug; };
+		2D66BEDF1389F7D6E2C1AB3A = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = (
+				C5AF559E110329B9AB35DFCB,
+				2B9C3DDF5283C5EFF1BC924A ); runOnlyForDeploymentPostprocessing = 0; };
+		A03E7D17BA28722252A8596E = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = (
+				B8EE628D560C9E474792128A,
+				1A85E080026A7A0DF0521F27,
+				CD4F6761CDABB836FBADCD80,
+				2378CAD03E1C80BF0AB4AAF5,
+				998FC7F9C3E8817D8C617391,
+				BA527B7D0D87CA5A51EF0D17,
+				B3470DC81675B3D490C27CFD,
+				5F6E53AD9FA709E7146A1CE6,
+				BE6B15C227B2372ACE438EC8,
+				13F86C7E26FBFDC47423240D,
+				0F57CE11332869C573488442,
+				0B15BCE918EEF2CCA0AEA482,
+				C07B7D53DED5507FB71A2686,
+				310713516FB26036F129F9B4,
+				8FBDE042A025832E21E6F8F4,
+				BB430A3EF5BE9214B1032DDD,
+				3BF49ACD7F6CF44414FB5922,
+				222381A07D9F4638272B89EF,
+				12510AB6876158F044CAAD7E,
+				82DFEC1C0E1F1717ECFF46EF,
+				2C4FDCA1E6A76506B05D37DD,
+				6EA3790C69AED29FC35AED08,
+				3EDCB8DC01EBCD339722C0B2,
+				562759D28DA0813B92BD7FEA,
+				3BCBF5B38F6056D9FE683B0E,
+				0178872E496D3D0EAC27AE7E,
+				9A83928E33C71ADCAE448871,
+				8B6C8D046A64E8F9A7A8A8A1,
+				04F763266CBDE1710ECDFBD1,
+				CAC2A323D111092F0ACA800D,
+				0719C742CAB513BC7D9133DE,
+				0A1BCC62A1753D24D01A3DAD,
+				4CF799D17D4295DD3F50720D,
+				0209777D29C1CF3F89FBF254,
+				FF44DE892280A28F67C7BFDD,
+				274E9F984B46BBDBF714003B,
+				BA8E15B815FCC3C7830C972E,
+				1AEFD23290811D94FB840E74,
+				5E0BF5CD6F5667E5A0C37651,
+				64F134ABEC30EA26D5473D34,
+				53E5A8D73743B36758DA4DA4,
+				FF8B3EA68E9255C839ABA3F8,
+				384EC6340C198680B166A0A6,
+				BA1577A5D5903CDC1BCFAB06,
+				B98E92A0AAE513E18217810A,
+				A526339B267B82C838D28D09,
+				5A0669E021464C9CF05C2B95,
+				1FDA7AEB064670D6A6B1E5A9,
+				E1B3A08EBE789ABADC0C025E,
+				9261B00A2C5E7187B5C04319,
+				2FB5C71C709DD3E25C6CC541,
+				B74FCF91BC16623F02F69D86,
+				8394B72883ACCC5CB2A13768,
+				5492D1307B92C3F63B9EDF09,
+				E810879EB266795004A4C22F,
+				F68DD438981A3BA730A626CB,
+				C4A41EFBBFFC08EC2CC75C92,
+				C7C650DB080B12CD2552EC3B,
+				F44235BA2064C306B810CE62,
+				80917C6EBEAD3F8DF0C99FD3,
+				52944F2FAC4CEBFB17FB29E4,
+				D88B64FB4066D913682D5BA3 ); runOnlyForDeploymentPostprocessing = 0; };
+		F7397085463A6685B1FBF65E = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = (
+				4078BC0343B4DFB6F9816BD5,
+				40646EF394D495C462E347F1,
+				63154C3F49640855CA89CB9B,
+				2985C4121F644A826FC287AE,
+				1A30D1B8526CD3E1AB79110C,
+				30C3A2842A3EA665887A7CC8,
+				FA58715E881C0982BF8E2FCD,
+				853FB568630B7E920E30E0EE,
+				C3F1C6DDE2A344D657712C75,
+				BADB457195977038A2BF09A4,
+				D05E8CCFCD6C2065EDE16DD6 ); runOnlyForDeploymentPostprocessing = 0; };
+		B9F5F2231C12730B54095416 = { isa = PBXNativeTarget; buildConfigurationList = 2AA133E6B7020EDFA692D765; buildPhases = (
+				2D66BEDF1389F7D6E2C1AB3A,
+				A03E7D17BA28722252A8596E,
+				F7397085463A6685B1FBF65E ); buildRules = ( ); dependencies = ( ); name = TouchKeys; productName = TouchKeys; productReference = 90E8A67FBC9B5B91FEB780F5; productInstallPath = "$(HOME)/Applications"; productType = "com.apple.product-type.application"; };
+		787330A1C9FABF4487F9ECA8 = { isa = PBXProject; buildConfigurationList = 5CD567AF6D8BF6EDE3603F6C; attributes = { LastUpgradeCheck = 0440; }; compatibilityVersion = "Xcode 3.2"; hasScannedForEncodings = 0; mainGroup = A688FB8FEAB02AF9705DCC1C; projectDirPath = ""; projectRoot = ""; targets = ( B9F5F2231C12730B54095416 ); };
+	};
+	rootObject = 787330A1C9FABF4487F9ECA8;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Builds/MacOSX/TouchKeys.xcodeproj/project.xcworkspace/contents.xcworkspacedata	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Workspace
+   version = "1.0">
+   <FileRef
+      location = "self:TouchKeys.xcodeproj">
+   </FileRef>
+</Workspace>
Binary file Builds/MacOSX/TouchKeys.xcodeproj/project.xcworkspace/xcuserdata/apm.xcuserdatad/UserInterfaceState.xcuserstate has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Builds/MacOSX/TouchKeys.xcodeproj/project.xcworkspace/xcuserdata/apm.xcuserdatad/WorkspaceSettings.xcsettings	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>HasAskedToTakeAutomaticSnapshotBeforeSignificantChanges</key>
+	<true/>
+	<key>SnapshotAutomaticallyBeforeSignificantChanges</key>
+	<false/>
+</dict>
+</plist>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Builds/MacOSX/TouchKeys.xcodeproj/xcuserdata/apm.xcuserdatad/xcdebugger/Breakpoints.xcbkptlist	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Bucket
+   type = "1"
+   version = "1.0">
+   <FileBreakpoints>
+      <FileBreakpoint
+         shouldBeEnabled = "No"
+         ignoreCount = "0"
+         continueAfterRunningActions = "No"
+         filePath = "../../Source/Mappings/TouchkeyKeyDivisionMapping.cpp"
+         timestampString = "403549105.00036"
+         startingColumnNumber = "9223372036854775807"
+         endingColumnNumber = "9223372036854775807"
+         startingLineNumber = "208"
+         endingLineNumber = "208"
+         landmarkName = "TouchkeyKeyDivisionMapping::sendPitchBendMessage(float pitchBendSemitones, bool force)"
+         landmarkType = "5">
+      </FileBreakpoint>
+   </FileBreakpoints>
+</Bucket>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Builds/MacOSX/TouchKeys.xcodeproj/xcuserdata/apm.xcuserdatad/xcschemes/TouchKeys.xcscheme	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,86 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+   LastUpgradeVersion = "0460"
+   version = "1.3">
+   <BuildAction
+      parallelizeBuildables = "YES"
+      buildImplicitDependencies = "YES">
+      <BuildActionEntries>
+         <BuildActionEntry
+            buildForTesting = "YES"
+            buildForRunning = "YES"
+            buildForProfiling = "YES"
+            buildForArchiving = "YES"
+            buildForAnalyzing = "YES">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "B9F5F2231C12730B54095416"
+               BuildableName = "TouchKeys.app"
+               BlueprintName = "TouchKeys"
+               ReferencedContainer = "container:TouchKeys.xcodeproj">
+            </BuildableReference>
+         </BuildActionEntry>
+      </BuildActionEntries>
+   </BuildAction>
+   <TestAction
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      shouldUseLaunchSchemeArgsEnv = "YES"
+      buildConfiguration = "Debug">
+      <Testables>
+      </Testables>
+      <MacroExpansion>
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "B9F5F2231C12730B54095416"
+            BuildableName = "TouchKeys.app"
+            BlueprintName = "TouchKeys"
+            ReferencedContainer = "container:TouchKeys.xcodeproj">
+         </BuildableReference>
+      </MacroExpansion>
+   </TestAction>
+   <LaunchAction
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      launchStyle = "0"
+      useCustomWorkingDirectory = "NO"
+      buildConfiguration = "Debug"
+      ignoresPersistentStateOnLaunch = "NO"
+      debugDocumentVersioning = "NO"
+      allowLocationSimulation = "YES">
+      <BuildableProductRunnable>
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "B9F5F2231C12730B54095416"
+            BuildableName = "TouchKeys.app"
+            BlueprintName = "TouchKeys"
+            ReferencedContainer = "container:TouchKeys.xcodeproj">
+         </BuildableReference>
+      </BuildableProductRunnable>
+      <AdditionalOptions>
+      </AdditionalOptions>
+   </LaunchAction>
+   <ProfileAction
+      shouldUseLaunchSchemeArgsEnv = "YES"
+      savedToolIdentifier = ""
+      useCustomWorkingDirectory = "NO"
+      buildConfiguration = "Release"
+      debugDocumentVersioning = "YES">
+      <BuildableProductRunnable>
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "B9F5F2231C12730B54095416"
+            BuildableName = "TouchKeys.app"
+            BlueprintName = "TouchKeys"
+            ReferencedContainer = "container:TouchKeys.xcodeproj">
+         </BuildableReference>
+      </BuildableProductRunnable>
+   </ProfileAction>
+   <AnalyzeAction
+      buildConfiguration = "Debug">
+   </AnalyzeAction>
+   <ArchiveAction
+      buildConfiguration = "Release"
+      revealArchiveInOrganizer = "YES">
+   </ArchiveAction>
+</Scheme>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Builds/MacOSX/TouchKeys.xcodeproj/xcuserdata/apm.xcuserdatad/xcschemes/xcschememanagement.plist	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>SchemeUserState</key>
+	<dict>
+		<key>TouchKeys.xcscheme</key>
+		<dict>
+			<key>orderHint</key>
+			<integer>0</integer>
+		</dict>
+	</dict>
+	<key>SuppressBuildableAutocreation</key>
+	<dict>
+		<key>B9F5F2231C12730B54095416</key>
+		<dict>
+			<key>primary</key>
+			<true/>
+		</dict>
+	</dict>
+</dict>
+</plist>
Binary file Resources/tk-icon-128.png has changed
Binary file Resources/tk-icon-256.png has changed
Binary file Resources/tk-icon-512.png has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/Display/KeyPositionGraphDisplay.cpp	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,262 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+
+  KeyPositionGraphDisplay.cpp: implements a graph of key position over time
+  for a specific key press, which is useful for analysis of continuous key
+  position.
+*/
+
+#include "KeyPositionGraphDisplay.h"
+#include <iostream>
+#include <cmath>
+
+// Class constants
+// Display margins
+const float KeyPositionGraphDisplay::kDisplaySideMargin = 0.5;
+const float KeyPositionGraphDisplay::kDisplayBottomMargin = 0.5;
+const float KeyPositionGraphDisplay::kDisplayTopMargin = 0.5;
+
+// Size of the graph area
+const float KeyPositionGraphDisplay::kDisplayGraphWidth = 20.0;
+const float KeyPositionGraphDisplay::kDisplayGraphHeight = 10.0;
+
+KeyPositionGraphDisplay::KeyPositionGraphDisplay() :
+displayPixelWidth_(1.0), displayPixelHeight_(1.0), totalDisplayWidth_(1.0), totalDisplayHeight_(1.0), 
+xMin_(0.0), xMax_(1.0), yMin_(-0.2), yMax_(1.2), needsUpdate_(true)
+{
+	// Initialize OpenGL settings: 2D only
+    
+	//glMatrixMode(GL_PROJECTION);
+	//glDisable(GL_DEPTH_TEST);
+    
+    pressStartTimestamp_ = pressFinishTimestamp_ = releaseStartTimestamp_ = releaseFinishTimestamp_ = missing_value<timestamp_type>::missing();
+    pressStartPosition_ = pressFinishPosition_ = releaseStartPosition_ = releaseFinishPosition_ = missing_value<key_position>::missing();
+    
+    totalDisplayWidth_ = kDisplaySideMargin*2 + kDisplayGraphWidth;
+    totalDisplayHeight_ = kDisplayTopMargin + kDisplayBottomMargin + kDisplayGraphHeight;
+}
+
+void KeyPositionGraphDisplay::setDisplaySize(float width, float height) {
+	ScopedLock sl(displayMutex_);
+	displayPixelWidth_ = width;
+	displayPixelHeight_ = height;
+	refreshViewport();
+}
+
+
+// Render the keyboard display
+
+void KeyPositionGraphDisplay::render() {
+	// Start with a light gray background
+	glClearColor(0.8, 0.8, 0.8, 1.0);
+	glClear(GL_COLOR_BUFFER_BIT);
+	glLoadIdentity();
+    
+    float invAspectRatio = totalDisplayWidth_ / totalDisplayHeight_; 
+	float scaleValue = 2.0 / totalDisplayWidth_;
+	
+	glScalef(scaleValue, scaleValue * invAspectRatio, scaleValue);
+	glTranslatef(-1.0 / scaleValue, -totalDisplayHeight_ / 2.0, 0);
+	glTranslatef(kDisplaySideMargin, kDisplayBottomMargin, 0.0);
+	
+	ScopedLock sl(displayMutex_);
+    
+    // Draw the region where the graph will be plotted (white with black outline)
+    glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
+    glColor3f(1.0, 1.0, 1.0);
+    
+    glBegin(GL_POLYGON);
+	glVertex2f(0, 0);
+    glVertex2f(0, kDisplayGraphHeight);
+	glVertex2f(kDisplayGraphWidth, kDisplayGraphHeight);
+	glVertex2f(kDisplayGraphWidth, 0);
+	glEnd();
+    
+    glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
+    glColor3f(0.0, 0.0, 0.0);
+    glLineWidth(1.0);
+    
+    glBegin(GL_POLYGON);
+	glVertex2f(0, 0);
+    glVertex2f(0, kDisplayGraphHeight);
+	glVertex2f(kDisplayGraphWidth, kDisplayGraphHeight);
+	glVertex2f(kDisplayGraphWidth, 0);
+	glEnd();
+    
+    // Draw gray lines for the 0.0 and 1.0 key positions
+    glColor3f(0.5, 0.5, 0.5);
+    glBegin(GL_LINES);
+    glVertex2f(0, graphToDisplayY(0.0));
+    glVertex2f(kDisplayGraphWidth, graphToDisplayY(0.0));
+    glEnd();
+    glBegin(GL_LINES);
+    glVertex2f(0, graphToDisplayY(1.0));
+    glVertex2f(kDisplayGraphWidth, graphToDisplayY(1.0));
+    glEnd();
+
+    // Draw the graph of position over time (if data exists)
+    if(!keyPositions_.empty() && !keyTimestamps_.empty()) {
+        glColor3f(0.0, 0.0, 0.0);
+        glBegin(GL_LINE_STRIP);
+        for(int index = 0; index < keyPositions_.size() && index < keyTimestamps_.size(); index++) {
+            glVertex2f(graphToDisplayX(keyTimestamps_[index]), graphToDisplayY(keyPositions_[index]));
+        }
+        glEnd();
+    }
+    
+    // Draw the important features, if they exist
+    if(!missing_value<timestamp_type>::isMissing(pressStartTimestamp_)) {
+        glColor3f(0.0, 0.0, 1.0);
+        glBegin(GL_LINES);
+        glVertex2f(graphToDisplayX(pressStartTimestamp_), 0);
+        glVertex2f(graphToDisplayX(pressStartTimestamp_), kDisplayGraphHeight);
+        glEnd();
+    }
+    if(!missing_value<timestamp_type>::isMissing(pressFinishTimestamp_)) {
+        glColor3f(1.0, 0.0, 0.0);
+        glBegin(GL_LINES);
+        glVertex2f(graphToDisplayX(pressFinishTimestamp_), 0);
+        glVertex2f(graphToDisplayX(pressFinishTimestamp_), kDisplayGraphHeight);
+        glEnd();
+    }
+    if(!missing_value<timestamp_type>::isMissing(releaseStartTimestamp_)) {
+        glColor3f(0.0, 0.0, 1.0);
+        glBegin(GL_LINES);
+        glVertex2f(graphToDisplayX(releaseStartTimestamp_), 0);
+        glVertex2f(graphToDisplayX(releaseStartTimestamp_), kDisplayGraphHeight);
+        glEnd();
+    }
+    if(!missing_value<timestamp_type>::isMissing(releaseFinishTimestamp_)) {
+        glColor3f(1.0, 0.0, 0.0);
+        glBegin(GL_LINES);
+        glVertex2f(graphToDisplayX(releaseFinishTimestamp_), 0);
+        glVertex2f(graphToDisplayX(releaseFinishTimestamp_), kDisplayGraphHeight);
+        glEnd();
+    }
+    
+    needsUpdate_ = false;
+	
+	glFlush();
+}
+
+// Mouse interaction methods
+
+void KeyPositionGraphDisplay::mouseDown(float x, float y) {
+	//Point mousePoint = {x, y};
+	//Point scaledPoint = screenToInternal(mousePoint);
+	
+	//needsUpdate_ = true;
+}
+
+void KeyPositionGraphDisplay::mouseDragged(float x, float y) {
+	//Point mousePoint = {x, y};
+	//Point scaledPoint = screenToInternal(mousePoint);
+
+    //needsUpdate_ = true;
+}
+
+void KeyPositionGraphDisplay::mouseUp(float x, float y) {
+	//Point mousePoint = {x, y};
+	//Point scaledPoint = screenToInternal(mousePoint);
+	
+	//needsUpdate_ = true;
+}
+
+void KeyPositionGraphDisplay::rightMouseDown(float x, float y) {
+	//Point mousePoint = {x, y};
+	//Point scaledPoint = screenToInternal(mousePoint);
+	
+	//needsUpdate_ = true;
+}
+
+void KeyPositionGraphDisplay::rightMouseDragged(float x, float y) {
+	//Point mousePoint = {x, y};
+	//Point scaledPoint = screenToInternal(mousePoint);
+}
+
+void KeyPositionGraphDisplay::rightMouseUp(float x, float y) {
+	//Point mousePoint = {x, y};
+	//Point scaledPoint = screenToInternal(mousePoint);
+}
+
+float KeyPositionGraphDisplay::graphToDisplayX(float x) {
+    return kDisplayGraphWidth*(x - xMin_)/(xMax_ - xMin_);
+}
+
+float KeyPositionGraphDisplay::graphToDisplayY(float y) {
+    return kDisplayGraphHeight*(y - yMin_)/(yMax_ - yMin_);
+}
+
+void KeyPositionGraphDisplay::refreshViewport() {
+	glViewport(0, 0, displayPixelWidth_, displayPixelHeight_);
+}
+
+// Copy data from the circular buffer to our internal buffer for 
+void KeyPositionGraphDisplay::copyKeyDataFromBuffer(Node<key_position>& keyBuffer,
+                                                    const Node<key_position>::size_type startIndex,
+                                                    const Node<key_position>::size_type endIndex) {
+    // Clear existing information
+    keyPositions_.clear();
+    keyTimestamps_.clear();
+    
+    Node<key_position>::size_type index = startIndex;
+    if(index < keyBuffer.beginIndex())
+        index = keyBuffer.beginIndex();
+    
+    // Iterate through the buffer, copying positions and timestamps
+    while(index < endIndex && index < keyBuffer.endIndex()) {
+        keyPositions_.push_back(keyBuffer[index]);
+        keyTimestamps_.push_back(keyBuffer.timestampAt(index));
+        index++;
+    }
+    
+    // Scale the axes according to what's being displayed. The Y axis should stay
+    // more or less constant (for now), but X will change depending on timestamps
+    xMin_ = keyTimestamps_.front();
+    xMax_ = keyTimestamps_.back();
+    yMin_ = -0.2;
+    yMax_ = 1.2;
+    needsUpdate_ = true;
+}
+
+// Conversion from internal coordinate space to external pixel values and back
+
+// Pixel values go from 0,0 (lower left) to displayPixelWidth_, displayPixelHeight_ (upper right)
+// Internal values go from -totalDisplayWidth_/2, -totalDisplayHeight_/2 (lower left)
+//   to totalDisplayWidth_/2, totalDisplayHeight_/2 (upper right)
+
+// Pixel value in --> OpenGL value out
+KeyPositionGraphDisplay::Point KeyPositionGraphDisplay::screenToInternal(Point& inPoint) {
+	Point out;
+	
+	out.x = -totalDisplayWidth_*0.5 + (inPoint.x/displayPixelWidth_) * totalDisplayWidth_;
+	out.y = -totalDisplayHeight_*0.5 + (inPoint.y/displayPixelHeight_) * totalDisplayHeight_;
+	
+	return out;
+}
+
+// OpenGL value in --> Pixel value out
+KeyPositionGraphDisplay::Point KeyPositionGraphDisplay::internalToScreen(Point& inPoint) {
+	Point out;
+	
+	out.x = ((inPoint.x + totalDisplayWidth_*0.5)/totalDisplayWidth_) * displayPixelWidth_;
+	out.y = ((inPoint.y + totalDisplayHeight_*0.5)/totalDisplayHeight_) * displayPixelHeight_;
+    
+	return out;
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/Display/KeyPositionGraphDisplay.h	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,130 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+
+  KeyPositionGraphDisplay.h: implements a graph of key position over time
+  for a specific key press, which is useful for analysis of continuous key
+  position.
+*/
+
+#ifndef __touchkeys__KeyPositionGraphDisplay__
+#define __touchkeys__KeyPositionGraphDisplay__
+
+#include <iostream>
+#include <vector>
+#include "../JuceLibraryCode/JuceHeader.h"
+#include "../TouchKeys/PianoTypes.h"
+#include "../Utility/Node.h"
+#include "OpenGLDisplayBase.h"
+
+
+
+// This class uses OpenGL to handle drawing of a graph showing key position
+// over time, for debugging or analysis purposes.
+
+class KeyPositionGraphDisplay : public OpenGLDisplayBase {
+	// Internal data structures and constants
+private:
+    // Display margins
+    static const float kDisplaySideMargin;
+    static const float kDisplayBottomMargin;
+    static const float kDisplayTopMargin;
+    
+    // Size of the graph area
+    static const float kDisplayGraphWidth;
+    static const float kDisplayGraphHeight;
+    
+	typedef struct {
+		float x;
+		float y;
+	} Point;
+	
+public:
+	KeyPositionGraphDisplay();
+	
+	// Setup methods for display size and keyboard range
+	void setDisplaySize(float width, float height);
+	
+	// Drawing methods
+	bool needsRender() { return needsUpdate_; }
+	void render();
+	
+	// Interaction methods
+	void mouseDown(float x, float y);
+	void mouseDragged(float x, float y);
+	void mouseUp(float x, float y);
+	void rightMouseDown(float x, float y);
+	void rightMouseDragged(float x, float y);
+	void rightMouseUp(float x, float y);
+    
+    // Data update methods
+    void copyKeyDataFromBuffer(Node<key_position>& keyBuffer, const Node<key_position>::size_type startIndex,
+                               const Node<key_position>::size_type endIndex);
+    void setKeyPressStart(key_position position, timestamp_type timestamp) {
+        pressStartTimestamp_ = timestamp;
+        pressStartPosition_ = position;
+        needsUpdate_ = true;
+    }
+    void setKeyPressFinish(key_position position, timestamp_type timestamp) {
+        pressFinishTimestamp_ = timestamp;
+        pressFinishPosition_ = position;
+        needsUpdate_ = true;
+    }
+    void setKeyReleaseStart(key_position position, timestamp_type timestamp) {
+        releaseStartTimestamp_ = timestamp;
+        releaseStartPosition_ = position;
+        needsUpdate_ = true;
+    }
+    void setKeyReleaseFinish(key_position position, timestamp_type timestamp) {
+        releaseFinishTimestamp_ = timestamp;
+        releaseFinishPosition_ = position;
+        needsUpdate_ = true;
+    }
+    
+private:
+    // Convert mathematical XY coordinate space to drawing positions
+    float graphToDisplayX(float x);
+    float graphToDisplayY(float y);
+    
+	void refreshViewport();
+	
+	// Conversion from internal coordinate space to external pixel values and back
+	Point screenToInternal(Point& inPoint);
+	Point internalToScreen(Point& inPoint);
+	
+    
+private:
+	float displayPixelWidth_, displayPixelHeight_;	// Pixel resolution of the surrounding window
+	float totalDisplayWidth_, totalDisplayHeight_;	// Size of the internal view (centered around origin)
+    float xMin_, xMax_, yMin_, yMax_;               // Coordinates for the graph axes
+
+	bool needsUpdate_;								// Whether the keyboard should be redrawn
+	CriticalSection displayMutex_;					// Synchronize access between data and display threads
+    
+    std::vector<key_position> keyPositions_;        // Positions (0-1 normalized) of the key
+    std::vector<timestamp_type> keyTimestamps_;     // Timestamps corresponding to the above positions
+    
+    // Details on features of key motion: start, end, release, etc.
+    key_position pressStartPosition_, pressFinishPosition_;
+    timestamp_type pressStartTimestamp_, pressFinishTimestamp_;
+    key_position releaseStartPosition_, releaseFinishPosition_;
+    timestamp_type releaseStartTimestamp_, releaseFinishTimestamp_;
+};
+
+
+#endif /* defined(__touchkeys__KeyPositionGraphDisplay__) */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/Display/KeyboardDisplay.cpp	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,696 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+
+  KeyboardDisplay.cpp: displays the keyboard state, including active MIDI
+  notes and current touch position and size.
+*/
+
+#include "KeyboardDisplay.h"
+#include <iostream>
+#include <cmath>
+
+// Class constants
+
+const float KeyboardDisplay::kWhiteKeyFrontWidth = 1.0;
+const float KeyboardDisplay::kBlackKeyWidth = 0.5;
+const float KeyboardDisplay::kWhiteKeyFrontLength = 2.3;
+const float KeyboardDisplay::kWhiteKeyBackLength = 4.1;
+const float KeyboardDisplay::kBlackKeyLength = 4.0;
+const float KeyboardDisplay::kInterKeySpacing = 0.1;
+const float KeyboardDisplay::kAnalogSliderVerticalSpacing = 0.2;
+const float KeyboardDisplay::kAnalogSliderLength = 3.0;
+const float KeyboardDisplay::kAnalogSliderWidth = 0.4;
+const float KeyboardDisplay::kAnalogSliderMinimumValue = -0.2;
+const float KeyboardDisplay::kAnalogSliderMaximumValue = 1.2;
+const float KeyboardDisplay::kAnalogSliderZeroLocation = kAnalogSliderLength * (0.0 - kAnalogSliderMinimumValue) / (kAnalogSliderMaximumValue - kAnalogSliderMinimumValue);
+const float KeyboardDisplay::kAnalogSliderOneLocation = kAnalogSliderLength * (1.0 - kAnalogSliderMinimumValue) / (kAnalogSliderMaximumValue - kAnalogSliderMinimumValue);
+
+// Individual geometry for C, D, E, F, G, A, B, c'
+
+const float KeyboardDisplay::kWhiteKeyBackOffsets[9] = {0, 0.22, 0.42, 0, 0.14, 0.3, 0.44, 0.22, 0};
+const float KeyboardDisplay::kWhiteKeyBackWidths[9] = {0.6, 0.58, 0.58, 0.56, 0.56, 0.56, 0.56, 0.58, 1.0};
+
+// Display margins
+
+const float KeyboardDisplay::kDisplaySideMargin = 0.4;
+const float KeyboardDisplay::kDisplayBottomMargin = 0.8;
+const float KeyboardDisplay::kDisplayTopMargin = 0.8;
+
+// Key shape constants
+
+const int KeyboardDisplay::kShapeForNote[12] = {0, -1, 1, -1, 2, 3, -1, 4, -1, 5, -1, 6};
+const int KeyboardDisplay::kWhiteToChromatic[7] = {0, 2, 4, 5, 7, 9, 11};
+const float KeyboardDisplay::kWhiteKeyFrontBackCutoff = (6.5 / 19.0);
+
+// Touch constants
+const float KeyboardDisplay::kDisplayMinTouchSize = 0.1;
+const float KeyboardDisplay::kDisplayTouchSizeScaler = 0.5;
+
+KeyboardDisplay::KeyboardDisplay() : lowestMidiNote_(0), highestMidiNote_(0),
+totalDisplayWidth_(1.0), totalDisplayHeight_(1.0), displayPixelWidth_(1.0), displayPixelHeight_(1.0),
+needsUpdate_(true), currentHighlightedKey_(-1), touchSensingEnabled_(false), analogSensorsPresent_(false) {
+	// Initialize OpenGL settings: 2D only
+	  
+	//glMatrixMode(GL_PROJECTION);
+	//glDisable(GL_DEPTH_TEST);
+	
+    clearAllTouches();
+    for(int i = 0; i < 128; i++)
+        midiActiveForKey_[i] = false;
+}
+
+void KeyboardDisplay::setKeyboardRange(int lowest, int highest) {
+	if(lowest < 0 || highest < 0)
+		return;
+	
+    ScopedLock sl(displayMutex_);
+    
+	lowestMidiNote_ = lowest;
+	if(keyShape(lowest) < 0)	// Lowest key must always be a white key for display to
+		lowest++;				// render properly
+	
+	highestMidiNote_ = highest;
+	
+	// Recalculate relevant display parameters
+	// Display size is based on the number of white keys
+	
+	int numKeys = 0;
+	for(int i = lowestMidiNote_; i <= highestMidiNote_; i++) {
+		if(keyShape(i) >= 0)
+			numKeys++;
+        if(i >= 0 && i < 128) {
+            analogValueForKey_[i] = 0.0;
+            analogValueIsCalibratedForKey_[i] = false;
+        }
+	}
+	
+	if(numKeys == 0) {
+		return;
+	}
+
+	// Width: N keys, N-1 interkey spaces, 2 side margins
+	totalDisplayWidth_ = (float)numKeys * (kWhiteKeyFrontWidth + kInterKeySpacing) 
+	- kInterKeySpacing + 2.0 * kDisplaySideMargin;
+	
+	// Height: white key height plus top and bottom margins
+    if(analogSensorsPresent_)
+        totalDisplayHeight_ = kDisplayTopMargin + kDisplayBottomMargin + kWhiteKeyFrontLength + kWhiteKeyBackLength
+                                + kAnalogSliderVerticalSpacing + kAnalogSliderLength;
+    else
+        totalDisplayHeight_ = kDisplayTopMargin + kDisplayBottomMargin + kWhiteKeyFrontLength + kWhiteKeyBackLength;
+}
+
+void KeyboardDisplay::setDisplaySize(float width, float height) { 
+    ScopedLock sl(displayMutex_);
+    
+	displayPixelWidth_ = width; 
+	displayPixelHeight_ = height; 
+	refreshViewport();
+}
+
+
+// Render the keyboard display
+
+void KeyboardDisplay::render() {
+	if(lowestMidiNote_ == highestMidiNote_)
+		return;
+	
+	// Start with a light gray background
+	glClearColor(0.8, 0.8, 0.8, 1.0);
+	glClear(GL_COLOR_BUFFER_BIT);
+	glLoadIdentity();
+
+	float invAspectRatio = totalDisplayWidth_ / totalDisplayHeight_; //displayPixelWidth_ / displayPixelHeight_;
+	float scaleValue = 2.0 / totalDisplayWidth_;	
+	
+	glScalef(scaleValue, scaleValue * invAspectRatio, scaleValue);
+	glTranslatef(-1.0 / scaleValue, -totalDisplayHeight_ / 2.0, 0);
+	glTranslatef(kDisplaySideMargin, kDisplayBottomMargin, 0.0);
+	
+    //ScopedLock sl(displayMutex_);
+    
+	glPushMatrix();
+    
+	// Draw the keys themselves first, with analog values if present, then draw the touches
+	for(int key = lowestMidiNote_; key <= highestMidiNote_; key++) {
+		if(keyShape(key) >= 0) {
+			// White keys: draw and move the frame over for the next key
+			drawWhiteKey(0, 0, keyShape(key), key == lowestMidiNote_, key == highestMidiNote_,
+						 /*(key == currentHighlightedKey_) ||*/ midiActiveForKey_[key]);
+            // Analog slider should be centered with respect to the back of the white key
+            if(analogSensorsPresent_ && keyShape(key) >= 0) {
+                float sliderOffset = kWhiteKeyBackOffsets[keyShape(key)] + (kWhiteKeyBackWidths[keyShape(key)] - kAnalogSliderWidth) * 0.5;
+                drawAnalogSlider(sliderOffset, kWhiteKeyFrontLength + kWhiteKeyBackLength + kAnalogSliderVerticalSpacing,
+                                 analogValueIsCalibratedForKey_[key], true, analogValueForKey_[key]);
+            }
+			glTranslatef(kWhiteKeyFrontWidth + kInterKeySpacing, 0, 0);
+		}
+		else {
+			// Black keys: draw and leave the frame in place
+			int previousWhiteKeyShape = keyShape(key - 1);
+			float offsetH = -1.0 + kWhiteKeyBackOffsets[previousWhiteKeyShape] + kWhiteKeyBackWidths[previousWhiteKeyShape];
+			float offsetV = kWhiteKeyFrontLength + kWhiteKeyBackLength - kBlackKeyLength;
+
+			glTranslatef(offsetH, offsetV, 0.0);
+			drawBlackKey(0, 0, /*(key == currentHighlightedKey_) ||*/ midiActiveForKey_[key]);
+            if(analogSensorsPresent_) {
+                drawAnalogSlider((kBlackKeyWidth - kAnalogSliderWidth) * 0.5, kBlackKeyLength + kAnalogSliderVerticalSpacing,
+                                 analogValueIsCalibratedForKey_[key], false, analogValueForKey_[key]);
+            }
+			glTranslatef(-offsetH, -offsetV, 0.0);
+		}
+	}
+	
+	// Restore to the original location we used when drawing the keys
+	glPopMatrix();
+	
+    // Transfer the touch display data from storage buffer to a local copy we can use for display.
+    // This avoids OpenGL calls happening while the mutex is locked, which stalls the data producer thread
+    displayMutex_.enter();
+    memcpy(currentTouchesMirror_, currentTouches_, 128*sizeof(TouchInfo));
+    displayMutex_.exit();
+    
+	// Draw touches
+	for(int key = lowestMidiNote_; key <= highestMidiNote_; key++) {
+		if(keyShape(key) >= 0) {
+			// Check whether there are any current touches for this key
+			//if(currentTouches_.count(key) > 0) {
+            if(currentTouchesMirror_[key].active) {
+				TouchInfo& t = currentTouchesMirror_[key];
+				
+				if(t.locV1 >= 0)
+					drawWhiteTouch(0, 0, keyShape(key), t.locH, t.locV1, t.size1);
+				if(t.locV2 >= 0)
+					drawWhiteTouch(0, 0, keyShape(key), t.locH, t.locV2, t.size2);
+				if(t.locV3 >= 0)
+					drawWhiteTouch(0, 0, keyShape(key), t.locH, t.locV3, t.size3);
+			}
+			
+			glTranslatef(kWhiteKeyFrontWidth + kInterKeySpacing, 0, 0);			
+		}
+		else {
+			// Black keys: draw and leave the frame in place
+			int previousWhiteKeyShape = keyShape(key - 1);
+			float offsetH = -1.0 + kWhiteKeyBackOffsets[previousWhiteKeyShape] + kWhiteKeyBackWidths[previousWhiteKeyShape];
+			float offsetV = kWhiteKeyFrontLength + kWhiteKeyBackLength - kBlackKeyLength;
+			
+			glTranslatef(offsetH, offsetV, 0.0);
+			
+			// Check whether there are any current touches for this key
+			//if(currentTouches_.count(key) > 0) {
+            if(currentTouchesMirror_[key].active) {
+				TouchInfo& t = currentTouchesMirror_[key];
+				
+				if(t.locV1 >= 0)
+					drawBlackTouch(0, 0, t.locH, t.locV1, t.size1);
+				if(t.locV2 >= 0)
+					drawBlackTouch(0, 0, t.locH, t.locV2, t.size2);
+				if(t.locV3 >= 0)
+					drawBlackTouch(0, 0, t.locH, t.locV3, t.size3);
+			}
+			
+			glTranslatef(-offsetH, -offsetV, 0.0);
+		}
+	}
+
+	needsUpdate_ = false;
+    
+	glFlush();
+}
+
+// Mouse interaction methods
+
+void KeyboardDisplay::mouseDown(float x, float y) {
+	Point mousePoint = {x, y};
+	Point scaledPoint = screenToInternal(mousePoint);	
+	
+	currentHighlightedKey_ = keyForLocation(scaledPoint);
+	needsUpdate_ = true;
+}
+
+void KeyboardDisplay::mouseDragged(float x, float y) {
+	Point mousePoint = {x, y};
+	Point scaledPoint = screenToInternal(mousePoint);	
+	
+	currentHighlightedKey_ = keyForLocation(scaledPoint);
+	needsUpdate_ = true;
+}
+
+void KeyboardDisplay::mouseUp(float x, float y) {
+	//Point mousePoint = {x, y};
+	//Point scaledPoint = screenToInternal(mousePoint);
+	
+	// When the mouse is released, see if it was over a key.  If so, take any action
+	// associated with clicking that key.
+	
+	if(currentHighlightedKey_ != -1)
+		keyClicked(currentHighlightedKey_);
+	
+	//currentHighlightedKey_ = -1;
+	needsUpdate_ = true;	
+}
+
+void KeyboardDisplay::rightMouseDown(float x, float y) {
+	Point mousePoint = {x, y};	
+	Point scaledPoint = screenToInternal(mousePoint);
+	
+	int key = keyForLocation(scaledPoint);
+	if(key != -1)
+		keyRightClicked(key);
+	
+	needsUpdate_ = true;
+}
+
+void KeyboardDisplay::rightMouseDragged(float x, float y) {
+	//Point mousePoint = {x, y};
+	//Point scaledPoint = screenToInternal(mousePoint);
+}
+
+void KeyboardDisplay::rightMouseUp(float x, float y) {
+	//Point mousePoint = {x, y};
+	//Point scaledPoint = screenToInternal(mousePoint);
+}
+
+void KeyboardDisplay::keyClicked(int key) {
+	
+}
+
+void KeyboardDisplay::keyRightClicked(int key) {
+	
+}
+
+// Insert new touch information for the given key and request a display update.
+
+void KeyboardDisplay::setTouchForKey(int key, const KeyTouchFrame& touch) {
+	if(key < lowestMidiNote_ || key > highestMidiNote_)
+		return;
+    
+    ScopedLock sl(displayMutex_);
+	
+	//TouchInfo t = {touch.locH, touch.locs[0], touch.locs[1], touch.locs[2], touch.sizes[0], touch.sizes[1], touch.sizes[2]};
+	//currentTouches_[key] = t;
+	currentTouches_[key] = {true, touch.locH, touch.locs[0], touch.locs[1], touch.locs[2], touch.sizes[0], touch.sizes[1], touch.sizes[2]};
+    
+	needsUpdate_ = true;
+}
+
+// Clear touch information for this key
+
+void KeyboardDisplay::clearTouchForKey(int key) {
+    ScopedLock sl(displayMutex_);
+	
+	//currentTouches_.erase(key);
+    currentTouches_[key].active = 0;
+    
+	needsUpdate_ = true;
+}
+
+// Clear all current touch information
+
+void KeyboardDisplay::clearAllTouches() {
+    ScopedLock sl(displayMutex_);
+	
+	//currentTouches_.clear();
+    for(int i = 0; i < 128; i++)
+        currentTouches_[i].active = false;
+    
+	needsUpdate_ = true;
+}
+
+// Indicate whether the given key is calibrated or not
+
+void KeyboardDisplay::setAnalogCalibrationStatusForKey(int key, bool isCalibrated) {
+    if(key < 0 || key > 127)
+        return;
+    analogValueIsCalibratedForKey_[key] = isCalibrated;
+    needsUpdate_ = true;
+}
+
+// Set the current value of the analog sensor for the given key.
+// Whether calibrated or not, the data should be in the range 0.0-1.0
+// with a bit of room for deviation outside that range (i.e. for extra key
+// pressure > 1.0, or for resting key states slightly miscalibrated < 0.0).
+
+void KeyboardDisplay::setAnalogValueForKey(int key, float value) {
+    if(key < 0 || key > 127)
+        return;
+    analogValueForKey_[key] = value;
+    needsUpdate_ = true;
+}
+
+// Clear all the analog data for all keys
+void KeyboardDisplay::clearAnalogData() {
+    for(int key = 0; key < 128; key++) {
+        analogValueForKey_[key] = 0.0;
+    }
+    needsUpdate_ = true;
+}
+
+void KeyboardDisplay::setMidiActive(int key, bool active) {
+    if(key < 0 || key > 127)
+        return;
+    midiActiveForKey_[key] = active;
+    needsUpdate_ = true;
+}
+
+void KeyboardDisplay::clearMidiData() {
+    for(int key = 0; key < 128; key++)
+        midiActiveForKey_[key] = false;
+    needsUpdate_ = true;
+}
+
+// Indicate whether a given key has touch sensing capability
+
+void KeyboardDisplay::setTouchSensorPresentForKey(int key, bool present) {
+	if(key < 0 || key > 127 || !touchSensingEnabled_)
+		return;
+	touchSensingPresentOnKey_[key] = present;
+}
+
+// Indicate whether touch sensing is active at all on the keyboard.
+// Clear all key-specific information on whether a touch-sensing key is connected
+
+void KeyboardDisplay::setTouchSensingEnabled(bool enabled) {
+	touchSensingEnabled_ = enabled;
+	
+	for(int i = 0; i < 128; i++)
+		touchSensingPresentOnKey_[i] = false;
+}
+
+// Draw the outline of a white key.  Shape ranges from 0-7, giving the type of white key to draw
+// Coordinates give the lower-left corner of the key
+
+void KeyboardDisplay::drawWhiteKey(float x, float y, int shape, bool first, bool last, bool highlighted) {
+	// First and last keys will have special geometry since there is no black key below
+	// Figure out the precise geometry in this case...
+	
+	float backOffset, backWidth;
+	
+	if(first) {
+		backOffset = 0.0;
+		backWidth = kWhiteKeyBackOffsets[shape] + kWhiteKeyBackWidths[shape];
+	}
+	else if(last) {
+		backOffset = kWhiteKeyBackOffsets[shape];
+		backWidth = 1.0 - kWhiteKeyBackOffsets[shape];
+	}
+	else {
+		backOffset = kWhiteKeyBackOffsets[shape];
+		backWidth = kWhiteKeyBackWidths[shape]; 
+	}
+	
+	// First draw white fill as two squares
+	glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);	
+	if(highlighted)
+		glColor3f(1.0, 0.8, 0.8);
+	else
+		glColor3f(1.0, 1.0, 1.0);
+	glBegin(GL_QUADS);
+	
+	glVertex2f(x, y);
+	glVertex2f(x, y + kWhiteKeyFrontLength);
+	glVertex2f(x + kWhiteKeyFrontWidth, y + kWhiteKeyFrontLength);
+	glVertex2f(x + kWhiteKeyFrontWidth, y);
+	
+	glVertex2f(x + backOffset, y + kWhiteKeyFrontLength);
+	glVertex2f(x + backOffset, y + kWhiteKeyFrontLength + kWhiteKeyBackLength);
+	glVertex2f(x + backOffset + backWidth, y + kWhiteKeyFrontLength + kWhiteKeyBackLength);
+	glVertex2f(x + backOffset + backWidth, y + kWhiteKeyFrontLength);
+	
+	glEnd();
+	
+	// Now draw the outline as black line segments
+	glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
+    glColor3f(0.0, 0.0, 0.0);			
+	glBegin(GL_POLYGON);
+	
+	glVertex2f(x, y);
+    glVertex2f(x, y + kWhiteKeyFrontLength);
+    glVertex2f(x + backOffset, y + kWhiteKeyFrontLength);
+    glVertex2f(x + backOffset, y + kWhiteKeyFrontLength + kWhiteKeyBackLength);
+    glVertex2f(x + backOffset + backWidth, y + kWhiteKeyFrontLength + kWhiteKeyBackLength);
+    glVertex2f(x + backOffset + backWidth, y + kWhiteKeyFrontLength);
+    glVertex2f(x + kWhiteKeyFrontWidth, y + kWhiteKeyFrontLength);
+    glVertex2f(x + kWhiteKeyFrontWidth, y);
+
+	glEnd();
+}
+
+// Draw the outline of a black key, given its lower-left corner
+
+void KeyboardDisplay::drawBlackKey(float x, float y, bool highlighted) {
+	glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
+	if(highlighted)
+		glColor3f(0.7, 0.0, 0.0);
+	else
+		glColor3f(0.0, 0.0, 0.0);					// Display color black
+	glBegin(GL_POLYGON);
+	
+	glVertex2f(x, y);
+    glVertex2f(x, y + kBlackKeyLength);
+	glVertex2f(x + kBlackKeyWidth, y + kBlackKeyLength);
+	glVertex2f(x + kBlackKeyWidth, y);
+	
+	glEnd();
+}
+
+// Draw a circle indicating a touch on the white key surface
+
+void KeyboardDisplay::drawWhiteTouch(float x, float y, int shape, float touchLocH, float touchLocV, float touchSize) {
+	glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
+	glColor3f(1.0, 0.0, 1.0);
+	
+	glBegin(GL_POLYGON);
+	if(/*touchLocV < kWhiteKeyFrontBackCutoff && */touchLocH >= 0.0) { // FIXME: find a more permanent solution
+		// Here, the touch is in a location that has both horizontal and vertical information.
+		for(int i = 0; i < 360; i += 5) {
+			glVertex2f(x + cosf((float)i*3.14159/180.0)*(kDisplayMinTouchSize + touchSize*kDisplayTouchSizeScaler)
+					   + touchLocH*kWhiteKeyFrontWidth,
+					   y + sinf((float)i*3.14159/180.0)*(kDisplayMinTouchSize + touchSize*kDisplayTouchSizeScaler)
+					   + kWhiteKeyFrontLength*(touchLocV/kWhiteKeyFrontBackCutoff));
+		}
+	}
+	else {
+		// The touch is in the back part of the key, or for some reason lacks horizontal information
+		for(int i = 0; i < 360; i += 5) {
+			glVertex2f(x + cosf((float)i*3.14159/180.0)*(kDisplayMinTouchSize + touchSize*kDisplayTouchSizeScaler)
+					   + kWhiteKeyBackOffsets[shape] + kWhiteKeyBackWidths[shape]/2,
+					   y + sinf((float)i*3.14159/180.0)*(kDisplayMinTouchSize + touchSize*kDisplayTouchSizeScaler) 
+					   + kWhiteKeyFrontLength + (kWhiteKeyBackLength*
+												 ((touchLocV-kWhiteKeyFrontBackCutoff)/(1.0-kWhiteKeyFrontBackCutoff))));
+		}		
+	}
+	glEnd();
+}
+
+// Draw a circle indicating a touch on the black key surface
+
+void KeyboardDisplay::drawBlackTouch(float x, float y, float touchLocH, float touchLocV, float touchSize) {
+	glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
+	glColor3f(0.0, 1.0, 0.0);
+	
+	glBegin(GL_POLYGON);
+    
+    if(touchLocH < 0.0)
+        touchLocH = 0.5;
+
+	for(int i = 0; i < 360; i += 5) {
+		glVertex2f(x + cosf((float)i*3.14159/180.0)*(kDisplayMinTouchSize + touchSize*kDisplayTouchSizeScaler)
+                   + touchLocH * kBlackKeyWidth,
+				   y + sinf((float)i*3.14159/180.0)*(kDisplayMinTouchSize + touchSize*kDisplayTouchSizeScaler) + kBlackKeyLength*touchLocV);
+	}	
+
+	glEnd();
+}
+
+// Draw a slider bar indicating the current key analog position
+
+void KeyboardDisplay::drawAnalogSlider(float x, float y, bool calibrated, bool whiteKey, float value) {
+    // First some gray lines indicating the 0.0 and 1.0 marks
+    glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
+    glColor3f(0.5, 0.5, 0.5);
+    
+	glBegin(GL_POLYGON);
+	glVertex2f(x, y + kAnalogSliderZeroLocation);
+    glVertex2f(x, y + kAnalogSliderOneLocation);
+	glVertex2f(x + kAnalogSliderWidth, y + kAnalogSliderOneLocation);
+	glVertex2f(x + kAnalogSliderWidth, y + kAnalogSliderZeroLocation);
+	glEnd();
+    
+    // Draw a red box at the top for uncalibrated values
+    if(!calibrated) {
+        glColor3f(1.0, 0.0, 0.0);
+        glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
+        glBegin(GL_POLYGON);
+        glVertex2f(x, y + kAnalogSliderOneLocation);
+        glVertex2f(x, y + kAnalogSliderLength);
+        glVertex2f(x + kAnalogSliderWidth, y + kAnalogSliderLength);
+        glVertex2f(x + kAnalogSliderWidth, y + kAnalogSliderOneLocation);
+        glEnd();
+    }
+    
+    // Next the filled part indicating the specific value (same color as touches), then the outline
+    if(whiteKey)
+        glColor3f(1.0, 0.0, 1.0);
+    else
+        glColor3f(0.0, 1.0, 0.0);
+    glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
+	glBegin(GL_POLYGON);
+    
+    float locationForValue = kAnalogSliderLength * (value - kAnalogSliderMinimumValue) / (kAnalogSliderMaximumValue - kAnalogSliderMinimumValue);
+    if(locationForValue < 0.0)
+        locationForValue = 0.0;
+    if(locationForValue > kAnalogSliderLength)
+        locationForValue = kAnalogSliderLength;
+    
+    // Draw solid box from 0.0 to current value
+    glVertex2f(x, y + kAnalogSliderZeroLocation);
+    glVertex2f(x, y + locationForValue);
+    glVertex2f(x + kAnalogSliderWidth, y + locationForValue);
+    glVertex2f(x + kAnalogSliderWidth, y + kAnalogSliderZeroLocation);
+	glEnd();
+    
+    glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
+    glColor3f(0.0, 0.0, 0.0);
+    
+	glBegin(GL_POLYGON);
+	glVertex2f(x, y);
+    glVertex2f(x, y + kAnalogSliderLength);
+	glVertex2f(x + kAnalogSliderWidth, y + kAnalogSliderLength);
+	glVertex2f(x + kAnalogSliderWidth, y);
+	glEnd();
+}
+
+void KeyboardDisplay::refreshViewport() {
+	//glViewport(0, 0, displayPixelWidth_, displayPixelHeight_);
+}
+
+// Conversion from internal coordinate space to external pixel values and back
+
+// Pixel values go from 0,0 (lower left) to displayPixelWidth_, displayPixelHeight_ (upper right)
+// Internal values go from -totalDisplayWidth_/2, -totalDisplayHeight_/2 (lower left)
+//   to totalDisplayWidth_/2, totalDisplayHeight_/2 (upper right)
+
+// Pixel value in --> OpenGL value out
+KeyboardDisplay::Point KeyboardDisplay::screenToInternal(Point& inPoint) {
+	Point out;
+	
+	out.x = -totalDisplayWidth_*0.5 + (inPoint.x/displayPixelWidth_) * totalDisplayWidth_;
+	out.y = -totalDisplayHeight_*0.5 + (inPoint.y/displayPixelHeight_) * totalDisplayHeight_;
+	
+	return out;
+}
+
+// OpenGL value in --> Pixel value out
+KeyboardDisplay::Point KeyboardDisplay::internalToScreen(Point& inPoint) {
+	Point out;
+	
+	out.x = ((inPoint.x + totalDisplayWidth_*0.5)/totalDisplayWidth_) * displayPixelWidth_;
+	out.y = ((inPoint.y + totalDisplayHeight_*0.5)/totalDisplayHeight_) * displayPixelHeight_;
+			  
+	return out;
+}
+
+// Given an internal-coordinate representation, return the number of the key that it belongs
+// in, otherwise return -1 if no key matches.
+
+int KeyboardDisplay::keyForLocation(Point& internalPoint) {
+	// First, check that the point is within the overall bounding box of the keyboard
+	if(internalPoint.y < -totalDisplayHeight_*0.5 + kDisplayBottomMargin ||
+	   internalPoint.y > totalDisplayHeight_*0.5 - kDisplayTopMargin)
+		return -1;
+	if(internalPoint.x < -totalDisplayWidth_*0.5 + kDisplaySideMargin ||
+	   internalPoint.x > totalDisplayWidth_*0.5 - kDisplaySideMargin)
+		return -1;
+	
+	// Now, look for the key region corresponding to this horizontal location
+	// hLoc indicates the relative distance from the beginning of the first key
+	
+	float hLoc = internalPoint.x + totalDisplayWidth_*0.5 - kDisplaySideMargin;
+	
+	if(hLoc < 0.0)
+		return -1;
+	
+	// normalizedHLoc indicates the index of the white key this touch is near.
+	float normalizedHLoc = hLoc / (kWhiteKeyFrontWidth + kInterKeySpacing);
+	
+	// Two relevant regions: front of the white keys, back of the white keys with black keys
+	// Distinguish them by vertical position.
+	
+	int shapeOfBottomKey = keyShape(lowestMidiNote_);						// White key index of lowest key
+	int lowestC = (lowestMidiNote_ / 12) * 12;								// C below lowest key
+	int whiteKeyNumber = floorf(normalizedHLoc);							// Number of white key
+	int whiteOctaveNumber = (whiteKeyNumber + shapeOfBottomKey) / 7;		// Octave the key is in
+	int chromaticKeyNumber = 12 * whiteOctaveNumber + kWhiteToChromatic[(whiteKeyNumber + shapeOfBottomKey) % 7];
+	
+	// Check if we're on the front area of the white keys, and if so, ignore points located in the gaps
+	// between the keys
+	
+	if(internalPoint.y + totalDisplayHeight_*0.5 - kDisplayBottomMargin <= kWhiteKeyFrontLength) {
+		if(normalizedHLoc - floorf(normalizedHLoc) > kWhiteKeyFrontWidth / (kWhiteKeyFrontWidth + kInterKeySpacing))
+			return -1;		
+		return lowestC + chromaticKeyNumber;		
+	}
+	else {
+		// Back of white keys, or black keys
+		
+		int whiteKeyShape = keyShape(chromaticKeyNumber);
+		if(whiteKeyShape < 0)	// Shouldn't happen
+			return -1;
+		
+		float locRelativeToLeft = (normalizedHLoc - floorf(normalizedHLoc)) * (kWhiteKeyFrontWidth + kInterKeySpacing);
+		
+		// Check if we are in the back region of the white key.  Handle the lowest and highest notes specially since
+		// the white keys are generally wider on account of no adjacent black key.
+		if(lowestC + chromaticKeyNumber == lowestMidiNote_) {
+			if(locRelativeToLeft <= kWhiteKeyBackOffsets[whiteKeyShape] + kWhiteKeyBackWidths[whiteKeyShape])
+				return lowestC + chromaticKeyNumber;
+		}
+		else if(lowestC + chromaticKeyNumber == highestMidiNote_) {
+			if(locRelativeToLeft >= kWhiteKeyBackOffsets[whiteKeyShape])
+				return lowestC + chromaticKeyNumber;
+		}
+		else if(locRelativeToLeft >= kWhiteKeyBackOffsets[whiteKeyShape] &&
+		   locRelativeToLeft <= kWhiteKeyBackOffsets[whiteKeyShape] + kWhiteKeyBackWidths[whiteKeyShape]) {
+			return lowestC + chromaticKeyNumber;	
+		}
+		
+		// By now, we've established that we're not on top of a white key.  See if we align to a black key.
+		// Watch the vertical gap between white and black keys
+		if(internalPoint.y + totalDisplayHeight_*0.5 - kDisplayBottomMargin <=
+		   kWhiteKeyFrontLength + kWhiteKeyBackLength - kBlackKeyLength)
+			return -1;
+		
+		// Is there a black key below this white key?
+		if(keyShape(chromaticKeyNumber - 1) < 0) {		
+			if(locRelativeToLeft <= kWhiteKeyBackOffsets[whiteKeyShape] - kInterKeySpacing &&
+			   lowestC + chromaticKeyNumber > lowestMidiNote_)
+				return lowestC + chromaticKeyNumber - 1;
+		}
+		// Or is there a black key above this white key?
+		if(keyShape(chromaticKeyNumber + 1) < 0) {
+			if(locRelativeToLeft >= kWhiteKeyBackOffsets[whiteKeyShape] + kWhiteKeyBackWidths[whiteKeyShape] + kInterKeySpacing
+			   && lowestC + chromaticKeyNumber < highestMidiNote_)
+				return lowestC + chromaticKeyNumber + 1;
+		}
+	}
+
+	// If all else fails, assume we're not on any key
+	return -1;
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/Display/KeyboardDisplay.h	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,181 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+
+  KeyboardDisplay.h: displays the keyboard state, including active MIDI
+  notes and current touch position and size.
+*/
+
+#ifndef KEYBOARD_DISPLAY_H
+#define KEYBOARD_DISPLAY_H
+
+#include <iostream>
+#include <map>
+//#include <OpenGL/gl.h>
+#include "../JuceLibraryCode/JuceHeader.h"
+#include "../TouchKeys/KeyTouchFrame.h"
+#include "OpenGLDisplayBase.h"
+
+
+// This class uses OpenGL to implement the actual drawing of the piano keyboard graphics.
+// Graphics include the current state of each key and the touches on the surface.
+
+class KeyboardDisplay : public OpenGLDisplayBase {
+	// Internal data structures and constants
+private:
+    // Display dimensions, normalized to the width of one white key
+ 
+    static const float kWhiteKeyFrontWidth;
+    static const float kBlackKeyWidth;
+    static const float kWhiteKeyFrontLength;
+    static const float kWhiteKeyBackLength;
+    static const float kBlackKeyLength;
+    static const float kInterKeySpacing;
+    static const float kAnalogSliderVerticalSpacing;
+    static const float kAnalogSliderLength;
+    static const float kAnalogSliderWidth;
+    static const float kAnalogSliderMinimumValue;
+    static const float kAnalogSliderMaximumValue;
+    static const float kAnalogSliderZeroLocation;
+    static const float kAnalogSliderOneLocation;
+    
+    // Individual geometry for C, D, E, F, G, A, B, c'
+    
+    static const float kWhiteKeyBackOffsets[9];
+    static const float kWhiteKeyBackWidths[9];
+    
+    // Display margins
+    
+    static const float kDisplaySideMargin;
+    static const float kDisplayBottomMargin;
+    static const float kDisplayTopMargin;
+    
+    // Key shape constants
+    
+    static const int kShapeForNote[12];
+    static const int kWhiteToChromatic[7];
+    static const float kWhiteKeyFrontBackCutoff;
+    
+    // Touch constants
+    static const float kDisplayMinTouchSize;
+    static const float kDisplayTouchSizeScaler;
+    
+    
+	typedef struct {
+        bool  active;
+		float locH;
+		float locV1;
+		float locV2;
+		float locV3;
+		float size1;
+		float size2;
+		float size3;
+	} TouchInfo;
+	
+	typedef struct {
+		float x;
+		float y;
+	} Point;
+	
+public:
+	KeyboardDisplay();
+	
+	// Setup methods for display size and keyboard range
+	void setKeyboardRange(int lowest, int highest);
+	float keyboardAspectRatio() { return totalDisplayWidth_ / totalDisplayHeight_; }
+	void setDisplaySize(float width, float height);
+	
+	// Drawing methods
+	bool needsRender() { return needsUpdate_; }
+	void render();
+	
+	// Interaction methods
+	void mouseDown(float x, float y);
+	void mouseDragged(float x, float y);
+	void mouseUp(float x, float y);
+	void rightMouseDown(float x, float y);
+	void rightMouseDragged(float x, float y);
+	void rightMouseUp(float x, float y);
+	
+	// Take action associated with clicking a key.  These are called within the mouse
+	// methods but may also be called externally.
+	void keyClicked(int key);
+	void keyRightClicked(int key);
+	
+	// State-change methods
+	void setTouchForKey(int key, const KeyTouchFrame& touch);
+	void clearTouchForKey(int key);
+	void clearAllTouches();
+    void setAnalogCalibrationStatusForKey(int key, bool isCalibrated);
+	void setAnalogValueForKey(int key, float value);
+    void clearAnalogData();
+    void setMidiActive(int key, bool active);
+    void clearMidiData();
+    
+    void setAnalogSensorsPresent(bool present) { analogSensorsPresent_ = present; }
+	void setTouchSensorPresentForKey(int key, bool present);
+	void setTouchSensingEnabled(bool enabled);
+	
+private:
+	void drawWhiteKey(float x, float y, int shape, bool first, bool last, bool highlighted);
+	void drawBlackKey(float x, float y, bool highlighted);
+	
+	void drawWhiteTouch(float x, float y, int shape, float touchLocH, float touchLocV, float touchSize);
+	void drawBlackTouch(float x, float y, float touchLocH, float touchLocV, float touchSize);
+    
+    void drawAnalogSlider(float x, float y, bool calibrated, bool whiteKey, float value);
+	
+	// Indicate the shape of the given MIDI note.  0-6 for white keys C-B, -1 for black keys.
+	// We handle unusual shaped keys at the top or bottom of the keyboard separately.
+	
+	int keyShape(int key) { 
+		if(key < 0) return -1;
+		return kShapeForNote[key % 12]; 
+	}
+	
+	void refreshViewport();
+	
+	// Conversion from internal coordinate space to external pixel values and back
+	Point screenToInternal(Point& inPoint);
+	Point internalToScreen(Point& inPoint);
+	
+	// Figure out which key (if any) the current point corresponds to
+	int keyForLocation(Point& internalPoint);
+		
+private:
+	
+	int lowestMidiNote_, highestMidiNote_;			// Which keys should be displayed (use MIDI note numbers)	
+	float totalDisplayWidth_, totalDisplayHeight_;	// Size of the internal view (centered around origin)
+    float displayPixelWidth_, displayPixelHeight_;	// Pixel resolution of the surrounding window
+	bool needsUpdate_;								// Whether the keyboard should be redrawn
+	int currentHighlightedKey_;						// What key is being clicked on at the moment
+	bool touchSensingEnabled_;						// Whether touch-sensitive keys are being used
+	bool touchSensingPresentOnKey_[128];			// Whether the key with this MIDI note has a touch sensor
+    
+    bool analogSensorsPresent_;                     // Whether the given device has analog sensors at all
+    bool analogValueIsCalibratedForKey_[128];       // Whether the analog sensor is calibrated on this key
+    float analogValueForKey_[128];                  // Latest analog sensor value for each key
+    bool midiActiveForKey_[128];                    // Whether the MIDI note is on for each key
+	
+    TouchInfo currentTouches_[128];                 // Touch data for each key
+    TouchInfo currentTouchesMirror_[128];           // Mirror of the above, used for active display
+	//std::map<int, TouchInfo> currentTouches_;		// Collection of current touch data
+	CriticalSection displayMutex_;					// Synchronize access between data and display threads
+};
+
+#endif /* KEYBOARD_DISPLAY_H */
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/Display/OpenGLDisplayBase.h	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,52 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+
+  OpenGLDisplayBase.h: virtual base class for a renderer that handles
+  arbitrary display sizes and user mouse events.
+*/
+
+#ifndef touchkeys_OpenGLDisplayBase_h
+#define touchkeys_OpenGLDisplayBase_h
+
+// Virtual base class that implements some basic methods that the OS-specific
+// GUI can attach to. Specific displays are subclasses of this
+
+class OpenGLDisplayBase {
+public:
+    OpenGLDisplayBase() {}
+    
+    virtual ~OpenGLDisplayBase() {}
+	
+	// Setup method for display size
+	virtual void setDisplaySize(float width, float height) = 0;
+	
+	// Drawing methods
+	virtual bool needsRender() = 0;
+	virtual void render() = 0;
+	
+	// Interaction methods
+	virtual void mouseDown(float x, float y) = 0;
+	virtual void mouseDragged(float x, float y) = 0;
+	virtual void mouseUp(float x, float y) = 0;
+	virtual void rightMouseDown(float x, float y) = 0;
+	virtual void rightMouseDragged(float x, float y) = 0;
+	virtual void rightMouseUp(float x, float y) = 0;
+};
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/Display/OpenGLJuceCanvas.h	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,96 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+
+  OpenGLJuceCanvas.h: Juce Component subclass which connects to the
+  OpenGLDisplayBase class. Provides a generic interface between Juce and
+  the specific OpenGL renderer.
+*/
+
+#ifndef __TouchKeys__OpenGLJuceCanvas__
+#define __TouchKeys__OpenGLJuceCanvas__
+
+#include "../JuceLibraryCode/JuceHeader.h"
+#include "OpenGLDisplayBase.h"
+
+#if JUCE_OPENGL
+
+//==============================================================================
+class OpenGLJuceCanvas  : public Component,
+                          public OpenGLRenderer
+{
+public:
+    // *** Constructor / Destructor ***
+    OpenGLJuceCanvas(OpenGLDisplayBase& display) : display_(display) {
+        openGLContext_.setRenderer (this);
+        openGLContext_.setComponentPaintingEnabled (true);
+        openGLContext_.setContinuousRepainting (true);
+        openGLContext_.attachTo (*this);
+    }
+    ~OpenGLJuceCanvas() {
+        openGLContext_.detach();
+    }
+    
+    // *** OpenGL Context Methods ***
+    void newOpenGLContextCreated() {}
+    void openGLContextClosing() {}
+    
+    void mouseDown (const MouseEvent& e)
+    {
+        if(e.mods.isLeftButtonDown())
+            display_.mouseDown(e.x, e.y);
+        else if(e.mods.isRightButtonDown())
+            display_.rightMouseDown(e.x, e.y);
+    }
+    
+    void mouseDrag (const MouseEvent& e)
+    {
+        if(e.mods.isLeftButtonDown())
+            display_.mouseDragged(e.x, e.y);
+        else if(e.mods.isRightButtonDown())
+            display_.rightMouseDragged(e.x, e.y);
+    }
+    
+    void mouseUp (const MouseEvent& e)
+    {
+        if(e.mods.isLeftButtonDown())
+            display_.mouseUp(e.x, e.y);
+        else if(e.mods.isRightButtonDown())
+            display_.rightMouseUp(e.x, e.y);
+    }
+    
+    
+    void resized() {
+        display_.setDisplaySize(getWidth(), getHeight());
+    }
+    
+    void paint (Graphics&) {}
+    
+    void renderOpenGL()
+    {
+        display_.render();
+    }
+    
+private:
+    OpenGLDisplayBase& display_;    // Reference to the TouchKeys-specific OpenGL Display
+    OpenGLContext openGLContext_;
+};
+
+#endif /* JUCE_OPENGL */
+
+#endif /* defined(__TouchKeys__OpenGLJuceCanvas__) */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/Display/RawSensorDisplay.cpp	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,188 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+
+  RawSensorDisplay.cpp: simple graph for showing raw TouchKeys sensor values
+*/
+
+#include "RawSensorDisplay.h"
+#include <iostream>
+#include <cmath>
+
+// Class constants
+// Display margins
+const float RawSensorDisplay::kDisplaySideMargin = 0.5;
+const float RawSensorDisplay::kDisplayBottomMargin = 0.5;
+const float RawSensorDisplay::kDisplayTopMargin = 0.5;
+
+// Size of bar graphs and spacing
+const float RawSensorDisplay::kDisplayBarWidth = 0.5;
+const float RawSensorDisplay::kDisplayBarSpacing = 0.25;
+const float RawSensorDisplay::kDisplayBarHeight = 10.0;
+
+
+RawSensorDisplay::RawSensorDisplay() :
+displayPixelWidth_(1.0), displayPixelHeight_(1.0), totalDisplayWidth_(1.0), totalDisplayHeight_(1.0), 
+yMin_(-10), yMax_(256), needsUpdate_(true) {
+	// Initialize OpenGL settings: 2D only
+    
+	//glMatrixMode(GL_PROJECTION);
+	//glDisable(GL_DEPTH_TEST);
+    
+    totalDisplayWidth_ = kDisplaySideMargin*2 + kDisplayBarWidth;
+    totalDisplayHeight_ = kDisplayTopMargin + kDisplayBottomMargin + kDisplayBarHeight;
+}
+
+void RawSensorDisplay::setDisplaySize(float width, float height) {
+    ScopedLock sl(displayMutex_);
+    
+	displayPixelWidth_ = width;
+	displayPixelHeight_ = height;
+	refreshViewport();
+}
+
+
+// Render the keyboard display
+
+void RawSensorDisplay::render() {
+	// Start with a light gray background
+	glClearColor(0.8, 0.8, 0.8, 1.0);
+	glClear(GL_COLOR_BUFFER_BIT);
+	glLoadIdentity();
+    
+    float invAspectRatio = totalDisplayWidth_ / totalDisplayHeight_;
+	float scaleValue = 2.0 / totalDisplayWidth_;
+	
+	glScalef(scaleValue, scaleValue * invAspectRatio, scaleValue);
+	glTranslatef(-1.0 / scaleValue, -totalDisplayHeight_ / 2.0, 0);
+	glTranslatef(0.0, kDisplayBottomMargin, 0.0);
+	
+    ScopedLock sl(displayMutex_);
+    
+    // Draw the line for zero value
+    glColor3f(0.5, 0.5, 0.5);
+    glBegin(GL_LINES);
+    glVertex2f(0, graphToDisplayY(0.0));
+    glVertex2f(totalDisplayWidth_, graphToDisplayY(0.0));
+    glEnd();
+    
+    glTranslatef(kDisplaySideMargin, 0.0, 0.0);
+    
+    for(int i = 0; i < displayValues_.size(); i++) {
+        // Draw each bar in sequence
+        glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
+        glColor3f(1.0, 0.0, 0.0);
+        
+        glBegin(GL_POLYGON);
+        glVertex2f(0, graphToDisplayY(0));
+        glVertex2f(0, graphToDisplayY(displayValues_[i]));
+        glVertex2f(kDisplayBarWidth, graphToDisplayY(displayValues_[i]));
+        glVertex2f(kDisplayBarWidth, graphToDisplayY(0));
+        glEnd();
+        
+        glTranslatef(kDisplayBarWidth + kDisplayBarSpacing, 0.0, 0.0);
+    }
+    
+    needsUpdate_ = false;
+	
+	glFlush();
+}
+
+// Copy new data into the display buffer
+void RawSensorDisplay::setDisplayData(std::vector<int> const& values) {
+    displayValues_ = values;
+    
+    // Update display width according to number of data points
+    totalDisplayWidth_ = kDisplaySideMargin*2 + (kDisplayBarWidth + kDisplayBarSpacing) * displayValues_.size();
+    
+    needsUpdate_ = true;
+}
+
+// Mouse interaction methods
+
+void RawSensorDisplay::mouseDown(float x, float y) {
+	//Point mousePoint = {x, y};
+	//Point scaledPoint = screenToInternal(mousePoint);
+	
+	//needsUpdate_ = true;
+}
+
+void RawSensorDisplay::mouseDragged(float x, float y) {
+	//Point mousePoint = {x, y};
+	//Point scaledPoint = screenToInternal(mousePoint);
+    
+    //needsUpdate_ = true;
+}
+
+void RawSensorDisplay::mouseUp(float x, float y) {
+	//Point mousePoint = {x, y};
+	//Point scaledPoint = screenToInternal(mousePoint);
+	
+	//needsUpdate_ = true;
+}
+
+void RawSensorDisplay::rightMouseDown(float x, float y) {
+	//Point mousePoint = {x, y};
+	//Point scaledPoint = screenToInternal(mousePoint);
+	
+	//needsUpdate_ = true;
+}
+
+void RawSensorDisplay::rightMouseDragged(float x, float y) {
+	//Point mousePoint = {x, y};
+	//Point scaledPoint = screenToInternal(mousePoint);
+}
+
+void RawSensorDisplay::rightMouseUp(float x, float y) {
+	//Point mousePoint = {x, y};
+	//Point scaledPoint = screenToInternal(mousePoint);
+}
+
+float RawSensorDisplay::graphToDisplayY(float y) {
+    return kDisplayBarHeight*(y - yMin_)/(yMax_ - yMin_);
+}
+
+void RawSensorDisplay::refreshViewport() {
+	glViewport(0, 0, displayPixelWidth_, displayPixelHeight_);
+}
+
+// Conversion from internal coordinate space to external pixel values and back
+
+// Pixel values go from 0,0 (lower left) to displayPixelWidth_, displayPixelHeight_ (upper right)
+// Internal values go from -totalDisplayWidth_/2, -totalDisplayHeight_/2 (lower left)
+//   to totalDisplayWidth_/2, totalDisplayHeight_/2 (upper right)
+
+// Pixel value in --> OpenGL value out
+RawSensorDisplay::Point RawSensorDisplay::screenToInternal(Point& inPoint) {
+	Point out;
+	
+	out.x = -totalDisplayWidth_*0.5 + (inPoint.x/displayPixelWidth_) * totalDisplayWidth_;
+	out.y = -totalDisplayHeight_*0.5 + (inPoint.y/displayPixelHeight_) * totalDisplayHeight_;
+	
+	return out;
+}
+
+// OpenGL value in --> Pixel value out
+RawSensorDisplay::Point RawSensorDisplay::internalToScreen(Point& inPoint) {
+	Point out;
+	
+	out.x = ((inPoint.x + totalDisplayWidth_*0.5)/totalDisplayWidth_) * displayPixelWidth_;
+	out.y = ((inPoint.y + totalDisplayHeight_*0.5)/totalDisplayHeight_) * displayPixelHeight_;
+    
+	return out;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/Display/RawSensorDisplay.h	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,100 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+
+  RawSensorDisplay.h: simple graph for showing raw TouchKeys sensor values
+*/
+
+#ifndef __touchkeys__RawSensorDisplay__
+#define __touchkeys__RawSensorDisplay__
+
+#include <iostream>
+#include <vector>
+//#include <OpenGL/gl.h>
+#include "../JuceLibraryCode/JuceHeader.h"
+#include "../TouchKeys/PianoTypes.h"
+#include "../Utility/Node.h"
+#include "OpenGLDisplayBase.h"
+
+
+// This class uses OpenGL to draw a bar graph of key sensor data,
+// for the purpose of debugging and validating individual keys
+
+class RawSensorDisplay : public OpenGLDisplayBase {
+	// Internal data structures and constants
+private:
+    // Display margins
+    static const float kDisplaySideMargin;
+    static const float kDisplayBottomMargin;
+    static const float kDisplayTopMargin;
+    
+    // Size of bar graphs and spacing
+    static const float kDisplayBarWidth;
+    static const float kDisplayBarSpacing;
+    static const float kDisplayBarHeight;
+    
+	typedef struct {
+		float x;
+		float y;
+	} Point;
+	
+public:
+	RawSensorDisplay();
+	
+	// Setup methods for display size and keyboard range
+	void setDisplaySize(float width, float height);
+    float aspectRatio() { return totalDisplayWidth_ / totalDisplayHeight_; }
+	
+	// Drawing methods
+	bool needsRender() { return needsUpdate_; }
+	void render();
+	
+	// Interaction methods
+	void mouseDown(float x, float y);
+	void mouseDragged(float x, float y);
+	void mouseUp(float x, float y);
+	void rightMouseDown(float x, float y);
+	void rightMouseDragged(float x, float y);
+	void rightMouseUp(float x, float y);
+    
+    // Data update methods
+    void setDisplayData(std::vector<int> const& values);
+    
+private:
+    // Convert mathematical XY coordinate space to drawing positions
+    float graphToDisplayX(float x);
+    float graphToDisplayY(float y);
+    
+	void refreshViewport();
+	
+	// Conversion from internal coordinate space to external pixel values and back
+	Point screenToInternal(Point& inPoint);
+	Point internalToScreen(Point& inPoint);
+	
+    
+private:
+	float displayPixelWidth_, displayPixelHeight_;	// Pixel resolution of the surrounding window
+	float totalDisplayWidth_, totalDisplayHeight_;	// Size of the internal view (centered around origin)
+    float yMin_, yMax_;                             // Range of the graph axes
+    
+	bool needsUpdate_;								// Whether the keyboard should be redrawn
+	CriticalSection displayMutex_;					// Synchronize access between data and display threads
+    std::vector<int> displayValues_;                // Values to display as a bar graph
+};
+
+#endif /* defined(__touchkeys__RawSensorDisplay__) */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/GUI/ControlWindowMainComponent.cpp	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,851 @@
+/*
+  ==============================================================================
+
+  This is an automatically generated GUI class created by the Introjucer!
+
+  Be careful when adding custom code to these files, as only the code within
+  the "//[xyz]" and "//[/xyz]" sections will be retained when the file is loaded
+  and re-saved.
+
+  Created with Introjucer version: 3.1.0
+
+  ------------------------------------------------------------------------------
+
+  The Introjucer is part of the JUCE library - "Jules' Utility Class Extensions"
+  Copyright 2004-13 by Raw Material Software Ltd.
+
+  ==============================================================================
+*/
+
+//[Headers] You can add your own extra header files here...
+#include "KeyboardZoneComponent.h"
+//[/Headers]
+
+#include "ControlWindowMainComponent.h"
+
+
+//[MiscUserDefs] You can add your own user definitions and misc code here...
+//[/MiscUserDefs]
+
+//==============================================================================
+ControlWindowMainComponent::ControlWindowMainComponent ()
+    : controller_(0)
+{
+    addAndMakeVisible (dataLoggingGroupComponent = new GroupComponent ("MIDI input group",
+                                                                       "Data Logging"));
+
+    addAndMakeVisible (midiInputGroupComponent = new GroupComponent ("MIDI input group",
+                                                                     "MIDI Input"));
+
+    addAndMakeVisible (midiInputDeviceComboBox = new ComboBox ("MIDI input combo box"));
+    midiInputDeviceComboBox->setEditableText (false);
+    midiInputDeviceComboBox->setJustificationType (Justification::centredLeft);
+    midiInputDeviceComboBox->setTextWhenNothingSelected (String::empty);
+    midiInputDeviceComboBox->setTextWhenNoChoicesAvailable ("(no choices)");
+    midiInputDeviceComboBox->addListener (this);
+
+    addAndMakeVisible (label = new Label ("new label",
+                                          "Device:"));
+    label->setFont (Font (15.00f, Font::plain));
+    label->setJustificationType (Justification::centredLeft);
+    label->setEditable (false, false, false);
+    label->setColour (TextEditor::textColourId, Colours::black);
+    label->setColour (TextEditor::backgroundColourId, Colour (0x00000000));
+
+    addAndMakeVisible (groupComponent = new GroupComponent ("new group",
+                                                            "TouchKeys"));
+
+    addAndMakeVisible (label2 = new Label ("new label",
+                                           "Device:\n"));
+    label2->setFont (Font (15.00f, Font::plain));
+    label2->setJustificationType (Justification::centredLeft);
+    label2->setEditable (false, false, false);
+    label2->setColour (TextEditor::textColourId, Colours::black);
+    label2->setColour (TextEditor::backgroundColourId, Colour (0x00000000));
+
+    addAndMakeVisible (touchkeyDeviceComboBox = new ComboBox ("TouchKeys combo box"));
+    touchkeyDeviceComboBox->setEditableText (false);
+    touchkeyDeviceComboBox->setJustificationType (Justification::centredLeft);
+    touchkeyDeviceComboBox->setTextWhenNothingSelected (String::empty);
+    touchkeyDeviceComboBox->setTextWhenNoChoicesAvailable ("(no choices)");
+    touchkeyDeviceComboBox->addListener (this);
+
+    addAndMakeVisible (label3 = new Label ("new label",
+                                           "Status:\n"));
+    label3->setFont (Font (15.00f, Font::plain));
+    label3->setJustificationType (Justification::centredLeft);
+    label3->setEditable (false, false, false);
+    label3->setColour (TextEditor::textColourId, Colours::black);
+    label3->setColour (TextEditor::backgroundColourId, Colour (0x00000000));
+
+    addAndMakeVisible (touchkeyStartButton = new TextButton ("TouchKeys start button"));
+    touchkeyStartButton->setButtonText ("Start");
+    touchkeyStartButton->addListener (this);
+
+    addAndMakeVisible (touchkeyStatusLabel = new Label ("TouchKeys status label",
+                                                        "not running"));
+    touchkeyStatusLabel->setFont (Font (15.00f, Font::plain));
+    touchkeyStatusLabel->setJustificationType (Justification::centredLeft);
+    touchkeyStatusLabel->setEditable (false, false, false);
+    touchkeyStatusLabel->setColour (TextEditor::textColourId, Colours::black);
+    touchkeyStatusLabel->setColour (TextEditor::backgroundColourId, Colour (0x00000000));
+
+    addAndMakeVisible (oscGroupComponent = new GroupComponent ("OSC group",
+                                                               "OSC Output"));
+
+    addAndMakeVisible (label7 = new Label ("new label",
+                                           "Host:"));
+    label7->setFont (Font (15.00f, Font::plain));
+    label7->setJustificationType (Justification::centredLeft);
+    label7->setEditable (false, false, false);
+    label7->setColour (TextEditor::textColourId, Colours::black);
+    label7->setColour (TextEditor::backgroundColourId, Colour (0x00000000));
+
+    addAndMakeVisible (oscHostTextEditor = new TextEditor ("new text editor"));
+    oscHostTextEditor->setMultiLine (false);
+    oscHostTextEditor->setReturnKeyStartsNewLine (false);
+    oscHostTextEditor->setReadOnly (false);
+    oscHostTextEditor->setScrollbarsShown (true);
+    oscHostTextEditor->setCaretVisible (true);
+    oscHostTextEditor->setPopupMenuEnabled (true);
+    oscHostTextEditor->setText ("127.0.0.1");
+
+    addAndMakeVisible (label8 = new Label ("new label",
+                                           "Port:"));
+    label8->setFont (Font (15.00f, Font::plain));
+    label8->setJustificationType (Justification::centredLeft);
+    label8->setEditable (false, false, false);
+    label8->setColour (TextEditor::textColourId, Colours::black);
+    label8->setColour (TextEditor::backgroundColourId, Colour (0x00000000));
+
+    addAndMakeVisible (oscPortTextEditor = new TextEditor ("new text editor"));
+    oscPortTextEditor->setMultiLine (false);
+    oscPortTextEditor->setReturnKeyStartsNewLine (false);
+    oscPortTextEditor->setReadOnly (false);
+    oscPortTextEditor->setScrollbarsShown (true);
+    oscPortTextEditor->setCaretVisible (true);
+    oscPortTextEditor->setPopupMenuEnabled (true);
+    oscPortTextEditor->setText ("8000");
+
+    addAndMakeVisible (oscEnableButton = new ToggleButton ("OSC enable button"));
+    oscEnableButton->setButtonText ("Enable OSC output");
+    oscEnableButton->addListener (this);
+
+    addAndMakeVisible (oscEnableRawButton = new ToggleButton ("OSC enable raw button"));
+    oscEnableRawButton->setButtonText ("Send raw frames");
+    oscEnableRawButton->addListener (this);
+
+    addAndMakeVisible (label4 = new Label ("new label",
+                                           "Lowest Octave:"));
+    label4->setFont (Font (15.00f, Font::plain));
+    label4->setJustificationType (Justification::centredLeft);
+    label4->setEditable (false, false, false);
+    label4->setColour (TextEditor::textColourId, Colours::black);
+    label4->setColour (TextEditor::backgroundColourId, Colour (0x00000000));
+
+    addAndMakeVisible (touchkeyOctaveComboBox = new ComboBox ("TouchKeys octave box"));
+    touchkeyOctaveComboBox->setEditableText (false);
+    touchkeyOctaveComboBox->setJustificationType (Justification::centredLeft);
+    touchkeyOctaveComboBox->setTextWhenNothingSelected (String::empty);
+    touchkeyOctaveComboBox->setTextWhenNoChoicesAvailable ("(no choices)");
+    touchkeyOctaveComboBox->addListener (this);
+
+    addAndMakeVisible (loggingButton = new TextButton ("logging button"));
+    loggingButton->setButtonText ("Start Logging");
+    loggingButton->addListener (this);
+
+    addAndMakeVisible (oscInputGroupComponent = new GroupComponent ("MIDI input group",
+                                                                    "OSC Input"));
+
+    addAndMakeVisible (oscInputEnableButton = new ToggleButton ("OSC input enable button"));
+    oscInputEnableButton->setButtonText ("Enable OSC input");
+    oscInputEnableButton->addListener (this);
+
+    addAndMakeVisible (label6 = new Label ("new label",
+                                           "Port:"));
+    label6->setFont (Font (15.00f, Font::plain));
+    label6->setJustificationType (Justification::centredLeft);
+    label6->setEditable (false, false, false);
+    label6->setColour (TextEditor::textColourId, Colours::black);
+    label6->setColour (TextEditor::backgroundColourId, Colour (0x00000000));
+
+    addAndMakeVisible (oscInputPortTextEditor = new TextEditor ("new text editor"));
+    oscInputPortTextEditor->setMultiLine (false);
+    oscInputPortTextEditor->setReturnKeyStartsNewLine (false);
+    oscInputPortTextEditor->setReadOnly (false);
+    oscInputPortTextEditor->setScrollbarsShown (true);
+    oscInputPortTextEditor->setCaretVisible (true);
+    oscInputPortTextEditor->setPopupMenuEnabled (true);
+    oscInputPortTextEditor->setText ("8001");
+
+    addAndMakeVisible (playLogButton = new TextButton ("play log button"));
+    playLogButton->setButtonText ("Play Log...");
+    playLogButton->addListener (this);
+
+    addAndMakeVisible (keyboardZoneTabbedComponent = new TabbedComponent (TabbedButtonBar::TabsAtTop));
+    keyboardZoneTabbedComponent->setTabBarDepth (30);
+    keyboardZoneTabbedComponent->setCurrentTabIndex (-1);
+
+    addAndMakeVisible (addZoneButton = new TextButton ("add zone button"));
+    addZoneButton->setButtonText ("Add");
+    addZoneButton->addListener (this);
+
+    addAndMakeVisible (removeZoneButton = new TextButton ("remove zone button"));
+    removeZoneButton->setButtonText ("Del");
+    removeZoneButton->addListener (this);
+
+    addAndMakeVisible (touchkeyAutodetectButton = new TextButton ("TouchKeys autodetect button"));
+    touchkeyAutodetectButton->setButtonText ("Detect");
+    touchkeyAutodetectButton->addListener (this);
+
+
+    //[UserPreSize]
+    lastSelectedMidiInputID_ = -1;
+    lastSegmentUniqueIdentifier_ = -1;
+
+    // Add octave labels to combo box
+    for(int i = 0; i <= kTouchkeysMaxOctave; i++) {
+        touchkeyOctaveComboBox->addItem("C" + String(i), i + kTouchkeysComponentComboBoxOffset);
+    }
+    //[/UserPreSize]
+
+    setSize (872, 444);
+
+
+    //[Constructor] You can add your own custom stuff here..
+    oscHostTextEditor->addListener(this);
+    oscPortTextEditor->addListener(this);
+    oscInputPortTextEditor->addListener(this);
+    //[/Constructor]
+}
+
+ControlWindowMainComponent::~ControlWindowMainComponent()
+{
+    //[Destructor_pre]. You can add your own custom destruction code here..
+    //[/Destructor_pre]
+
+    dataLoggingGroupComponent = nullptr;
+    midiInputGroupComponent = nullptr;
+    midiInputDeviceComboBox = nullptr;
+    label = nullptr;
+    groupComponent = nullptr;
+    label2 = nullptr;
+    touchkeyDeviceComboBox = nullptr;
+    label3 = nullptr;
+    touchkeyStartButton = nullptr;
+    touchkeyStatusLabel = nullptr;
+    oscGroupComponent = nullptr;
+    label7 = nullptr;
+    oscHostTextEditor = nullptr;
+    label8 = nullptr;
+    oscPortTextEditor = nullptr;
+    oscEnableButton = nullptr;
+    oscEnableRawButton = nullptr;
+    label4 = nullptr;
+    touchkeyOctaveComboBox = nullptr;
+    loggingButton = nullptr;
+    oscInputGroupComponent = nullptr;
+    oscInputEnableButton = nullptr;
+    label6 = nullptr;
+    oscInputPortTextEditor = nullptr;
+    playLogButton = nullptr;
+    keyboardZoneTabbedComponent = nullptr;
+    addZoneButton = nullptr;
+    removeZoneButton = nullptr;
+    touchkeyAutodetectButton = nullptr;
+
+
+    //[Destructor]. You can add your own custom destruction code here..
+    //[/Destructor]
+}
+
+//==============================================================================
+void ControlWindowMainComponent::paint (Graphics& g)
+{
+    //[UserPrePaint] Add your own custom painting code here..
+    //[/UserPrePaint]
+
+    g.fillAll (Colour (0xffd2d2d2));
+
+    //[UserPaint] Add your own custom painting code here..
+    //[/UserPaint]
+}
+
+void ControlWindowMainComponent::resized()
+{
+    dataLoggingGroupComponent->setBounds (8, 392, 304, 64);
+    midiInputGroupComponent->setBounds (8, 144, 304, 64);
+    midiInputDeviceComboBox->setBounds (72, 168, 224, 24);
+    label->setBounds (16, 168, 55, 24);
+    groupComponent->setBounds (8, 8, 304, 128);
+    label2->setBounds (16, 32, 55, 24);
+    touchkeyDeviceComboBox->setBounds (72, 32, 224, 24);
+    label3->setBounds (16, 96, 55, 24);
+    touchkeyStartButton->setBounds (216, 96, 79, 24);
+    touchkeyStatusLabel->setBounds (72, 96, 136, 24);
+    oscGroupComponent->setBounds (8, 288, 304, 96);
+    label7->setBounds (16, 344, 55, 24);
+    oscHostTextEditor->setBounds (64, 344, 128, 24);
+    label8->setBounds (200, 344, 40, 24);
+    oscPortTextEditor->setBounds (240, 344, 56, 24);
+    oscEnableButton->setBounds (24, 312, 144, 24);
+    oscEnableRawButton->setBounds (176, 312, 144, 24);
+    label4->setBounds (16, 64, 104, 24);
+    touchkeyOctaveComboBox->setBounds (120, 64, 88, 24);
+    loggingButton->setBounds (24, 416, 128, 24);
+    oscInputGroupComponent->setBounds (8, 216, 304, 64);
+    oscInputEnableButton->setBounds (24, 240, 152, 24);
+    label6->setBounds (200, 240, 40, 24);
+    oscInputPortTextEditor->setBounds (240, 240, 56, 24);
+    playLogButton->setBounds (168, 416, 128, 24);
+    keyboardZoneTabbedComponent->setBounds (320, 0, 552, 464);
+    addZoneButton->setBounds (776, 4, 38, 20);
+    removeZoneButton->setBounds (824, 4, 38, 20);
+    touchkeyAutodetectButton->setBounds (216, 64, 79, 24);
+    //[UserResized] Add your own custom resize handling here..
+    //[/UserResized]
+}
+
+void ControlWindowMainComponent::comboBoxChanged (ComboBox* comboBoxThatHasChanged)
+{
+    //[UsercomboBoxChanged_Pre]
+    if(controller_ == 0)
+        return;
+    //[/UsercomboBoxChanged_Pre]
+
+    if (comboBoxThatHasChanged == midiInputDeviceComboBox)
+    {
+        //[UserComboBoxCode_midiInputDeviceComboBox] -- add your combo box handling code here..
+
+        // Look up the selected ID, remembering that Juce indices start at 1 and the first of
+        // these is "Disabled"
+        int selection = midiInputDeviceComboBox->getSelectedId() - kMidiInputDeviceComboBoxOffset;
+        if(selection == 1 - kMidiInputDeviceComboBoxOffset) {   // Disabled
+            if(controller_->midiTouchkeysStandaloneModeIsEnabled())
+                controller_->midiTouchkeysStandaloneModeDisable();
+            controller_->disableAllMIDIInputPorts();
+        }
+        else if(selection == 2 - kMidiInputDeviceComboBoxOffset) {  // Standalone mode
+            controller_->disableAllMIDIInputPorts();
+            controller_->midiTouchkeysStandaloneModeEnable();
+        }
+        else if(selection >= 0 && selection < midiInputDeviceIDs_.size()) {
+            int deviceId = midiInputDeviceIDs_[selection];
+            if(controller_->midiTouchkeysStandaloneModeIsEnabled())
+                controller_->midiTouchkeysStandaloneModeDisable();
+            controller_->disableAllMIDIInputPorts();
+            controller_->enableMIDIInputPort(deviceId);
+        }
+        //[/UserComboBoxCode_midiInputDeviceComboBox]
+    }
+    else if (comboBoxThatHasChanged == touchkeyDeviceComboBox)
+    {
+        //[UserComboBoxCode_touchkeyDeviceComboBox] -- add your combo box handling code here..
+        // Nothing to do here right away -- wait until start button is pressed
+        //[/UserComboBoxCode_touchkeyDeviceComboBox]
+    }
+    else if (comboBoxThatHasChanged == touchkeyOctaveComboBox)
+    {
+        //[UserComboBoxCode_touchkeyOctaveComboBox] -- add your combo box handling code here..
+        int octave = touchkeyOctaveComboBox->getSelectedId() - kTouchkeysComponentComboBoxOffset;
+
+        // Convert octave number to MIDI note (C4 = 60)
+        if(controller_ != 0)
+            controller_->touchkeyDeviceSetLowestMidiNote((octave + 1)*12);
+        //[/UserComboBoxCode_touchkeyOctaveComboBox]
+    }
+
+    //[UsercomboBoxChanged_Post]
+    //[/UsercomboBoxChanged_Post]
+}
+
+void ControlWindowMainComponent::buttonClicked (Button* buttonThatWasClicked)
+{
+    //[UserbuttonClicked_Pre]
+    if(controller_ == 0)
+        return;
+    //[/UserbuttonClicked_Pre]
+
+    if (buttonThatWasClicked == touchkeyStartButton)
+    {
+        //[UserButtonCode_touchkeyStartButton] -- add your button handler code here..
+        if(controller_->touchkeyDeviceIsRunning()) {
+            // TouchKeys were running. Stop and close.
+            controller_->closeTouchkeyDevice();
+        }
+        else {
+            // TouchKeys weren't running. Open and start.
+            String devName = controller_->touchkeyDevicePrefix().c_str();
+            devName += touchkeyDeviceComboBox->getText();
+
+            // This will attempt to start the device and update the state accordingly
+            controller_->touchkeyDeviceStartupSequence(devName.toUTF8());
+        }
+        //[/UserButtonCode_touchkeyStartButton]
+    }
+    else if (buttonThatWasClicked == oscEnableButton)
+    {
+        //[UserButtonCode_oscEnableButton] -- add your button handler code here..
+        controller_->oscTransmitSetEnabled(oscEnableButton->getToggleState());
+        //[/UserButtonCode_oscEnableButton]
+    }
+    else if (buttonThatWasClicked == oscEnableRawButton)
+    {
+        //[UserButtonCode_oscEnableRawButton] -- add your button handler code here..
+        controller_->oscTransmitSetRawDataEnabled(oscEnableRawButton->getToggleState());
+        //[/UserButtonCode_oscEnableRawButton]
+    }
+    else if (buttonThatWasClicked == loggingButton)
+    {
+        //[UserButtonCode_loggingButton] -- add your button handler code here..
+        if(controller_->isLogging())
+            controller_->stopLogging();
+        else
+            controller_->startLogging();
+        //[/UserButtonCode_loggingButton]
+    }
+    else if (buttonThatWasClicked == oscInputEnableButton)
+    {
+        //[UserButtonCode_oscInputEnableButton] -- add your button handler code here..
+        //[/UserButtonCode_oscInputEnableButton]
+    }
+    else if (buttonThatWasClicked == playLogButton)
+    {
+        //[UserButtonCode_playLogButton] -- add your button handler code here..
+        //[/UserButtonCode_playLogButton]
+    }
+    else if (buttonThatWasClicked == addZoneButton)
+    {
+        //[UserButtonCode_addZoneButton] -- add your button handler code here..
+        controller_->midiSegmentAdd();
+        //[/UserButtonCode_addZoneButton]
+    }
+    else if (buttonThatWasClicked == removeZoneButton)
+    {
+        //[UserButtonCode_removeZoneButton] -- add your button handler code here..
+        int tabIndex = keyboardZoneTabbedComponent->getCurrentTabIndex();
+        if(tabIndex != 0) {
+            KeyboardZoneComponent* selectedComponent = static_cast<KeyboardZoneComponent*> (keyboardZoneTabbedComponent->getTabContentComponent(tabIndex));
+            controller_->midiSegmentRemove(selectedComponent->keyboardSegment());
+        }
+        //[/UserButtonCode_removeZoneButton]
+    }
+    else if (buttonThatWasClicked == touchkeyAutodetectButton)
+    {
+        //[UserButtonCode_touchkeyAutodetectButton] -- add your button handler code here..
+        if(controller_->touchkeyDeviceIsAutodetecting())
+            controller_->touchkeyDeviceStopAutodetecting();
+        else
+            controller_->touchkeyDeviceAutodetectLowestMidiNote();
+        //[/UserButtonCode_touchkeyAutodetectButton]
+    }
+
+    //[UserbuttonClicked_Post]
+    //[/UserbuttonClicked_Post]
+}
+
+
+
+//[MiscUserCode] You can add your own definitions of your custom methods or any other code here...
+
+void ControlWindowMainComponent::textEditorReturnKeyPressed(TextEditor &editor)
+{
+    if(controller_ == 0)
+        return;
+    if(&editor == oscHostTextEditor || &editor == oscPortTextEditor)
+        updateOscHostPort();
+}
+
+void ControlWindowMainComponent::textEditorEscapeKeyPressed(TextEditor &editor)
+{
+    // Nothing to do here
+}
+
+void ControlWindowMainComponent::textEditorFocusLost(TextEditor &editor)
+{
+    textEditorReturnKeyPressed(editor);
+}
+
+// Update list of TouchKeys and MIDI input devices
+void ControlWindowMainComponent::updateInputDeviceList()
+{
+    if(controller_ == 0)
+        return;
+
+    // *** TouchKeys devices ***
+    vector<string> tkdevices = controller_->availableTouchkeyDevices();
+    vector<string>::iterator tkit;
+    int counter;
+
+    touchkeyDeviceComboBox->clear();
+
+    if(tkdevices.size() == 0) {
+        touchkeyDeviceComboBox->addItem("No devices", 1);
+        touchkeyDeviceComboBox->setSelectedId(1, dontSendNotification);
+        touchkeyDeviceComboBox->setEnabled(false);
+        touchkeyStartButton->setEnabled(false);
+    }
+    else {
+        counter = 1;
+        for(tkit = tkdevices.begin(); tkit != tkdevices.end(); ++tkit) {
+            touchkeyDeviceComboBox->addItem(tkit->c_str(), counter++);
+        }
+        touchkeyDeviceComboBox->setSelectedId(1, dontSendNotification);
+        touchkeyDeviceComboBox->setEnabled(true);
+        touchkeyStartButton->setEnabled(true);
+    }
+
+    // *** MIDI input devices ***
+    vector<pair<int, string> > devices = controller_->availableMIDIInputDevices();
+    vector<pair<int, string> >::iterator it;
+
+    midiInputDeviceComboBox->clear();
+    midiInputDeviceIDs_.clear();
+    midiInputDeviceComboBox->addItem("Disabled", 1);
+    midiInputDeviceComboBox->addItem("TouchKeys Standalone", 2);
+    counter = kMidiInputDeviceComboBoxOffset;
+    for(it = devices.begin(); it != devices.end(); ++it) {
+        midiInputDeviceComboBox->addItem((*it).second.c_str(), counter);
+        midiInputDeviceIDs_.push_back(it->first);
+        counter++;
+    }
+}
+
+void ControlWindowMainComponent::updateOscHostPort()
+{
+    if(controller_ == 0)
+        return;
+
+    String oscHost = oscHostTextEditor->getText();
+    String oscPort = oscPortTextEditor->getText();
+    controller_->oscTransmitClearAddresses();
+    controller_->oscTransmitAddAddress(oscHost.toUTF8(), oscPort.toUTF8());
+}
+
+// Synchronize the UI state with the underlying state of the controller
+void ControlWindowMainComponent::synchronize() {
+    if(controller_ == 0)
+        return;
+
+    // Update TouchKeys status
+    if(controller_->touchkeyDeviceIsRunning()) {
+        touchkeyStartButton->setButtonText("Stop");
+        touchkeyStatusLabel->setText("Running", dontSendNotification);
+    }
+    else if(controller_->touchkeyDeviceErrorOccurred()) {
+        touchkeyStartButton->setButtonText("Start");
+        touchkeyStatusLabel->setText(controller_->touchkeyDeviceErrorMessage().c_str(), dontSendNotification);
+    }
+    else {
+        touchkeyStartButton->setButtonText("Start");
+        touchkeyStatusLabel->setText("Not running", dontSendNotification);
+    }
+
+    // Update MIDI input status
+    if(controller_->midiTouchkeysStandaloneModeIsEnabled()) {
+        midiInputDeviceComboBox->setSelectedId(2, dontSendNotification);
+    }
+    else {
+        const std::vector<int>& selectedMidiInputDevices(controller_->selectedMIDIInputPorts());
+        if(selectedMidiInputDevices.empty()) {
+            midiInputDeviceComboBox->setSelectedId(1, dontSendNotification);
+        }
+        else if(selectedMidiInputDevices.front() != lastSelectedMidiInputID_){
+            // Input has changed from before. Find it in vector
+            // If there is more than one selected ID, we will only take the first one for
+            // the current UI. This affects the display but not the functionality.
+            for(int i = 0; i < midiInputDeviceIDs_.size(); i++) {
+                if(midiInputDeviceIDs_[i] == selectedMidiInputDevices.front()) {
+                    midiInputDeviceComboBox->setSelectedId(i + kMidiInputDeviceComboBoxOffset, dontSendNotification);
+                    break;
+                }
+            }
+            // ...and cache this as the last ID so we don't search again next time
+            lastSelectedMidiInputID_ = selectedMidiInputDevices.front();
+        }
+    }
+
+    // Update OSC status
+    oscEnableButton->setToggleState(controller_->oscTransmitEnabled(), dontSendNotification);
+    oscEnableRawButton->setToggleState(controller_->oscTransmitRawDataEnabled(), dontSendNotification);
+
+    // Update the OSC fields only if the text editors aren't active
+    if(!oscHostTextEditor->hasKeyboardFocus(true) && !oscPortTextEditor->hasKeyboardFocus(true)) {
+        const std::vector<lo_address>& oscAddresses = controller_->oscTransmitAddresses();
+        if(oscAddresses.empty()) {
+            oscHostTextEditor->setText("", false);
+            oscPortTextEditor->setText("", false);
+        }
+        else {
+            // Take the first address to display in the text editor. As with MIDI input,
+            // this doesn't affect the functionality, only the UI display.
+            lo_address firstAddress = oscAddresses.front();
+
+            oscHostTextEditor->setText(lo_address_get_hostname(firstAddress), false);
+            oscPortTextEditor->setText(lo_address_get_port(firstAddress), false);
+        }
+    }
+
+    // Set the octave button
+    int octave = (controller_->touchkeyDeviceLowestMidiNote() / 12) - 1;
+    if(octave >= 0 && octave <= kTouchkeysMaxOctave)
+        touchkeyOctaveComboBox->setSelectedId(octave + kTouchkeysComponentComboBoxOffset, dontSendNotification);
+
+    // Enable or disable the autodetect button depending on the device status
+    if(!controller_->touchkeyDeviceIsRunning()) {
+        touchkeyAutodetectButton->setEnabled(false);
+    }
+    else if(controller_->touchkeyDeviceIsAutodetecting()) {
+        touchkeyAutodetectButton->setEnabled(true);
+        touchkeyAutodetectButton->setButtonText("Cancel");
+    }
+    else {
+        touchkeyAutodetectButton->setEnabled(true);
+        touchkeyAutodetectButton->setButtonText("Detect");
+    }
+
+    // Set the text on the logging button
+    if(controller_->isLogging()) {
+        loggingButton->setButtonText("Stop Logging");
+    }
+    else
+        loggingButton->setButtonText("Start Logging");
+
+    // Update segments list if it has changed
+    if(lastSegmentUniqueIdentifier_ != controller_->midiSegmentUniqueIdentifier())
+        updateKeyboardSegments();
+
+    // Synchronize every tab component
+    for(int tab = 0; tab < keyboardZoneTabbedComponent->getNumTabs(); tab++) {
+        KeyboardZoneComponent *component = static_cast<KeyboardZoneComponent*> (keyboardZoneTabbedComponent->getTabContentComponent(tab));
+        component->synchronize();
+    }
+
+    // Update add/remove buttons
+    if(keyboardZoneTabbedComponent->getCurrentTabIndex() <= 0) {
+        removeZoneButton->setEnabled(false);
+    }
+    else {
+        removeZoneButton->setEnabled(true);
+    }
+    if(controller_->midiSegmentsCount() >= 8)
+        addZoneButton->setEnabled(false);
+    else
+        addZoneButton->setEnabled(true);
+}
+
+// Update the state of the keyboard segment tab bar. Called only when segments change
+void ControlWindowMainComponent::updateKeyboardSegments()
+{
+    if(controller_ == 0)
+        return;
+    // Update the identifier to say we've matched the current state of the segments
+    lastSegmentUniqueIdentifier_ = controller_->midiSegmentUniqueIdentifier();
+
+    // Save the current selected index in case we later remove it
+    int currentlySelectedIndex = keyboardZoneTabbedComponent->getCurrentTabIndex();
+    KeyboardZoneComponent* currentlySelectedComponent = static_cast<KeyboardZoneComponent*> (keyboardZoneTabbedComponent->getTabContentComponent(currentlySelectedIndex));
+    MidiKeyboardSegment* currentlySelectedSegment = 0;
+    if(currentlySelectedComponent != 0)
+        currentlySelectedSegment = currentlySelectedComponent->keyboardSegment();
+    bool selectedNewTab = false;
+
+    // First, go through the segments and create tabs as needed
+    int maxNumSegments = controller_->midiSegmentsCount();
+    for(int i = 0; i < maxNumSegments; i++) {
+        MidiKeyboardSegment* segment = controller_->midiSegment(i);
+        bool matched = false;
+        if(segment == 0)
+            continue;
+        // Look for this segment among the tabs we already have
+        for(int tab = 0; tab < keyboardZoneTabbedComponent->getNumTabs(); tab++) {
+            KeyboardZoneComponent *component = static_cast<KeyboardZoneComponent*> (keyboardZoneTabbedComponent->getTabContentComponent(tab));
+            if(component->keyboardSegment() == segment) {
+                // Found it...
+                matched = true;
+                break;
+            }
+        }
+        // If we didn't find it, add a tab for this segment
+        if(!matched) {
+            KeyboardZoneComponent *newComponent = new KeyboardZoneComponent();
+            newComponent->setMainApplicationController(controller_);
+            newComponent->setKeyboardSegment(segment);
+
+            char name[16];
+            snprintf(name, 16, "Zone %d", segment->outputPort());
+
+            // Add the component, telling the tab manager to take charge of deleting it at the end
+            keyboardZoneTabbedComponent->addTab(name, Colours::lightgrey, newComponent, true);
+            keyboardZoneTabbedComponent->setCurrentTabIndex(keyboardZoneTabbedComponent->getNumTabs() - 1);
+            selectedNewTab = true;
+
+            //std::cout << "Adding tab for segment " << segment << endl;
+        }
+    }
+
+    // Now go through the other way and remove tabs that are no longer needed
+    // Iterate through each tab: find a match in the segments
+    int tab = 0;
+    while(tab < keyboardZoneTabbedComponent->getNumTabs()) {
+        KeyboardZoneComponent *component = static_cast<KeyboardZoneComponent*> (keyboardZoneTabbedComponent->getTabContentComponent(tab));
+        MidiKeyboardSegment *segment = component->keyboardSegment();
+        bool matched = false;
+
+        for(int i = 0; i < maxNumSegments; i++) {
+            if(segment == controller_->midiSegment(i)) {
+                matched = true;
+                break;
+            }
+        }
+        if(segment == 0 || !matched) {
+            //std::cout << "Removing tab for nonexistent segment " << segment << endl;
+
+            if(currentlySelectedSegment == segment) {
+                // The currently selected tab has been removed. Select the prior one.
+                if(currentlySelectedIndex > 0)
+                    keyboardZoneTabbedComponent->setCurrentTabIndex(currentlySelectedIndex - 1);
+                else
+                    keyboardZoneTabbedComponent->setCurrentTabIndex(0);
+            }
+
+            // This tab holds a nonexistent segment and should be removed
+            keyboardZoneTabbedComponent->removeTab(tab);
+
+            // And we have to start over again since the tab indexing has changed
+            tab = 0;
+        }
+        else    // Found a match: check the next tab
+            tab++;
+        // Eventually, we get to the end of the list of tabs an we know every existing tab matches a segment
+    }
+}
+
+//[/MiscUserCode]
+
+
+//==============================================================================
+#if 0
+/*  -- Introjucer information section --
+
+    This is where the Introjucer stores the metadata that describe this GUI layout, so
+    make changes in here at your peril!
+
+BEGIN_JUCER_METADATA
+
+<JUCER_COMPONENT documentType="Component" className="ControlWindowMainComponent"
+                 componentName="" parentClasses="public Component, public TextEditor::Listener"
+                 constructorParams="" variableInitialisers="controller_(0)" snapPixels="8"
+                 snapActive="1" snapShown="1" overlayOpacity="0.330" fixedSize="1"
+                 initialWidth="872" initialHeight="444">
+  <BACKGROUND backgroundColour="ffd2d2d2"/>
+  <GROUPCOMPONENT name="MIDI input group" id="87491da999138aa9" memberName="dataLoggingGroupComponent"
+                  virtualName="" explicitFocusOrder="0" pos="8 392 304 64" title="Data Logging"/>
+  <GROUPCOMPONENT name="MIDI input group" id="ce80a86ee6475cd9" memberName="midiInputGroupComponent"
+                  virtualName="" explicitFocusOrder="0" pos="8 144 304 64" title="MIDI Input"/>
+  <COMBOBOX name="MIDI input combo box" id="def32c74505cfa50" memberName="midiInputDeviceComboBox"
+            virtualName="" explicitFocusOrder="0" pos="72 168 224 24" editable="0"
+            layout="33" items="" textWhenNonSelected="" textWhenNoItems="(no choices)"/>
+  <LABEL name="new label" id="ad7bc4640d8023b7" memberName="label" virtualName=""
+         explicitFocusOrder="0" pos="16 168 55 24" edTextCol="ff000000"
+         edBkgCol="0" labelText="Device:" editableSingleClick="0" editableDoubleClick="0"
+         focusDiscardsChanges="0" fontname="Default font" fontsize="15"
+         bold="0" italic="0" justification="33"/>
+  <GROUPCOMPONENT name="new group" id="9106305fd2211185" memberName="groupComponent"
+                  virtualName="" explicitFocusOrder="0" pos="8 8 304 128" title="TouchKeys"/>
+  <LABEL name="new label" id="944877a84dcfc602" memberName="label2" virtualName=""
+         explicitFocusOrder="0" pos="16 32 55 24" edTextCol="ff000000"
+         edBkgCol="0" labelText="Device:&#10;" editableSingleClick="0"
+         editableDoubleClick="0" focusDiscardsChanges="0" fontname="Default font"
+         fontsize="15" bold="0" italic="0" justification="33"/>
+  <COMBOBOX name="TouchKeys combo box" id="871223bdcad0e693" memberName="touchkeyDeviceComboBox"
+            virtualName="" explicitFocusOrder="0" pos="72 32 224 24" editable="0"
+            layout="33" items="" textWhenNonSelected="" textWhenNoItems="(no choices)"/>
+  <LABEL name="new label" id="1cdf89082d95c72c" memberName="label3" virtualName=""
+         explicitFocusOrder="0" pos="16 96 55 24" edTextCol="ff000000"
+         edBkgCol="0" labelText="Status:&#10;" editableSingleClick="0"
+         editableDoubleClick="0" focusDiscardsChanges="0" fontname="Default font"
+         fontsize="15" bold="0" italic="0" justification="33"/>
+  <TEXTBUTTON name="TouchKeys start button" id="1bb1c69c957fc984" memberName="touchkeyStartButton"
+              virtualName="" explicitFocusOrder="0" pos="216 96 79 24" buttonText="Start"
+              connectedEdges="0" needsCallback="1" radioGroupId="0"/>
+  <LABEL name="TouchKeys status label" id="c91b132696e6ba1d" memberName="touchkeyStatusLabel"
+         virtualName="" explicitFocusOrder="0" pos="72 96 136 24" edTextCol="ff000000"
+         edBkgCol="0" labelText="not running" editableSingleClick="0"
+         editableDoubleClick="0" focusDiscardsChanges="0" fontname="Default font"
+         fontsize="15" bold="0" italic="0" justification="33"/>
+  <GROUPCOMPONENT name="OSC group" id="8268119e22809825" memberName="oscGroupComponent"
+                  virtualName="" explicitFocusOrder="0" pos="8 288 304 96" title="OSC Output"/>
+  <LABEL name="new label" id="896c0c48a1cf50a" memberName="label7" virtualName=""
+         explicitFocusOrder="0" pos="16 344 55 24" edTextCol="ff000000"
+         edBkgCol="0" labelText="Host:" editableSingleClick="0" editableDoubleClick="0"
+         focusDiscardsChanges="0" fontname="Default font" fontsize="15"
+         bold="0" italic="0" justification="33"/>
+  <TEXTEDITOR name="new text editor" id="84778d0bbebedd36" memberName="oscHostTextEditor"
+              virtualName="" explicitFocusOrder="0" pos="64 344 128 24" initialText="127.0.0.1"
+              multiline="0" retKeyStartsLine="0" readonly="0" scrollbars="1"
+              caret="1" popupmenu="1"/>
+  <LABEL name="new label" id="157c85bf83a7f936" memberName="label8" virtualName=""
+         explicitFocusOrder="0" pos="200 344 40 24" edTextCol="ff000000"
+         edBkgCol="0" labelText="Port:" editableSingleClick="0" editableDoubleClick="0"
+         focusDiscardsChanges="0" fontname="Default font" fontsize="15"
+         bold="0" italic="0" justification="33"/>
+  <TEXTEDITOR name="new text editor" id="7c21f0c238812d11" memberName="oscPortTextEditor"
+              virtualName="" explicitFocusOrder="0" pos="240 344 56 24" initialText="8000"
+              multiline="0" retKeyStartsLine="0" readonly="0" scrollbars="1"
+              caret="1" popupmenu="1"/>
+  <TOGGLEBUTTON name="OSC enable button" id="ccd52591cfd0b632" memberName="oscEnableButton"
+                virtualName="" explicitFocusOrder="0" pos="24 312 144 24" buttonText="Enable OSC output"
+                connectedEdges="0" needsCallback="1" radioGroupId="0" state="0"/>
+  <TOGGLEBUTTON name="OSC enable raw button" id="4aaf8f80edaff24" memberName="oscEnableRawButton"
+                virtualName="" explicitFocusOrder="0" pos="176 312 144 24" buttonText="Send raw frames"
+                connectedEdges="0" needsCallback="1" radioGroupId="0" state="0"/>
+  <LABEL name="new label" id="c5873c6498f8156d" memberName="label4" virtualName=""
+         explicitFocusOrder="0" pos="16 64 104 24" edTextCol="ff000000"
+         edBkgCol="0" labelText="Lowest Octave:" editableSingleClick="0"
+         editableDoubleClick="0" focusDiscardsChanges="0" fontname="Default font"
+         fontsize="15" bold="0" italic="0" justification="33"/>
+  <COMBOBOX name="TouchKeys octave box" id="36ace32027c81d30" memberName="touchkeyOctaveComboBox"
+            virtualName="" explicitFocusOrder="0" pos="120 64 88 24" editable="0"
+            layout="33" items="" textWhenNonSelected="" textWhenNoItems="(no choices)"/>
+  <TEXTBUTTON name="logging button" id="44705422dd1cb795" memberName="loggingButton"
+              virtualName="" explicitFocusOrder="0" pos="24 416 128 24" buttonText="Start Logging"
+              connectedEdges="0" needsCallback="1" radioGroupId="0"/>
+  <GROUPCOMPONENT name="MIDI input group" id="bb54712f78382055" memberName="oscInputGroupComponent"
+                  virtualName="" explicitFocusOrder="0" pos="8 216 304 64" title="OSC Input"/>
+  <TOGGLEBUTTON name="OSC input enable button" id="22a196770a440560" memberName="oscInputEnableButton"
+                virtualName="" explicitFocusOrder="0" pos="24 240 152 24" buttonText="Enable OSC input"
+                connectedEdges="0" needsCallback="1" radioGroupId="0" state="0"/>
+  <LABEL name="new label" id="c680c2da87cdcbf2" memberName="label6" virtualName=""
+         explicitFocusOrder="0" pos="200 240 40 24" edTextCol="ff000000"
+         edBkgCol="0" labelText="Port:" editableSingleClick="0" editableDoubleClick="0"
+         focusDiscardsChanges="0" fontname="Default font" fontsize="15"
+         bold="0" italic="0" justification="33"/>
+  <TEXTEDITOR name="new text editor" id="d4a91e8bff5b6bc9" memberName="oscInputPortTextEditor"
+              virtualName="" explicitFocusOrder="0" pos="240 240 56 24" initialText="8001"
+              multiline="0" retKeyStartsLine="0" readonly="0" scrollbars="1"
+              caret="1" popupmenu="1"/>
+  <TEXTBUTTON name="play log button" id="44858f01a66d263d" memberName="playLogButton"
+              virtualName="" explicitFocusOrder="0" pos="168 416 128 24" buttonText="Play Log..."
+              connectedEdges="0" needsCallback="1" radioGroupId="0"/>
+  <TABBEDCOMPONENT name="keyboard zone tabbed component" id="33da3d6583cdacbf" memberName="keyboardZoneTabbedComponent"
+                   virtualName="" explicitFocusOrder="0" pos="320 0 552 464" orientation="top"
+                   tabBarDepth="30" initialTab="-1"/>
+  <TEXTBUTTON name="add zone button" id="1d2fa7fd74f31315" memberName="addZoneButton"
+              virtualName="" explicitFocusOrder="0" pos="776 4 38 20" buttonText="Add"
+              connectedEdges="0" needsCallback="1" radioGroupId="0"/>
+  <TEXTBUTTON name="remove zone button" id="7865f7787a191e0e" memberName="removeZoneButton"
+              virtualName="" explicitFocusOrder="0" pos="824 4 38 20" buttonText="Del"
+              connectedEdges="0" needsCallback="1" radioGroupId="0"/>
+  <TEXTBUTTON name="TouchKeys autodetect button" id="6e19894bc11d0276" memberName="touchkeyAutodetectButton"
+              virtualName="" explicitFocusOrder="0" pos="216 64 79 24" buttonText="Detect"
+              connectedEdges="0" needsCallback="1" radioGroupId="0"/>
+</JUCER_COMPONENT>
+
+END_JUCER_METADATA
+*/
+#endif
+
+
+//[EndFile] You can add extra defines here...
+//[/EndFile]
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/GUI/ControlWindowMainComponent.h	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,130 @@
+/*
+  ==============================================================================
+
+  This is an automatically generated GUI class created by the Introjucer!
+
+  Be careful when adding custom code to these files, as only the code within
+  the "//[xyz]" and "//[/xyz]" sections will be retained when the file is loaded
+  and re-saved.
+
+  Created with Introjucer version: 3.1.0
+
+  ------------------------------------------------------------------------------
+
+  The Introjucer is part of the JUCE library - "Jules' Utility Class Extensions"
+  Copyright 2004-13 by Raw Material Software Ltd.
+
+  ==============================================================================
+*/
+
+#ifndef __JUCE_HEADER_FD693E32C8291DFA__
+#define __JUCE_HEADER_FD693E32C8291DFA__
+
+//[Headers]     -- You can add your own extra header files here --
+#include "JuceHeader.h"
+#include "../MainApplicationController.h"
+//[/Headers]
+
+
+
+//==============================================================================
+/**
+                                                                    //[Comments]
+    An auto-generated component, created by the Introjucer.
+
+    Describe your class and how it works here!
+                                                                    //[/Comments]
+*/
+class ControlWindowMainComponent  : public Component,
+                                    public TextEditor::Listener,
+                                    public ComboBoxListener,
+                                    public ButtonListener
+{
+public:
+    //==============================================================================
+    ControlWindowMainComponent ();
+    ~ControlWindowMainComponent();
+
+    //==============================================================================
+    //[UserMethods]     -- You can add your own custom methods in this section.
+    void setMainApplicationController(MainApplicationController *controller) {
+        // Attach the user interface to the controller and vice-versa
+        controller_ = controller;
+        updateInputDeviceList();
+    }
+
+    // TextEditor listener methods
+    void textEditorTextChanged(TextEditor &editor) {}
+    void textEditorReturnKeyPressed(TextEditor &editor);
+    void textEditorEscapeKeyPressed(TextEditor &editor);
+    void textEditorFocusLost(TextEditor &editor);
+
+    void synchronize();
+    //[/UserMethods]
+
+    void paint (Graphics& g);
+    void resized();
+    void comboBoxChanged (ComboBox* comboBoxThatHasChanged);
+    void buttonClicked (Button* buttonThatWasClicked);
+
+
+
+private:
+    //[UserVariables]   -- You can add your own custom variables in this section.
+    enum {
+        // Offsets between Juce UI IDs and positions in vector
+        kMidiInputDeviceComboBoxOffset = 3,
+        kTouchkeysComponentComboBoxOffset = 1,
+        kTouchkeysMaxOctave = 6
+    };
+
+    void updateInputDeviceList();
+    void updateOscHostPort();
+    void updateKeyboardSegments();
+
+    MainApplicationController *controller_; // Pointer to the main application controller
+    std::vector<int> midiInputDeviceIDs_;
+    int lastSelectedMidiInputID_;
+    int lastSegmentUniqueIdentifier_;
+    //[/UserVariables]
+
+    //==============================================================================
+    ScopedPointer<GroupComponent> dataLoggingGroupComponent;
+    ScopedPointer<GroupComponent> midiInputGroupComponent;
+    ScopedPointer<ComboBox> midiInputDeviceComboBox;
+    ScopedPointer<Label> label;
+    ScopedPointer<GroupComponent> groupComponent;
+    ScopedPointer<Label> label2;
+    ScopedPointer<ComboBox> touchkeyDeviceComboBox;
+    ScopedPointer<Label> label3;
+    ScopedPointer<TextButton> touchkeyStartButton;
+    ScopedPointer<Label> touchkeyStatusLabel;
+    ScopedPointer<GroupComponent> oscGroupComponent;
+    ScopedPointer<Label> label7;
+    ScopedPointer<TextEditor> oscHostTextEditor;
+    ScopedPointer<Label> label8;
+    ScopedPointer<TextEditor> oscPortTextEditor;
+    ScopedPointer<ToggleButton> oscEnableButton;
+    ScopedPointer<ToggleButton> oscEnableRawButton;
+    ScopedPointer<Label> label4;
+    ScopedPointer<ComboBox> touchkeyOctaveComboBox;
+    ScopedPointer<TextButton> loggingButton;
+    ScopedPointer<GroupComponent> oscInputGroupComponent;
+    ScopedPointer<ToggleButton> oscInputEnableButton;
+    ScopedPointer<Label> label6;
+    ScopedPointer<TextEditor> oscInputPortTextEditor;
+    ScopedPointer<TextButton> playLogButton;
+    ScopedPointer<TabbedComponent> keyboardZoneTabbedComponent;
+    ScopedPointer<TextButton> addZoneButton;
+    ScopedPointer<TextButton> removeZoneButton;
+    ScopedPointer<TextButton> touchkeyAutodetectButton;
+
+
+    //==============================================================================
+    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ControlWindowMainComponent)
+};
+
+//[EndFile] You can add extra defines here...
+//[/EndFile]
+
+#endif   // __JUCE_HEADER_FD693E32C8291DFA__
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/GUI/GraphicsDisplayWindow.h	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,76 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+
+  GraphicsDisplayWindow.h: window containing an OpenGL graphics display,
+  for example the KeyboardDisplay visualization.
+*/
+
+#ifndef __GRAPHICSDISPLAYWINDOW_H_AD5467BB__
+#define __GRAPHICSDISPLAYWINDOW_H_AD5467BB__
+
+#ifndef TOUCHKEYS_NO_GUI
+
+#include "../JuceLibraryCode/JuceHeader.h"
+#include "../MainApplicationController.h"
+#include "../Display/OpenGLJuceCanvas.h"
+
+//==============================================================================
+/*
+*/
+class GraphicsDisplayWindow    : public DocumentWindow
+{
+public:
+    GraphicsDisplayWindow(String name, MainApplicationController& controller)
+    : DocumentWindow(name, Colours::lightgrey, DocumentWindow::allButtons),
+      controller_(controller)
+    {
+        // Initialize an OpenGL graphics object as the content with a default size
+        OpenGLJuceCanvas *canvas = new OpenGLJuceCanvas(controller_.keyboardDisplay());
+        canvas->setSize(300,200);
+        
+        // Set properties
+        setContentOwned(canvas, true);
+        setUsingNativeTitleBar(true);
+        setResizable(true, true);
+        getConstrainer()->setFixedAspectRatio(controller_.keyboardDisplay().keyboardAspectRatio());
+        setBoundsConstrained(getBounds());
+        
+        // Show window
+        setVisible(true);
+    }
+
+    ~GraphicsDisplayWindow()
+    {
+    }
+    
+    void closeButtonPressed()
+    {
+        setVisible(false);
+    }
+
+
+private:
+    MainApplicationController& controller_;
+    
+    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (GraphicsDisplayWindow)
+};
+
+#endif  // TOUCHKEYS_NO_GUI
+
+#endif  // __GRAPHICSDISPLAYWINDOW_H_AD5467BB__
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/GUI/KeyboardZoneComponent.cpp	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,712 @@
+/*
+  ==============================================================================
+
+  This is an automatically generated GUI class created by the Introjucer!
+
+  Be careful when adding custom code to these files, as only the code within
+  the "//[xyz]" and "//[/xyz]" sections will be retained when the file is loaded
+  and re-saved.
+
+  Created with Introjucer version: 3.1.0
+
+  ------------------------------------------------------------------------------
+
+  The Introjucer is part of the JUCE library - "Jules' Utility Class Extensions"
+  Copyright 2004-13 by Raw Material Software Ltd.
+
+  ==============================================================================
+*/
+
+//[Headers] You can add your own extra header files here...
+//[/Headers]
+
+#include "KeyboardZoneComponent.h"
+
+
+//[MiscUserDefs] You can add your own user definitions and misc code here...
+//[/MiscUserDefs]
+
+//==============================================================================
+KeyboardZoneComponent::KeyboardZoneComponent ()
+    : controller_(0), keyboardSegment_(0)
+{
+    addAndMakeVisible (midiOutputGroupComponent = new GroupComponent ("MIDI input group",
+                                                                      "MIDI Output"));
+
+    addAndMakeVisible (midiOutputDeviceComboBox = new ComboBox ("MIDI input combo box"));
+    midiOutputDeviceComboBox->setEditableText (false);
+    midiOutputDeviceComboBox->setJustificationType (Justification::centredLeft);
+    midiOutputDeviceComboBox->setTextWhenNothingSelected (String::empty);
+    midiOutputDeviceComboBox->setTextWhenNoChoicesAvailable ("(no choices)");
+    midiOutputDeviceComboBox->addListener (this);
+
+    addAndMakeVisible (label4 = new Label ("new label",
+                                           "Device:"));
+    label4->setFont (Font (15.00f, Font::plain));
+    label4->setJustificationType (Justification::centredLeft);
+    label4->setEditable (false, false, false);
+    label4->setColour (TextEditor::textColourId, Colours::black);
+    label4->setColour (TextEditor::backgroundColourId, Colour (0x00000000));
+
+    addAndMakeVisible (label5 = new Label ("new label",
+                                           "Mode:"));
+    label5->setFont (Font (15.00f, Font::plain));
+    label5->setJustificationType (Justification::centredLeft);
+    label5->setEditable (false, false, false);
+    label5->setColour (TextEditor::textColourId, Colours::black);
+    label5->setColour (TextEditor::backgroundColourId, Colour (0x00000000));
+
+    addAndMakeVisible (midiOutputModeComboBox = new ComboBox ("MIDI input combo box"));
+    midiOutputModeComboBox->setEditableText (false);
+    midiOutputModeComboBox->setJustificationType (Justification::centredLeft);
+    midiOutputModeComboBox->setTextWhenNothingSelected (String::empty);
+    midiOutputModeComboBox->setTextWhenNoChoicesAvailable ("(no choices)");
+    midiOutputModeComboBox->addListener (this);
+
+    addAndMakeVisible (midiOutputVoiceStealingButton = new ToggleButton ("Voice stealing button"));
+    midiOutputVoiceStealingButton->setButtonText ("Voice stealing");
+    midiOutputVoiceStealingButton->addListener (this);
+
+    addAndMakeVisible (label2 = new Label ("new label",
+                                           "Channels:"));
+    label2->setFont (Font (15.00f, Font::plain));
+    label2->setJustificationType (Justification::centredLeft);
+    label2->setEditable (false, false, false);
+    label2->setColour (TextEditor::textColourId, Colours::black);
+    label2->setColour (TextEditor::backgroundColourId, Colour (0x00000000));
+
+    addAndMakeVisible (midiOutputChannelLowEditor = new TextEditor ("new text editor"));
+    midiOutputChannelLowEditor->setMultiLine (false);
+    midiOutputChannelLowEditor->setReturnKeyStartsNewLine (false);
+    midiOutputChannelLowEditor->setReadOnly (false);
+    midiOutputChannelLowEditor->setScrollbarsShown (true);
+    midiOutputChannelLowEditor->setCaretVisible (true);
+    midiOutputChannelLowEditor->setPopupMenuEnabled (true);
+    midiOutputChannelLowEditor->setText (String::empty);
+
+    addAndMakeVisible (midiOutputChannelHighEditor = new TextEditor ("new text editor"));
+    midiOutputChannelHighEditor->setMultiLine (false);
+    midiOutputChannelHighEditor->setReturnKeyStartsNewLine (false);
+    midiOutputChannelHighEditor->setReadOnly (false);
+    midiOutputChannelHighEditor->setScrollbarsShown (true);
+    midiOutputChannelHighEditor->setCaretVisible (true);
+    midiOutputChannelHighEditor->setPopupMenuEnabled (true);
+    midiOutputChannelHighEditor->setText (String::empty);
+
+    addAndMakeVisible (label3 = new Label ("new label",
+                                           "to"));
+    label3->setFont (Font (15.00f, Font::plain));
+    label3->setJustificationType (Justification::centredLeft);
+    label3->setEditable (false, false, false);
+    label3->setColour (TextEditor::textColourId, Colours::black);
+    label3->setColour (TextEditor::backgroundColourId, Colour (0x00000000));
+
+    addAndMakeVisible (midiOutputGroupComponent2 = new GroupComponent ("MIDI input group",
+                                                                       "Range"));
+
+    addAndMakeVisible (label7 = new Label ("new label",
+                                           "to"));
+    label7->setFont (Font (15.00f, Font::plain));
+    label7->setJustificationType (Justification::centredLeft);
+    label7->setEditable (false, false, false);
+    label7->setColour (TextEditor::textColourId, Colours::black);
+    label7->setColour (TextEditor::backgroundColourId, Colour (0x00000000));
+
+    addAndMakeVisible (rangeLowComboBox = new ComboBox ("range low combo box"));
+    rangeLowComboBox->setEditableText (true);
+    rangeLowComboBox->setJustificationType (Justification::centredLeft);
+    rangeLowComboBox->setTextWhenNothingSelected (String::empty);
+    rangeLowComboBox->setTextWhenNoChoicesAvailable ("(no choices)");
+    rangeLowComboBox->addListener (this);
+
+    addAndMakeVisible (rangeHighComboBox = new ComboBox ("range high combo combo box"));
+    rangeHighComboBox->setEditableText (true);
+    rangeHighComboBox->setJustificationType (Justification::centredLeft);
+    rangeHighComboBox->setTextWhenNothingSelected (String::empty);
+    rangeHighComboBox->setTextWhenNoChoicesAvailable ("(no choices)");
+    rangeHighComboBox->addListener (this);
+
+    addAndMakeVisible (useAftertouchButton = new ToggleButton ("use aftertouch button"));
+    useAftertouchButton->setButtonText ("Use keyboard aftertouch");
+    useAftertouchButton->addListener (this);
+    useAftertouchButton->setToggleState (true, dontSendNotification);
+
+    addAndMakeVisible (usePitchWheelButton = new ToggleButton ("use aftertouch button"));
+    usePitchWheelButton->setButtonText ("Use keyboard pitchwheel");
+    usePitchWheelButton->addListener (this);
+    usePitchWheelButton->setToggleState (true, dontSendNotification);
+
+    addAndMakeVisible (useControllersButton = new ToggleButton ("use aftertouch button"));
+    useControllersButton->setButtonText ("Use keyboard controllers");
+    useControllersButton->addListener (this);
+
+    addAndMakeVisible (label6 = new Label ("new label",
+                                           "Transpose:"));
+    label6->setFont (Font (15.00f, Font::plain));
+    label6->setJustificationType (Justification::centredLeft);
+    label6->setEditable (false, false, false);
+    label6->setColour (TextEditor::textColourId, Colours::black);
+    label6->setColour (TextEditor::backgroundColourId, Colour (0x00000000));
+
+    addAndMakeVisible (midiOutputTransposeEditor = new TextEditor ("transposition text editor"));
+    midiOutputTransposeEditor->setMultiLine (false);
+    midiOutputTransposeEditor->setReturnKeyStartsNewLine (false);
+    midiOutputTransposeEditor->setReadOnly (false);
+    midiOutputTransposeEditor->setScrollbarsShown (true);
+    midiOutputTransposeEditor->setCaretVisible (true);
+    midiOutputTransposeEditor->setPopupMenuEnabled (true);
+    midiOutputTransposeEditor->setText (String::empty);
+
+    addAndMakeVisible (mappingListComponent = new MappingListComponent());
+    addAndMakeVisible (label8 = new Label ("new label",
+                                           "Mappings:"));
+    label8->setFont (Font (15.00f, Font::plain));
+    label8->setJustificationType (Justification::centredLeft);
+    label8->setEditable (false, false, false);
+    label8->setColour (TextEditor::textColourId, Colours::black);
+    label8->setColour (TextEditor::backgroundColourId, Colour (0x00000000));
+
+    addAndMakeVisible (addMappingButton = new TextButton ("add mapping button"));
+    addMappingButton->setButtonText ("Add Mapping...");
+    addMappingButton->addListener (this);
+
+
+    //[UserPreSize]
+    // Add modes to MIDI mode toggle box
+    midiOutputModeComboBox->addItem("Passthrough", MidiKeyboardSegment::ModePassThrough + kMidiOutputModeComboBoxOffset);
+    midiOutputModeComboBox->addItem("Monophonic", MidiKeyboardSegment::ModeMonophonic + kMidiOutputModeComboBoxOffset);
+    midiOutputModeComboBox->addItem("Polyphonic", MidiKeyboardSegment::ModePolyphonic + kMidiOutputModeComboBoxOffset);
+
+    // Populate the range combo boxes with notes of the 88-key keyboard
+    for(int note = 12; note <= 127; note++) {
+        rangeLowComboBox->addItem(MainApplicationController::midiNoteName(note).c_str(), note);
+        rangeHighComboBox->addItem(MainApplicationController::midiNoteName(note).c_str(), note);
+    }
+
+    lastSelectedMidiOutputID_ = -100;
+
+    //[/UserPreSize]
+
+    setSize (552, 400);
+
+
+    //[Constructor] You can add your own custom stuff here..
+    midiOutputChannelLowEditor->addListener(this);
+    midiOutputChannelHighEditor->addListener(this);
+    midiOutputTransposeEditor->addListener(this);
+    addMappingButton->setTriggeredOnMouseDown(true);
+    //[/Constructor]
+}
+
+KeyboardZoneComponent::~KeyboardZoneComponent()
+{
+    //[Destructor_pre]. You can add your own custom destruction code here..
+    //[/Destructor_pre]
+
+    midiOutputGroupComponent = nullptr;
+    midiOutputDeviceComboBox = nullptr;
+    label4 = nullptr;
+    label5 = nullptr;
+    midiOutputModeComboBox = nullptr;
+    midiOutputVoiceStealingButton = nullptr;
+    label2 = nullptr;
+    midiOutputChannelLowEditor = nullptr;
+    midiOutputChannelHighEditor = nullptr;
+    label3 = nullptr;
+    midiOutputGroupComponent2 = nullptr;
+    label7 = nullptr;
+    rangeLowComboBox = nullptr;
+    rangeHighComboBox = nullptr;
+    useAftertouchButton = nullptr;
+    usePitchWheelButton = nullptr;
+    useControllersButton = nullptr;
+    label6 = nullptr;
+    midiOutputTransposeEditor = nullptr;
+    mappingListComponent = nullptr;
+    label8 = nullptr;
+    addMappingButton = nullptr;
+
+
+    //[Destructor]. You can add your own custom destruction code here..
+    //[/Destructor]
+}
+
+//==============================================================================
+void KeyboardZoneComponent::paint (Graphics& g)
+{
+    //[UserPrePaint] Add your own custom painting code here..
+    //[/UserPrePaint]
+
+    g.fillAll (Colour (0xffd2d2d2));
+
+    //[UserPaint] Add your own custom painting code here..
+    //[/UserPaint]
+}
+
+void KeyboardZoneComponent::resized()
+{
+    midiOutputGroupComponent->setBounds (200, 8, 344, 128);
+    midiOutputDeviceComboBox->setBounds (264, 32, 264, 24);
+    label4->setBounds (208, 32, 55, 24);
+    label5->setBounds (208, 64, 55, 24);
+    midiOutputModeComboBox->setBounds (264, 64, 152, 24);
+    midiOutputVoiceStealingButton->setBounds (424, 64, 112, 24);
+    label2->setBounds (208, 96, 56, 24);
+    midiOutputChannelLowEditor->setBounds (264, 96, 32, 24);
+    midiOutputChannelHighEditor->setBounds (320, 96, 32, 24);
+    label3->setBounds (296, 96, 32, 24);
+    midiOutputGroupComponent2->setBounds (8, 8, 184, 128);
+    label7->setBounds (88, 32, 32, 24);
+    rangeLowComboBox->setBounds (24, 32, 64, 24);
+    rangeHighComboBox->setBounds (112, 32, 64, 24);
+    useAftertouchButton->setBounds (24, 56, 152, 24);
+    usePitchWheelButton->setBounds (24, 80, 152, 24);
+    useControllersButton->setBounds (24, 104, 152, 24);
+    label6->setBounds (392, 96, 80, 24);
+    midiOutputTransposeEditor->setBounds (472, 96, 56, 24);
+    mappingListComponent->setBounds (0, 168, 552, 260);
+    label8->setBounds (8, 144, 88, 24);
+    addMappingButton->setBounds (440, 144, 104, 20);
+    //[UserResized] Add your own custom resize handling here..
+    //[/UserResized]
+}
+
+void KeyboardZoneComponent::comboBoxChanged (ComboBox* comboBoxThatHasChanged)
+{
+    //[UsercomboBoxChanged_Pre]
+    if(keyboardSegment_ == 0 || controller_ == 0)
+        return;
+    //[/UsercomboBoxChanged_Pre]
+
+    if (comboBoxThatHasChanged == midiOutputDeviceComboBox)
+    {
+        //[UserComboBoxCode_midiOutputDeviceComboBox] -- add your combo box handling code here..
+
+        // Look up the selected ID, remembering that Juce indices start at 1 and the first of
+        // these is "Disabled" followed by "Virtual Output Port"
+        int selection = midiOutputDeviceComboBox->getSelectedId() - kMidiOutputDeviceComboBoxOffset;
+        if(selection == 1 - kMidiOutputDeviceComboBoxOffset) {   // Disabled
+            controller_->disableMIDIOutputPort(keyboardSegment_->outputPort());
+        }
+        else if(selection == 2 - kMidiOutputDeviceComboBoxOffset) { // Virtual output
+            char st[20];
+            snprintf(st, 20, "TouchKeys %d", keyboardSegment_->outputPort());
+            controller_->enableMIDIOutputVirtualPort(keyboardSegment_->outputPort(), st);
+        }
+        else if(selection >= 0 && selection < midiOutputDeviceIDs_.size()) {
+            int deviceId = midiOutputDeviceIDs_[selection];
+            controller_->enableMIDIOutputPort(keyboardSegment_->outputPort(), deviceId);
+        }
+        //[/UserComboBoxCode_midiOutputDeviceComboBox]
+    }
+    else if (comboBoxThatHasChanged == midiOutputModeComboBox)
+    {
+        //[UserComboBoxCode_midiOutputModeComboBox] -- add your combo box handling code here..
+        int mode = midiOutputModeComboBox->getSelectedId() - kMidiOutputModeComboBoxOffset;
+        keyboardSegment_->setMode(mode);
+        //[/UserComboBoxCode_midiOutputModeComboBox]
+    }
+    else if (comboBoxThatHasChanged == rangeLowComboBox)
+    {
+        //[UserComboBoxCode_rangeLowComboBox] -- add your combo box handling code here..
+        updateSegmentRange();
+        //[/UserComboBoxCode_rangeLowComboBox]
+    }
+    else if (comboBoxThatHasChanged == rangeHighComboBox)
+    {
+        //[UserComboBoxCode_rangeHighComboBox] -- add your combo box handling code here..
+        updateSegmentRange();
+        //[/UserComboBoxCode_rangeHighComboBox]
+    }
+
+    //[UsercomboBoxChanged_Post]
+    //[/UsercomboBoxChanged_Post]
+}
+
+void KeyboardZoneComponent::buttonClicked (Button* buttonThatWasClicked)
+{
+    //[UserbuttonClicked_Pre]
+    if(keyboardSegment_ == 0)
+        return;
+    //[/UserbuttonClicked_Pre]
+
+    if (buttonThatWasClicked == midiOutputVoiceStealingButton)
+    {
+        //[UserButtonCode_midiOutputVoiceStealingButton] -- add your button handler code here..
+        bool stealing = midiOutputVoiceStealingButton->getToggleState();
+        keyboardSegment_->setVoiceStealingEnabled(stealing);
+        //[/UserButtonCode_midiOutputVoiceStealingButton]
+    }
+    else if (buttonThatWasClicked == useAftertouchButton)
+    {
+        //[UserButtonCode_useAftertouchButton] -- add your button handler code here..
+        bool aftertouch = useAftertouchButton->getToggleState();
+        keyboardSegment_->setUsesKeyboardChannelPressure(aftertouch);
+        //[/UserButtonCode_useAftertouchButton]
+    }
+    else if (buttonThatWasClicked == usePitchWheelButton)
+    {
+        //[UserButtonCode_usePitchWheelButton] -- add your button handler code here..
+        bool pitchwheel = usePitchWheelButton->getToggleState();
+        keyboardSegment_->setUsesKeyboardPitchWheel(pitchwheel);
+        //[/UserButtonCode_usePitchWheelButton]
+    }
+    else if (buttonThatWasClicked == useControllersButton)
+    {
+        //[UserButtonCode_useControllersButton] -- add your button handler code here..
+        bool controllers = useControllersButton->getToggleState();
+        keyboardSegment_->setUsesKeyboardMIDIControllers(controllers);
+        //[/UserButtonCode_useControllersButton]
+    }
+    else if (buttonThatWasClicked == addMappingButton)
+    {
+        //[UserButtonCode_addMappingButton] -- add your button handler code here..
+        // TODO: add new mapping
+        createMappingListPopup();
+        //[/UserButtonCode_addMappingButton]
+    }
+
+    //[UserbuttonClicked_Post]
+    //[/UserbuttonClicked_Post]
+}
+
+
+
+//[MiscUserCode] You can add your own definitions of your custom methods or any other code here...
+
+void KeyboardZoneComponent::textEditorReturnKeyPressed(TextEditor &editor)
+{
+    if(keyboardSegment_ == 0)
+        return;
+
+    if(&editor == midiOutputChannelLowEditor ||
+       &editor == midiOutputChannelHighEditor) {
+        // Change range of MIDI output channels
+        int rangeLow = atoi(midiOutputChannelLowEditor->getText().toUTF8());
+        int rangeHigh = atoi(midiOutputChannelHighEditor->getText().toUTF8());
+        if(rangeHigh > 16)
+            rangeHigh = 16;
+        if(rangeLow > 16)
+            rangeLow = 16;
+        if(rangeHigh < 1)
+            rangeHigh = 1;
+        if(rangeLow < 1)
+            rangeLow = 1;
+        keyboardSegment_->setOutputChannelLowest(rangeLow - 1); // 1-16 --> 0-15 indexing
+
+        int polyphony = rangeHigh - rangeLow + 1;
+        if(polyphony < 1)
+            polyphony = 1;
+        keyboardSegment_->setPolyphony(polyphony);
+    }
+    else if(&editor == midiOutputTransposeEditor) {
+        // Change output transposition (limiting possible range to +/- 4 octaves)
+        int transpose = atoi(midiOutputTransposeEditor->getText().toUTF8());
+        if(transpose < -48)
+            transpose = -48;
+        if(transpose > 48)
+            transpose = 48;
+        keyboardSegment_->setOutputTransposition(transpose);
+    }
+}
+
+void KeyboardZoneComponent::textEditorEscapeKeyPressed(TextEditor &editor)
+{
+
+}
+
+void KeyboardZoneComponent::textEditorFocusLost(TextEditor &editor)
+{
+    textEditorReturnKeyPressed(editor);
+}
+
+// Update state of GUI to reflect underlying controller
+void KeyboardZoneComponent::synchronize(bool forceUpdates)
+{
+    if(keyboardSegment_ == 0 || controller_ == 0)
+        return;
+
+    // Update note ranges
+    std::pair<int, int> range = keyboardSegment_->noteRange();
+    if(!rangeLowComboBox->hasKeyboardFocus(true) || forceUpdates) {
+        if(range.first < 12 || range.first > 127) {
+            rangeLowComboBox->setText(String(range.first));
+        }
+        else
+            rangeLowComboBox->setSelectedId(range.first, dontSendNotification);
+    }
+    if(!rangeHighComboBox->hasKeyboardFocus(true) || forceUpdates) {
+        if(range.second < 12 || range.second > 127) {
+            rangeHighComboBox->setText(String(range.second));
+        }
+        else
+            rangeHighComboBox->setSelectedId(range.second, dontSendNotification);
+    }
+
+    // Update MIDI output status
+    int selectedMidiOutputDevice = controller_->selectedMIDIOutputPort(keyboardSegment_->outputPort());
+    if(selectedMidiOutputDevice != lastSelectedMidiOutputID_) {
+        if(selectedMidiOutputDevice == MidiOutputController::kMidiOutputNotOpen)
+            midiOutputDeviceComboBox->setSelectedId(1, dontSendNotification);
+        else if(selectedMidiOutputDevice == MidiOutputController::kMidiVirtualOutputPortNumber)
+            midiOutputDeviceComboBox->setSelectedId(2, dontSendNotification);
+        else {
+            // Find the output device in the vector
+            for(int i = 0; i < midiOutputDeviceIDs_.size(); i++) {
+                if(midiOutputDeviceIDs_[i] == selectedMidiOutputDevice) {
+                    midiOutputDeviceComboBox->setSelectedId(i + kMidiOutputDeviceComboBoxOffset, dontSendNotification);
+                    break;
+                }
+            }
+        }
+        lastSelectedMidiOutputID_ = selectedMidiOutputDevice;
+    }
+
+    // Update the mode and the peripheral controls that go with it
+    int selectedMidiOutputMode = keyboardSegment_->mode();
+    midiOutputModeComboBox->setSelectedId(selectedMidiOutputMode + kMidiOutputModeComboBoxOffset, dontSendNotification);
+
+    if(selectedMidiOutputMode == MidiKeyboardSegment::ModePolyphonic) {
+        midiOutputVoiceStealingButton->setEnabled(true);
+        midiOutputVoiceStealingButton->setToggleState(keyboardSegment_->voiceStealingEnabled(), dontSendNotification);
+    }
+    else {
+        midiOutputVoiceStealingButton->setEnabled(false);
+        midiOutputVoiceStealingButton->setToggleState(false, dontSendNotification);
+    }
+
+    // Update text editors
+    if(!midiOutputChannelLowEditor->hasKeyboardFocus(true) || forceUpdates) {
+        int rangeLow = keyboardSegment_->outputChannelLowest() + 1; // 0-15 --> 1-16
+        midiOutputChannelLowEditor->setText(String(rangeLow));
+    }
+    if(!midiOutputTransposeEditor->hasKeyboardFocus(true) || forceUpdates) {
+        int transpose = keyboardSegment_->outputTransposition();
+        midiOutputTransposeEditor->setText(String(transpose));
+    }
+
+    if(selectedMidiOutputMode == MidiKeyboardSegment::ModePolyphonic) {
+        midiOutputChannelHighEditor->setEnabled(true);
+        if(!midiOutputChannelHighEditor->hasKeyboardFocus(true) || forceUpdates) {
+            int rangeHigh = keyboardSegment_->polyphony() + keyboardSegment_->outputChannelLowest();
+            midiOutputChannelHighEditor->setText(String(rangeHigh));
+        }
+    }
+    else {
+        midiOutputChannelHighEditor->setEnabled(false);
+        midiOutputChannelHighEditor->setText("", false);
+    }
+
+    // Update buttons
+    useAftertouchButton->setToggleState(keyboardSegment_->usesKeyboardChannnelPressure(), dontSendNotification);
+    usePitchWheelButton->setToggleState(keyboardSegment_->usesKeyboardPitchWheel(), dontSendNotification);
+    useControllersButton->setToggleState(keyboardSegment_->usesKeyboardMIDIControllers(), dontSendNotification);
+
+    // Update the mapping list
+    mappingListComponent->synchronize();
+}
+
+// Update the range of the keyboard segment
+void KeyboardZoneComponent::updateSegmentRange()
+{
+    int selectionLow = rangeLowComboBox->getSelectedId();
+    int noteLow = -1;
+    if(selectionLow == 0) {
+        // Not one of the predefined values that's selected. Parse the string.
+        noteLow = MainApplicationController::midiNoteNumberForName((const char *)(rangeLowComboBox->getText().toUTF8()));
+    }
+    else {
+        noteLow = selectionLow;
+    }
+
+    if(noteLow < 0 || noteLow > 127) {
+        // Out of range: keep the old value
+        noteLow = keyboardSegment_->noteRange().first;
+    }
+
+    int selectionHigh = rangeHighComboBox->getSelectedId();
+    int noteHigh = -1;
+    if(selectionHigh == 0) {
+        // Not one of the predefined values that's selected. Parse the string.
+        noteHigh = MainApplicationController::midiNoteNumberForName((const char *)(rangeHighComboBox->getText().toUTF8()));
+    }
+    else {
+        noteHigh = selectionHigh;
+    }
+
+    if(noteHigh < 0 || noteHigh > 127) {
+        // Out of range: keep the old value
+        noteHigh = keyboardSegment_->noteRange().second;
+    }
+
+    if(noteHigh < noteLow)
+        noteHigh = noteLow;
+    keyboardSegment_->setNoteRange(noteLow, noteHigh);
+}
+
+// Update the combo box with the current output devices
+void KeyboardZoneComponent::updateOutputDeviceList()
+{
+    if(controller_ == 0 || keyboardSegment_ == 0)
+        return;
+
+    // *** MIDI output devices ***
+    vector<pair<int, string> > devices = controller_->availableMIDIOutputDevices();
+    vector<pair<int, string> >::iterator it;
+    char virtualPortName[24];
+
+    snprintf(virtualPortName, 24, "Virtual Port (%d)", keyboardSegment_->outputPort());
+    midiOutputDeviceComboBox->clear();
+    midiOutputDeviceIDs_.clear();
+    midiOutputDeviceComboBox->addItem("Disabled", 1);
+    midiOutputDeviceComboBox->addItem(virtualPortName, 2);
+    int counter = kMidiOutputDeviceComboBoxOffset;
+    for(it = devices.begin(); it != devices.end(); ++it) {
+        if(it->first < 0)
+            continue;
+        midiOutputDeviceComboBox->addItem((*it).second.c_str(), counter);
+        midiOutputDeviceIDs_.push_back(it->first);
+        counter++;
+    }
+}
+
+// Create a popup menu containing a list of mapping factories
+void KeyboardZoneComponent::createMappingListPopup()
+{
+    if(controller_ == 0)
+        return;
+
+    PopupMenu menu;
+
+    for(int i = 0; i < controller_->numberOfMappingFactories(); i++) {
+        if(controller_->experimentalMappingsEnabled() || !controller_->mappingIsExperimental(i))
+            menu.addItem(i + 1, controller_->mappingFactoryNameForIndex(i));
+    }
+
+    menu.showMenuAsync(PopupMenu::Options().withTargetComponent(addMappingButton),
+                       ModalCallbackFunction::forComponent(staticMappingChosenCallback, this));
+}
+
+// Called from the popup menu, indicating the selected item
+void KeyboardZoneComponent::mappingChosenCallback(int result)
+{
+    if(controller_ == 0 || keyboardSegment_ == 0)
+        return;
+
+    // Items are numbered from 1 in the menu but from 0 in the array in the controller
+    if(result >= 1) {
+        MappingFactory *newFactory = controller_->createMappingFactoryForIndex(result - 1, *keyboardSegment_);
+
+        if(newFactory != 0) {
+            keyboardSegment_->addMappingFactory(newFactory, true);
+        }
+    }
+}
+//[/MiscUserCode]
+
+
+//==============================================================================
+#if 0
+/*  -- Introjucer information section --
+
+    This is where the Introjucer stores the metadata that describe this GUI layout, so
+    make changes in here at your peril!
+
+BEGIN_JUCER_METADATA
+
+<JUCER_COMPONENT documentType="Component" className="KeyboardZoneComponent" componentName=""
+                 parentClasses="public Component, public TextEditor::Listener"
+                 constructorParams="" variableInitialisers="controller_(0), keyboardSegment_(0)"
+                 snapPixels="8" snapActive="1" snapShown="1" overlayOpacity="0.330"
+                 fixedSize="1" initialWidth="552" initialHeight="400">
+  <BACKGROUND backgroundColour="ffd2d2d2"/>
+  <GROUPCOMPONENT name="MIDI input group" id="49eee95279c0cc95" memberName="midiOutputGroupComponent"
+                  virtualName="" explicitFocusOrder="0" pos="200 8 344 128" title="MIDI Output"/>
+  <COMBOBOX name="MIDI input combo box" id="244410f02f6c1c72" memberName="midiOutputDeviceComboBox"
+            virtualName="" explicitFocusOrder="0" pos="264 32 264 24" editable="0"
+            layout="33" items="" textWhenNonSelected="" textWhenNoItems="(no choices)"/>
+  <LABEL name="new label" id="e9b3daa69a8ac5c" memberName="label4" virtualName=""
+         explicitFocusOrder="0" pos="208 32 55 24" edTextCol="ff000000"
+         edBkgCol="0" labelText="Device:" editableSingleClick="0" editableDoubleClick="0"
+         focusDiscardsChanges="0" fontname="Default font" fontsize="15"
+         bold="0" italic="0" justification="33"/>
+  <LABEL name="new label" id="c578e3610ba16aaf" memberName="label5" virtualName=""
+         explicitFocusOrder="0" pos="208 64 55 24" edTextCol="ff000000"
+         edBkgCol="0" labelText="Mode:" editableSingleClick="0" editableDoubleClick="0"
+         focusDiscardsChanges="0" fontname="Default font" fontsize="15"
+         bold="0" italic="0" justification="33"/>
+  <COMBOBOX name="MIDI input combo box" id="b129ec2d38f47a91" memberName="midiOutputModeComboBox"
+            virtualName="" explicitFocusOrder="0" pos="264 64 152 24" editable="0"
+            layout="33" items="" textWhenNonSelected="" textWhenNoItems="(no choices)"/>
+  <TOGGLEBUTTON name="Voice stealing button" id="62c82600413ca060" memberName="midiOutputVoiceStealingButton"
+                virtualName="" explicitFocusOrder="0" pos="424 64 112 24" buttonText="Voice stealing"
+                connectedEdges="0" needsCallback="1" radioGroupId="0" state="0"/>
+  <LABEL name="new label" id="afb5095c42b66671" memberName="label2" virtualName=""
+         explicitFocusOrder="0" pos="208 96 56 24" edTextCol="ff000000"
+         edBkgCol="0" labelText="Channels:" editableSingleClick="0" editableDoubleClick="0"
+         focusDiscardsChanges="0" fontname="Default font" fontsize="15"
+         bold="0" italic="0" justification="33"/>
+  <TEXTEDITOR name="new text editor" id="8fba4a69492a2f4f" memberName="midiOutputChannelLowEditor"
+              virtualName="" explicitFocusOrder="0" pos="264 96 32 24" initialText=""
+              multiline="0" retKeyStartsLine="0" readonly="0" scrollbars="1"
+              caret="1" popupmenu="1"/>
+  <TEXTEDITOR name="new text editor" id="21b3096394683581" memberName="midiOutputChannelHighEditor"
+              virtualName="" explicitFocusOrder="0" pos="320 96 32 24" initialText=""
+              multiline="0" retKeyStartsLine="0" readonly="0" scrollbars="1"
+              caret="1" popupmenu="1"/>
+  <LABEL name="new label" id="f6b023e6043849e7" memberName="label3" virtualName=""
+         explicitFocusOrder="0" pos="296 96 32 24" edTextCol="ff000000"
+         edBkgCol="0" labelText="to" editableSingleClick="0" editableDoubleClick="0"
+         focusDiscardsChanges="0" fontname="Default font" fontsize="15"
+         bold="0" italic="0" justification="33"/>
+  <GROUPCOMPONENT name="MIDI input group" id="388fb821de641818" memberName="midiOutputGroupComponent2"
+                  virtualName="" explicitFocusOrder="0" pos="8 8 184 128" title="Range"/>
+  <LABEL name="new label" id="bff0e81cc2020a66" memberName="label7" virtualName=""
+         explicitFocusOrder="0" pos="88 32 32 24" edTextCol="ff000000"
+         edBkgCol="0" labelText="to" editableSingleClick="0" editableDoubleClick="0"
+         focusDiscardsChanges="0" fontname="Default font" fontsize="15"
+         bold="0" italic="0" justification="33"/>
+  <COMBOBOX name="range low combo box" id="86999fb7f9fe9c2" memberName="rangeLowComboBox"
+            virtualName="" explicitFocusOrder="0" pos="24 32 64 24" editable="1"
+            layout="33" items="" textWhenNonSelected="" textWhenNoItems="(no choices)"/>
+  <COMBOBOX name="range high combo combo box" id="7cba07ed947e85b2" memberName="rangeHighComboBox"
+            virtualName="" explicitFocusOrder="0" pos="112 32 64 24" editable="1"
+            layout="33" items="" textWhenNonSelected="" textWhenNoItems="(no choices)"/>
+  <TOGGLEBUTTON name="use aftertouch button" id="bd917dd46e68ffa3" memberName="useAftertouchButton"
+                virtualName="" explicitFocusOrder="0" pos="24 56 152 24" buttonText="Use keyboard aftertouch"
+                connectedEdges="0" needsCallback="1" radioGroupId="0" state="1"/>
+  <TOGGLEBUTTON name="use aftertouch button" id="479868bf74ee0a1a" memberName="usePitchWheelButton"
+                virtualName="" explicitFocusOrder="0" pos="24 80 152 24" buttonText="Use keyboard pitchwheel"
+                connectedEdges="0" needsCallback="1" radioGroupId="0" state="1"/>
+  <TOGGLEBUTTON name="use aftertouch button" id="e3b778166fac4e5f" memberName="useControllersButton"
+                virtualName="" explicitFocusOrder="0" pos="24 104 152 24" buttonText="Use keyboard controllers"
+                connectedEdges="0" needsCallback="1" radioGroupId="0" state="0"/>
+  <LABEL name="new label" id="fd730bc972dffbdb" memberName="label6" virtualName=""
+         explicitFocusOrder="0" pos="392 96 80 24" edTextCol="ff000000"
+         edBkgCol="0" labelText="Transpose:" editableSingleClick="0" editableDoubleClick="0"
+         focusDiscardsChanges="0" fontname="Default font" fontsize="15"
+         bold="0" italic="0" justification="33"/>
+  <TEXTEDITOR name="transposition text editor" id="6f96be1359a01685" memberName="midiOutputTransposeEditor"
+              virtualName="" explicitFocusOrder="0" pos="472 96 56 24" initialText=""
+              multiline="0" retKeyStartsLine="0" readonly="0" scrollbars="1"
+              caret="1" popupmenu="1"/>
+  <JUCERCOMP name="mapping list" id="4d5d007374cdad00" memberName="mappingListComponent"
+             virtualName="MappingListComponent" explicitFocusOrder="0" pos="0 168 552 260"
+             sourceFile="" constructorParams=""/>
+  <LABEL name="new label" id="759d38e4603010a8" memberName="label8" virtualName=""
+         explicitFocusOrder="0" pos="8 144 88 24" edTextCol="ff000000"
+         edBkgCol="0" labelText="Mappings:" editableSingleClick="0" editableDoubleClick="0"
+         focusDiscardsChanges="0" fontname="Default font" fontsize="15"
+         bold="0" italic="0" justification="33"/>
+  <TEXTBUTTON name="add mapping button" id="a5fd2f0afd2d74b2" memberName="addMappingButton"
+              virtualName="" explicitFocusOrder="0" pos="440 144 104 20" buttonText="Add Mapping..."
+              connectedEdges="0" needsCallback="1" radioGroupId="0"/>
+</JUCER_COMPONENT>
+
+END_JUCER_METADATA
+*/
+#endif
+
+
+//[EndFile] You can add extra defines here...
+//[/EndFile]
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/GUI/KeyboardZoneComponent.h	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,156 @@
+/*
+  ==============================================================================
+
+  This is an automatically generated GUI class created by the Introjucer!
+
+  Be careful when adding custom code to these files, as only the code within
+  the "//[xyz]" and "//[/xyz]" sections will be retained when the file is loaded
+  and re-saved.
+
+  Created with Introjucer version: 3.1.0
+
+  ------------------------------------------------------------------------------
+
+  The Introjucer is part of the JUCE library - "Jules' Utility Class Extensions"
+  Copyright 2004-13 by Raw Material Software Ltd.
+
+  ==============================================================================
+*/
+
+#ifndef __JUCE_HEADER_E3CF42F64919BE28__
+#define __JUCE_HEADER_E3CF42F64919BE28__
+
+//[Headers]     -- You can add your own extra header files here --
+#include "JuceHeader.h"
+#include "MappingListComponent.h"
+#include "../MainApplicationController.h"
+#include "../TouchKeys/MidiKeyboardSegment.h"
+//[/Headers]
+
+
+
+//==============================================================================
+/**
+                                                                    //[Comments]
+    An auto-generated component, created by the Introjucer.
+
+    Describe your class and how it works here!
+                                                                    //[/Comments]
+*/
+class KeyboardZoneComponent  : public Component,
+                               public TextEditor::Listener,
+                               public ComboBoxListener,
+                               public ButtonListener
+{
+public:
+    //==============================================================================
+    KeyboardZoneComponent ();
+    ~KeyboardZoneComponent();
+
+    //==============================================================================
+    //[UserMethods]     -- You can add your own custom methods in this section.
+    void setMainApplicationController(MainApplicationController *controller) {
+        // Attach the user interface to the controller and vice-versa
+        controller_ = controller;
+        mappingListComponent->setMainApplicationController(controller_);
+        if(controller_ != 0) {
+            // Update the controls to reflect the current state
+            updateOutputDeviceList();
+            synchronize(true);
+        }
+    }
+
+    void setKeyboardSegment(MidiKeyboardSegment *segment) {
+        keyboardSegment_ = segment;
+        mappingListComponent->setKeyboardSegment(keyboardSegment_);
+        if(controller_ != 0) {
+            // Update the controls to reflect the current state
+            updateOutputDeviceList();
+            synchronize(true);
+        }
+    }
+
+    MidiKeyboardSegment* keyboardSegment() {
+        return keyboardSegment_;
+    }
+
+    // TextEditor listener methods
+    void textEditorTextChanged(TextEditor &editor) {}
+    void textEditorReturnKeyPressed(TextEditor &editor);
+    void textEditorEscapeKeyPressed(TextEditor &editor);
+    void textEditorFocusLost(TextEditor &editor);
+
+    // Synchronize UI state to match underlying state of the back end
+    void synchronize(bool forceUpdates = false);
+
+    // Update the range of the keyboard segment
+    void updateSegmentRange();
+
+    static void staticMappingChosenCallback(int result, KeyboardZoneComponent* component) {
+        if (result != 0 && component != 0)
+            component->mappingChosenCallback(result);
+    }
+    void mappingChosenCallback(int result);
+    //[/UserMethods]
+
+    void paint (Graphics& g);
+    void resized();
+    void comboBoxChanged (ComboBox* comboBoxThatHasChanged);
+    void buttonClicked (Button* buttonThatWasClicked);
+
+
+
+private:
+    //[UserVariables]   -- You can add your own custom variables in this section.
+
+    enum {
+        // Offsets between Juce UI IDs and positions in vector
+        kMidiOutputDeviceComboBoxOffset = 3,
+        kMidiOutputModeComboBoxOffset = 1
+    };
+
+    // Update list of MIDI output devices
+    void updateOutputDeviceList();
+
+    // Create popup menu for mapping list
+    void createMappingListPopup();
+
+    MainApplicationController *controller_; // Pointer to the main application controller
+    MidiKeyboardSegment *keyboardSegment_;  // Pointer to the segment this component controls
+    std::vector<int> midiOutputDeviceIDs_;
+    int lastSelectedMidiOutputID_;
+    //[/UserVariables]
+
+    //==============================================================================
+    ScopedPointer<GroupComponent> midiOutputGroupComponent;
+    ScopedPointer<ComboBox> midiOutputDeviceComboBox;
+    ScopedPointer<Label> label4;
+    ScopedPointer<Label> label5;
+    ScopedPointer<ComboBox> midiOutputModeComboBox;
+    ScopedPointer<ToggleButton> midiOutputVoiceStealingButton;
+    ScopedPointer<Label> label2;
+    ScopedPointer<TextEditor> midiOutputChannelLowEditor;
+    ScopedPointer<TextEditor> midiOutputChannelHighEditor;
+    ScopedPointer<Label> label3;
+    ScopedPointer<GroupComponent> midiOutputGroupComponent2;
+    ScopedPointer<Label> label7;
+    ScopedPointer<ComboBox> rangeLowComboBox;
+    ScopedPointer<ComboBox> rangeHighComboBox;
+    ScopedPointer<ToggleButton> useAftertouchButton;
+    ScopedPointer<ToggleButton> usePitchWheelButton;
+    ScopedPointer<ToggleButton> useControllersButton;
+    ScopedPointer<Label> label6;
+    ScopedPointer<TextEditor> midiOutputTransposeEditor;
+    ScopedPointer<MappingListComponent> mappingListComponent;
+    ScopedPointer<Label> label8;
+    ScopedPointer<TextButton> addMappingButton;
+
+
+    //==============================================================================
+    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (KeyboardZoneComponent)
+};
+
+//[EndFile] You can add extra defines here...
+//[/EndFile]
+
+#endif   // __JUCE_HEADER_E3CF42F64919BE28__
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/GUI/MainWindow.cpp	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,295 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+ 
+  MainWindow.cpp: the control window, plus menu bar and Juce application methods
+*/
+
+#include "../../JuceLibraryCode/JuceHeader.h"
+#include "MainWindow.h"
+
+//==============================================================================
+MainWindow::MainWindow(MainApplicationController& controller)
+: DocumentWindow ("TouchKeys", Colours::lightgrey, DocumentWindow::allButtons),
+  controller_(controller)
+{
+    commandManager_.registerAllCommandsForTarget(this);
+    commandManager_.registerAllCommandsForTarget(JUCEApplication::getInstance());
+    
+    // This lets the command manager use keypresses that arrive in our window to send
+    // out commands
+    addKeyListener(commandManager_.getKeyMappings());
+    
+    // Create the menu bar, either in the window or at the top of the screen
+#if JUCE_MAC
+    MenuBarModel::setMacMainMenu(this);
+#else
+    // Menu bar goes at the top of the window
+    setMenuBar(this);
+    
+    // Window height should change to accommodate it
+    Component *actualMenuBar = getMenuBarComponent();
+    if(actualMenuBar != 0) {
+        Rectangle<int> r = actualMenuBar->getBounds();
+        setSize(getWidth(), getHeight() + r.getHeight());
+    }
+#endif /* JUCE_MAC */
+    
+    // Tells our menu bar model that it should watch this command manager for
+    // changes, and send change messages accordingly.
+    setApplicationCommandManagerToWatch(&commandManager_);
+    
+    mainComponent_.setMainApplicationController(&controller_);
+    setContentNonOwned(&mainComponent_, true);
+    
+    commandManager_.setFirstCommandTarget(this);
+    
+    // Start a timer that will keep the interface in sync with the application
+    startTimer(50);
+    
+    centreWithSize (getWidth(), getHeight());
+    setUsingNativeTitleBar(true);
+    setVisible (true);
+}
+
+MainWindow::~MainWindow() {
+    // Delete the menu bar before exiting
+#if JUCE_MAC
+    MenuBarModel::setMacMainMenu(nullptr);
+#else
+    setMenuBar(nullptr);
+#endif /* JUCE_MAC */
+}
+
+// Handle periodic GUI updates, keeping the GUI in sync with the underlying system
+void MainWindow::timerCallback() {
+    mainComponent_.synchronize();
+}
+
+// ***** Juce menu bar methods ****
+StringArray MainWindow::getMenuBarNames() {
+    const char* const names[] = { "File", "Edit", "Control", "Window", nullptr };
+    
+    return StringArray (names);
+}
+
+PopupMenu MainWindow::getMenuForIndex(int menuIndex, const String& menuName) {
+    PopupMenu menu;
+    
+    if(menuIndex == 0) { // File
+        menu.addCommandItem(&commandManager_, kCommandNewPreset);
+        menu.addSeparator();
+        menu.addCommandItem(&commandManager_, kCommandOpenPreset);
+        menu.addSeparator();
+        menu.addCommandItem(&commandManager_, kCommandSavePreset);
+        menu.addCommandItem(&commandManager_, kCommandSavePresetAs);
+#ifndef JUCE_MAC
+        menu.addSeparator();
+        menu.addCommandItem(&commandManager_, StandardApplicationCommandIDs::quit);
+#endif
+    }
+    else if(menuIndex == 1) { // Edit
+        menu.addCommandItem(&commandManager_, StandardApplicationCommandIDs::undo);
+        menu.addCommandItem(&commandManager_, StandardApplicationCommandIDs::redo);
+        menu.addSeparator();
+        menu.addCommandItem(&commandManager_, StandardApplicationCommandIDs::cut);
+        menu.addCommandItem(&commandManager_, StandardApplicationCommandIDs::copy);
+        menu.addCommandItem(&commandManager_, StandardApplicationCommandIDs::paste);
+        menu.addCommandItem(&commandManager_, StandardApplicationCommandIDs::del);
+        menu.addSeparator();
+        menu.addCommandItem(&commandManager_, StandardApplicationCommandIDs::selectAll);
+    }
+    else if(menuIndex == 2) { // Control
+        menu.addCommandItem(&commandManager_, kCommandRescanDevices);
+        menu.addSeparator();
+        menu.addCommandItem(&commandManager_, kCommandEnableExperimentalMappings);
+    }
+    else if(menuIndex == 3) { // Window
+        menu.addCommandItem(&commandManager_, kCommandShowControlWindow);
+        menu.addCommandItem(&commandManager_, kCommandShowKeyboardWindow);
+    }
+    
+    return menu;
+}
+
+void MainWindow::menuItemSelected(int menuItemID, int topLevelMenuIndex) {
+    // TODO
+}
+
+// **** Command Manager methods *****
+
+ApplicationCommandTarget* MainWindow::getNextCommandTarget() {
+    return findFirstTargetParentComponent();
+}
+
+// this returns the set of all commands that this target can perform..
+void MainWindow::getAllCommands(Array <CommandID>& commands) {
+    // this returns the set of all commands that this target can perform..
+    const CommandID ids[] = {
+        // File
+        kCommandNewPreset, kCommandOpenPreset, kCommandSavePreset,
+        kCommandSavePresetAs,
+        // Edit
+        StandardApplicationCommandIDs::undo,
+        StandardApplicationCommandIDs::redo,
+        StandardApplicationCommandIDs::cut,
+        StandardApplicationCommandIDs::copy,
+        StandardApplicationCommandIDs::paste,
+        StandardApplicationCommandIDs::del,
+        StandardApplicationCommandIDs::selectAll,
+        // Control
+        kCommandRescanDevices,
+        kCommandEnableExperimentalMappings,
+        // Window
+        kCommandShowControlWindow,
+        kCommandShowKeyboardWindow
+    };
+    
+    commands.addArray (ids, numElementsInArray(ids));
+}
+
+// This method is used when something needs to find out the details about one of the commands
+// that this object can perform..
+void MainWindow::getCommandInfo(CommandID commandID, ApplicationCommandInfo& result) {
+    const String presetsCategory("Presets");
+    const String editsCategory("Editing");
+    const String controlCategory("Control");
+    const String windowCategory("Window");
+    
+    switch (commandID) {
+            
+        // *** File Menu ***
+        case kCommandNewPreset:
+            result.setInfo("New Preset", "Clears the current settings", presetsCategory, 0);
+            result.setTicked(false);
+            result.setActive(false);
+            result.addDefaultKeypress ('N', ModifierKeys::commandModifier);
+            break;
+        case kCommandOpenPreset:
+            result.setInfo("Open Preset...", "Opens an existing preset", presetsCategory, 0);
+            result.setTicked(false);
+            result.setActive(false);
+            result.addDefaultKeypress ('O', ModifierKeys::commandModifier);
+            break;
+        case kCommandSavePreset:
+            result.setInfo("Save Preset", "Saves the current preset", presetsCategory, 0);
+            result.setTicked(false);
+            result.setActive(false);
+            result.addDefaultKeypress ('S', ModifierKeys::commandModifier);
+            break;
+        case kCommandSavePresetAs:
+            result.setInfo("Save Preset As...", "Saves the current preset with a new name", presetsCategory, 0);
+            result.setTicked (false);
+            result.setActive(false);
+            break;
+        // Quit command is handled by JuceApplication
+            
+        // *** Edit Menu ***
+        case StandardApplicationCommandIDs::undo:
+            result.setInfo("Undo", "Undo", editsCategory, 0);
+            result.setTicked(false);
+            result.setActive(false);
+            result.addDefaultKeypress ('Z', ModifierKeys::commandModifier);
+            break;
+        case StandardApplicationCommandIDs::redo:
+            result.setInfo("Redo", "Redo", editsCategory, 0);
+            result.setTicked(false);
+            result.setActive(false);
+            result.addDefaultKeypress ('Z', ModifierKeys::commandModifier | ModifierKeys::shiftModifier);
+            break;
+        case StandardApplicationCommandIDs::cut:
+            result.setInfo("Cut", "Cut", editsCategory, 0);
+            result.setTicked(false);
+            result.setActive(false);
+            result.addDefaultKeypress ('X', ModifierKeys::commandModifier);
+            break;
+        case StandardApplicationCommandIDs::copy:
+            result.setInfo("Copy", "Copy", editsCategory, 0);
+            result.setTicked(false);
+            result.setActive(false);
+            result.addDefaultKeypress ('C', ModifierKeys::commandModifier);
+            break;
+        case StandardApplicationCommandIDs::paste:
+            result.setInfo("Paste", "Paste", editsCategory, 0);
+            result.setTicked(false);
+            result.setActive(false);
+            result.addDefaultKeypress ('V', ModifierKeys::commandModifier);
+            break;
+        case StandardApplicationCommandIDs::del:
+            result.setInfo("Delete", "Delete", editsCategory, 0);
+            result.setTicked(false);
+            result.setActive(false);
+            break;
+        case StandardApplicationCommandIDs::selectAll:
+            result.setInfo("Select All", "Select All", editsCategory, 0);
+            result.setTicked(false);
+            result.setActive(false);
+            result.addDefaultKeypress ('A', ModifierKeys::commandModifier);
+            break;
+            
+        // *** Control Menu ***
+        case kCommandRescanDevices:
+            result.setInfo("Rescan Devices", "Rescans available TouchKeys and MIDI devices", controlCategory, 0);
+            result.setTicked(false);
+            result.setActive(false);
+            result.addDefaultKeypress ('R', ModifierKeys::commandModifier);
+            break;
+        case kCommandEnableExperimentalMappings:
+            result.setInfo("Enable Experimental Mappings", "Enables mappings which are still experimental", controlCategory, 0);
+            result.setTicked(controller_.experimentalMappingsEnabled());
+            break;
+            
+        // *** Window Menu ***
+        case kCommandShowControlWindow:
+            result.setInfo("TouchKeys Controls", "Show control and mapping window", windowCategory, 0);
+            result.setTicked(false);
+            break;
+        case kCommandShowKeyboardWindow:
+            result.setInfo("Keyboard Display", "Show keyboard display", windowCategory, 0);
+            result.setTicked(false);
+            break;
+            
+        default:
+            break;
+    };
+}
+
+bool MainWindow::perform(const InvocationInfo& info) {
+    switch (info.commandID)
+    {
+        case kCommandNewPreset:            
+            break;
+        case kCommandRescanDevices:
+            std::cout << "Rescan\n";
+            break;
+        case kCommandEnableExperimentalMappings:
+            controller_.setExperimentalMappingsEnabled(!controller_.experimentalMappingsEnabled());
+            break;
+        case kCommandShowControlWindow:
+            toFront(true);
+            break;
+        case kCommandShowKeyboardWindow:
+            controller_.showKeyboardDisplayWindow();
+            break;
+
+        default:
+            return false;
+    };
+    
+    return true;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/GUI/MainWindow.h	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,115 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+ 
+  MainWindow.h: the control window, plus menu bar and Juce application methods
+*/
+
+#ifndef MAINWINDOW_H_INCLUDED
+#define MAINWINDOW_H_INCLUDED
+
+#include "../../JuceLibraryCode/JuceHeader.h"
+#include "ControlWindowMainComponent.h"
+#include "../MainApplicationController.h"
+
+//==============================================================================
+/*
+ */
+
+class MainWindow    : public DocumentWindow, public Timer,
+                      public MenuBarModel, public ApplicationCommandTarget
+{
+private:
+    // Commands this application responds to
+    enum CommandIDs
+    {
+        // File menu
+        kCommandNewPreset    = 0x2001,
+        kCommandOpenPreset,
+        kCommandSavePreset,
+        kCommandSavePresetAs,
+        
+        // Edit menu
+        // (all standard)
+        
+        // Control menu
+        kCommandRescanDevices = 0x2020,
+        kCommandEnableExperimentalMappings,
+        
+        // Window menu
+        kCommandShowControlWindow = 0x2030,
+        kCommandShowKeyboardWindow
+    };
+    
+public:
+    MainWindow(MainApplicationController& controller);
+    ~MainWindow();
+    
+    void closeButtonPressed()
+    {
+        // This is called when the user tries to close this window. Here, we'll just
+        // ask the app to quit when this happens, but you can change this to do
+        // whatever you need.
+        JUCEApplication::getInstance()->systemRequestedQuit();
+    }
+    
+    // Method used by Juce timer which we will use for periodic UI updates
+    // from the underlying system state, in a configuration similar to how the
+    // Juce audio plugins work
+    void timerCallback();
+    
+    // ***** Menu Bar methods *****
+    
+    StringArray getMenuBarNames();
+    PopupMenu getMenuForIndex (int menuIndex, const String& menuName);
+    void menuItemSelected (int menuItemID, int topLevelMenuIndex);
+    
+    // ***** Application Command Manager methods *****
+    
+    // this will return the next parent component that is an ApplicationCommandTarget (in this
+    // case, there probably isn't one, but it's best to use this method in your own apps).
+    ApplicationCommandTarget* getNextCommandTarget();
+    
+    // this returns the set of all commands that this target can perform..
+    void getAllCommands (Array <CommandID>& commands);
+    
+    // This method is used when something needs to find out the details about one of the commands
+    // that this object can perform..
+    void getCommandInfo (CommandID commandID, ApplicationCommandInfo& result);
+    
+    // Perform a command
+    bool perform (const InvocationInfo& info);
+    
+    /* Note: Be careful if you override any DocumentWindow methods - the base
+     class uses a lot of them, so by overriding you might break its functionality.
+     It's best to do all your work in your content component instead, but if
+     you really have to override any DocumentWindow methods, make sure your
+     subclass also calls the superclass's method.
+     */
+    
+private:
+    // The command manager object used to dispatch command events
+    MainApplicationController& controller_;
+    ApplicationCommandManager commandManager_;    
+    ControlWindowMainComponent mainComponent_;
+
+    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainWindow)
+};
+
+
+#endif  // MAINWINDOW_H_INCLUDED
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/GUI/MappingEditorComponent.h	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,83 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+
+  MappingEditorComponent.h: Juce Component subclass from which all
+  mapping editors inherit.
+*/
+
+
+#ifndef __MAPPINGEDITORCOMPONENT_H_C93E7296__
+#define __MAPPINGEDITORCOMPONENT_H_C93E7296__
+
+#include "../../JuceLibraryCode/JuceHeader.h"
+
+//==============================================================================
+/*
+*/
+class MappingEditorComponent    : public Component
+{
+public:
+    MappingEditorComponent()
+    {
+        // In your constructor, you should add any child components, and
+        // initialise any special settings that your component needs.
+
+    }
+
+    virtual ~MappingEditorComponent()
+    {
+    }
+    
+    // Method to synchronize the GUI state to the underlying
+    // state of the mapping. Implemented in the subclass.
+    virtual void synchronize() {}
+
+    virtual void paint (Graphics& g)
+    {
+        /* This demo code just fills the component's background and
+           draws some placeholder text to get you started.
+
+           You should replace everything in this method with your own
+           drawing code..
+        */
+
+        g.fillAll (Colours::white);   // clear the background
+
+        g.setColour (Colours::grey);
+        g.drawRect (getLocalBounds(), 1);   // draw an outline around the component
+
+        g.setColour (Colours::lightblue);
+        g.setFont (14.0f);
+        g.drawText ("MappingEditorComponent", getLocalBounds(),
+                    Justification::centred, true);   // draw some placeholder text
+    }
+
+    virtual void resized()
+    {
+        // This method is where you should set the bounds of any child
+        // components that your component contains..
+
+    }
+
+private:
+    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MappingEditorComponent)
+};
+
+
+#endif  // __MAPPINGEDITORCOMPONENT_H_C93E7296__
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/GUI/MappingListComponent.cpp	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,145 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+
+  MappingListComponent.cpp: manages a ListBox to display the current
+  mappigns associated with a keyboard segment.
+*/
+
+#ifndef TOUCHKEYS_NO_GUI
+
+#include "../JuceLibraryCode/JuceHeader.h"
+#include "MappingListComponent.h"
+
+//==============================================================================
+MappingListComponent::MappingListComponent() : controller_(0), keyboardSegment_(0),
+  lastMappingFactoryIdentifier_(-1) {
+    // In your constructor, you should add any child components, and
+    // initialise any special settings that your component needs.
+    addAndMakeVisible(&listBox_);
+    listBox_.setModel(this);
+    listBox_.setColour(ListBox::outlineColourId, Colours::grey);
+    listBox_.setOutlineThickness(1);
+    listBox_.setRowHeight(72);
+}
+
+MappingListComponent::~MappingListComponent() {}
+
+#if 0
+void MappingListComponent::paint (Graphics& g) {
+    /* This demo code just fills the component's background and
+       draws some placeholder text to get you started.
+
+       You should replace everything in this method with your own
+       drawing code..
+    */
+
+    g.fillAll (Colours::white);   // clear the background
+
+    g.setColour (Colours::grey);
+    g.drawRect (getLocalBounds(), 1);   // draw an outline around the component
+
+    g.setColour (Colours::lightblue);
+    g.setFont (14.0f);
+    g.drawText ("MappingListComponent", getLocalBounds(),
+                Justification::centred, true);   // draw some placeholder text
+}
+#endif
+
+void MappingListComponent::resized() {
+    // This method is where you should set the bounds of any child
+    // components that your component contains..
+    listBox_.setBoundsInset (BorderSize<int> (4));
+}
+
+// Delete the given factory from the mapping list
+void MappingListComponent::deleteMapping(MappingFactory* factory) {
+    if(keyboardSegment_ == 0 || factory == 0)
+        return;
+    keyboardSegment_->removeMappingFactory(factory);
+}
+
+int MappingListComponent::getNumRows() {
+    if(controller_ == 0 || keyboardSegment_ == 0)
+        return 0;
+    return keyboardSegment_->mappingFactories().size();
+}
+
+// Given a row number and a possibly an existing component, return the component
+// that should be drawn in this row of the list. Whenever a new component is created,
+// the existing one should be deleted by this method (according to Juce docs).
+Component* MappingListComponent::refreshComponentForRow(int rowNumber, bool isRowSelected, Component *existingComponentToUpdate) {
+    if(keyboardSegment_ == 0)
+        return 0;
+
+    //std::cout << "refreshing component for row " << rowNumber << " (given " << existingComponentToUpdate << ")\n";
+    if(rowNumber < 0 || rowNumber >= getNumRows()) {
+        if(existingComponentToUpdate != 0)
+            delete existingComponentToUpdate;
+        return 0;
+    }
+    
+    // Get the current component for the row, creating it if it doesn't exist
+    MappingListItem *listItem = static_cast<MappingListItem*>(existingComponentToUpdate);
+    if(listItem == 0) {
+        listItem = new MappingListItem(*this);
+        listItem->setMappingFactory(keyboardSegment_->mappingFactories()[rowNumber]);
+        //std::cout << "item " << listItem << " was updated to factory " << keyboardSegment_->mappingFactories()[rowNumber] << std::endl;
+    }
+    else {
+        // Component exists; does it still point to a factory?
+        if(rowNumber >= keyboardSegment_->mappingFactories().size()) {
+            //std::cout << "Deleting component " << listItem << std::endl;
+            delete listItem;
+            return 0;
+        }
+        else if(keyboardSegment_->mappingFactories()[rowNumber] != listItem->mappingFactory()) {
+            //std::cout << "Changing item " << listItem << " to point to factory " << keyboardSegment_->mappingFactories()[rowNumber] << std::endl;
+            listItem->setMappingFactory(keyboardSegment_->mappingFactories()[rowNumber]);
+        }
+    }
+    
+    return listItem;
+}
+
+// Return whether a given component is selected or not (called by MappingListItem)
+bool MappingListComponent::isComponentSelected(Component *component) {
+    int rowNumber = listBox_.getRowNumberOfComponent(component);
+    if(rowNumber < 0)
+        return false;
+    return listBox_.isRowSelected(rowNumber);
+}
+
+
+void MappingListComponent::synchronize()
+{
+    if(keyboardSegment_ != 0) {
+        if(lastMappingFactoryIdentifier_ != keyboardSegment_->mappingFactoryUniqueIdentifier()) {
+            lastMappingFactoryIdentifier_ = keyboardSegment_->mappingFactoryUniqueIdentifier();
+            listBox_.updateContent();
+        }
+    }
+    
+    for(int i = 0; i < getNumRows(); i++) {
+        MappingListItem *listItem = static_cast<MappingListItem*>(listBox_.getComponentForRowNumber(i));
+        if(listItem != 0)
+            listItem->synchronize();
+    }
+}
+
+#endif  // TOUCHKEYS_NO_GUI
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/GUI/MappingListComponent.h	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,94 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+
+  MappingListComponent.h: manages a ListBox to display the current
+  mappigns associated with a keyboard segment.
+*/
+
+#ifndef TOUCHKEYS_NO_GUI
+
+#ifndef __MAPPINGLISTCOMPONENT_H_51502151__
+#define __MAPPINGLISTCOMPONENT_H_51502151__
+
+#include <vector>
+#include "../JuceLibraryCode/JuceHeader.h"
+#include "../MainApplicationController.h"
+#include "../TouchKeys/MidiKeyboardSegment.h"
+#include "MappingListItem.h"
+#include "../Mappings/MappingFactory.h"
+
+//==============================================================================
+/*
+*/
+class MappingListComponent    : public Component, public ListBoxModel
+{
+public:
+    MappingListComponent();
+    ~MappingListComponent();
+
+    //void paint (Graphics&);
+    void resized();
+    
+    // *** Mapping management methods ***
+    
+    // Attach the user interface to the controller and vice-versa
+    void setMainApplicationController(MainApplicationController *controller) {
+        controller_ = controller;
+        if(keyboardSegment_ != 0)
+            listBox_.updateContent();
+    }
+    void setKeyboardSegment(MidiKeyboardSegment *segment) {
+        keyboardSegment_ = segment;
+        if(controller_ != 0)
+            listBox_.updateContent();
+    }
+    
+    // Add or delete a mapping based on a Factory class created elsewhere
+    void addMapping(MappingFactory* factory);
+    void deleteMapping(MappingFactory* factory);
+
+    // *** ListBox methods ***
+    int getNumRows();
+    void paintListBoxItem(int rowNumber,
+                          Graphics& g,
+                          int width, int height,
+                          bool rowIsSelected) {}
+    Component* refreshComponentForRow(int rowNumber, bool isRowSelected, Component *existingComponentToUpdate);
+
+    // *** UI management methods ***
+    // Return whether a given component is selected or not (called by MappingListItem)
+    bool isComponentSelected(Component *component);
+    
+    // Update UI state to reflect underlying system state
+    void synchronize();
+    
+private:
+    ListBox listBox_;
+    MainApplicationController *controller_;
+    MidiKeyboardSegment *keyboardSegment_;
+    
+    int lastMappingFactoryIdentifier_;
+    
+    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MappingListComponent)
+};
+
+
+#endif  // __MAPPINGLISTCOMPONENT_H_51502151__
+
+#endif  // TOUCHKEYS_NO_GUI
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/GUI/MappingListItem.cpp	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,302 @@
+/*
+  ==============================================================================
+
+  This is an automatically generated GUI class created by the Introjucer!
+
+  Be careful when adding custom code to these files, as only the code within
+  the "//[xyz]" and "//[/xyz]" sections will be retained when the file is loaded
+  and re-saved.
+
+  Created with Introjucer version: 3.1.0
+
+  ------------------------------------------------------------------------------
+
+  The Introjucer is part of the JUCE library - "Jules' Utility Class Extensions"
+  Copyright 2004-13 by Raw Material Software Ltd.
+
+  ==============================================================================
+*/
+
+//[Headers] You can add your own extra header files here...
+#ifndef TOUCHKEYS_NO_GUI
+#include "MappingListComponent.h"
+//[/Headers]
+
+#include "MappingListItem.h"
+
+
+//[MiscUserDefs] You can add your own user definitions and misc code here...
+//[/MiscUserDefs]
+
+//==============================================================================
+MappingListItem::MappingListItem (MappingListComponent& listComponent)
+    : factory_(0), listComponent_(listComponent)
+{
+    addAndMakeVisible (bypassToggleButton = new ToggleButton ("Bypass toggle button"));
+    bypassToggleButton->setButtonText ("Bypass");
+    bypassToggleButton->addListener (this);
+
+    addAndMakeVisible (showDetailsButton = new TextButton ("Show details button"));
+    showDetailsButton->setButtonText ("Details...");
+    showDetailsButton->addListener (this);
+
+    addAndMakeVisible (mappingTypeLabel = new Label ("mapping type label",
+                                                     "MappingType"));
+    mappingTypeLabel->setFont (Font (18.00f, Font::plain));
+    mappingTypeLabel->setJustificationType (Justification::centred);
+    mappingTypeLabel->setEditable (false, false, false);
+    mappingTypeLabel->setColour (TextEditor::textColourId, Colours::black);
+    mappingTypeLabel->setColour (TextEditor::backgroundColourId, Colour (0x00000000));
+
+    addAndMakeVisible (mappingShortEditorComponent = new MappingEditorComponent());
+    mappingShortEditorComponent->setName ("mapping short editor component");
+
+    addAndMakeVisible (noSettingsLabel = new Label ("no settings label",
+                                                    "(no settings)"));
+    noSettingsLabel->setFont (Font (15.00f, Font::plain));
+    noSettingsLabel->setJustificationType (Justification::centred);
+    noSettingsLabel->setEditable (false, false, false);
+    noSettingsLabel->setColour (TextEditor::textColourId, Colours::black);
+    noSettingsLabel->setColour (TextEditor::backgroundColourId, Colour (0x00000000));
+
+    addAndMakeVisible (deleteButton = new TextButton ("delete button"));
+    deleteButton->setButtonText ("Delete...");
+    deleteButton->addListener (this);
+
+
+    //[UserPreSize]
+    //[/UserPreSize]
+
+    setSize (544, 72);
+
+
+    //[Constructor] You can add your own custom stuff here..
+    //[/Constructor]
+}
+
+MappingListItem::~MappingListItem()
+{
+    //[Destructor_pre]. You can add your own custom destruction code here..
+    //[/Destructor_pre]
+
+    bypassToggleButton = nullptr;
+    showDetailsButton = nullptr;
+    mappingTypeLabel = nullptr;
+    mappingShortEditorComponent = nullptr;
+    noSettingsLabel = nullptr;
+    deleteButton = nullptr;
+
+
+    //[Destructor]. You can add your own custom destruction code here..
+    //[/Destructor]
+}
+
+//==============================================================================
+void MappingListItem::paint (Graphics& g)
+{
+    //[UserPrePaint] Add your own custom painting code here..
+    //[/UserPrePaint]
+
+    g.fillAll (Colours::white);
+
+    g.setColour (Colour (0xffa52a60));
+    g.fillPath (internalPath1);
+    g.setColour (Colours::black);
+    g.strokePath (internalPath1, PathStrokeType (1.000f));
+
+    g.setColour (Colour (0xffa52a94));
+    g.fillPath (internalPath2);
+    g.setColour (Colours::black);
+    g.strokePath (internalPath2, PathStrokeType (0.500f));
+
+    //[UserPaint] Add your own custom painting code here..
+    /*MappingListComponent *parent = static_cast<MappingListComponent*>(getParentComponent());
+    if(parent->isComponentSelected(this)) {
+        g.setColour (Colours::lightblue);
+        g.drawRect (0, 0, 544, 72, 5);
+    }*/
+    //[/UserPaint]
+}
+
+void MappingListItem::resized()
+{
+    bypassToggleButton->setBounds (24, 44, 72, 24);
+    showDetailsButton->setBounds (456, 8, 80, 24);
+    mappingTypeLabel->setBounds (8, 4, 104, 40);
+    mappingShortEditorComponent->setBounds (120, 0, 328, 71);
+    noSettingsLabel->setBounds (208, 24, 150, 24);
+    deleteButton->setBounds (456, 44, 80, 20);
+    internalPath1.clear();
+    internalPath1.startNewSubPath (544.0f, 72.0f);
+    internalPath1.lineTo (0.0f, 72.0f);
+    internalPath1.closeSubPath();
+
+    internalPath2.clear();
+    internalPath2.startNewSubPath (119.0f, 16.0f);
+    internalPath2.lineTo (119.0f, 56.0f);
+    internalPath2.closeSubPath();
+
+    //[UserResized] Add your own custom resize handling here..
+    //[/UserResized]
+}
+
+void MappingListItem::buttonClicked (Button* buttonThatWasClicked)
+{
+    //[UserbuttonClicked_Pre]
+    if(factory_ == 0)
+        return;
+    //[/UserbuttonClicked_Pre]
+
+    if (buttonThatWasClicked == bypassToggleButton)
+    {
+        //[UserButtonCode_bypassToggleButton] -- add your button handler code here..
+        bool bypass = bypassToggleButton->getToggleState();
+        factory_->setBypassed(bypass);
+        //[/UserButtonCode_bypassToggleButton]
+    }
+    else if (buttonThatWasClicked == showDetailsButton)
+    {
+        //[UserButtonCode_showDetailsButton] -- add your button handler code here..
+        //[/UserButtonCode_showDetailsButton]
+    }
+    else if (buttonThatWasClicked == deleteButton)
+    {
+        //[UserButtonCode_deleteButton] -- add your button handler code here..
+        // Display an alert to confirm the user wants to delete this mapping
+        AlertWindow::showOkCancelBox (AlertWindow::QuestionIcon,
+                                      "Delete mapping",
+                                      "Are you sure you want to delete this mapping?",
+                                      String::empty,
+                                      String::empty,
+                                      0,
+                                      ModalCallbackFunction::forComponent (alertBoxResultChosen, this));
+        //[/UserButtonCode_deleteButton]
+    }
+
+    //[UserbuttonClicked_Post]
+    //[/UserbuttonClicked_Post]
+}
+
+
+
+//[MiscUserCode] You can add your own definitions of your custom methods or any other code here...
+
+// Called when user clicks a result in the alert box to confirm deletion
+void MappingListItem::alertBoxResultChosen(int result, MappingListItem *item)
+{
+    if(result != 0) {
+        item->deleteMapping();
+    }
+}
+
+// Delete this mapping factory
+void MappingListItem::deleteMapping()
+{
+    listComponent_.deleteMapping(factory_);
+}
+
+// Set the mapping factory and create any editor components it uses
+void MappingListItem::setMappingFactory(MappingFactory *factory)
+{
+    factory_ = factory;
+
+    if(factory_->hasBasicEditor()) {
+        // Has a short editor: make one and add it to the window, using the same bounds
+        // as before
+        const Rectangle<int>& bounds = mappingShortEditorComponent->getBounds();
+        mappingShortEditorComponent = factory_->createBasicEditor();
+        addAndMakeVisible(mappingShortEditorComponent);
+        mappingShortEditorComponent->setBounds(bounds);
+        noSettingsLabel->setVisible(false);
+    }
+    else {
+        noSettingsLabel->setVisible(true);
+        mappingShortEditorComponent->setVisible(false);
+    }
+
+    if(factory_->hasExtendedEditor()) {
+        // Has an extended editor: make one and keep it around for adding to a new window
+        mappingLongEditorComponent = factory_->createExtendedEditor();
+        showDetailsButton->setEnabled(true);
+    }
+    else {
+        mappingLongEditorComponent = nullptr;
+        showDetailsButton->setEnabled(false);
+    }
+
+    synchronize();
+}
+
+void MappingListItem::synchronize()
+{
+    if(factory_ == 0)
+        return;
+
+    // Update the label and the bypass button
+    mappingTypeLabel->setText(factory_->factoryTypeName().c_str(), dontSendNotification);
+    if(factory_->bypassed() != MappingFactory::kBypassOff)
+        bypassToggleButton->setToggleState(true, dontSendNotification);
+    else
+        bypassToggleButton->setToggleState(false, dontSendNotification);
+
+    // Update the short and long components if present
+    if(mappingShortEditorComponent != 0)
+        mappingShortEditorComponent->synchronize();
+    if(mappingLongEditorComponent != 0)
+        mappingLongEditorComponent->synchronize();
+}
+//[/MiscUserCode]
+
+
+//==============================================================================
+#if 0
+/*  -- Introjucer information section --
+
+    This is where the Introjucer stores the metadata that describe this GUI layout, so
+    make changes in here at your peril!
+
+BEGIN_JUCER_METADATA
+
+<JUCER_COMPONENT documentType="Component" className="MappingListItem" componentName=""
+                 parentClasses="public Component" constructorParams="MappingListComponent&amp; listComponent"
+                 variableInitialisers="factory_(0), listComponent_(listComponent)"
+                 snapPixels="8" snapActive="1" snapShown="1" overlayOpacity="0.330"
+                 fixedSize="1" initialWidth="544" initialHeight="72">
+  <BACKGROUND backgroundColour="ffffffff">
+    <PATH pos="0 0 100 100" fill="solid: ffa52a60" hasStroke="1" stroke="1, mitered, butt"
+          strokeColour="solid: ff000000" nonZeroWinding="1">s 544 72 l 0 72 x</PATH>
+    <PATH pos="0 0 100 100" fill="solid: ffa52a94" hasStroke="1" stroke="0.5, mitered, butt"
+          strokeColour="solid: ff000000" nonZeroWinding="1">s 119 16 l 119 56 x</PATH>
+  </BACKGROUND>
+  <TOGGLEBUTTON name="Bypass toggle button" id="cfe71c39a64f4704" memberName="bypassToggleButton"
+                virtualName="" explicitFocusOrder="0" pos="24 44 72 24" buttonText="Bypass"
+                connectedEdges="0" needsCallback="1" radioGroupId="0" state="0"/>
+  <TEXTBUTTON name="Show details button" id="17ac5d15223ada90" memberName="showDetailsButton"
+              virtualName="" explicitFocusOrder="0" pos="456 8 80 24" buttonText="Details..."
+              connectedEdges="0" needsCallback="1" radioGroupId="0"/>
+  <LABEL name="mapping type label" id="58b75e1d781dd4c6" memberName="mappingTypeLabel"
+         virtualName="" explicitFocusOrder="0" pos="8 4 104 40" edTextCol="ff000000"
+         edBkgCol="0" labelText="MappingType" editableSingleClick="0"
+         editableDoubleClick="0" focusDiscardsChanges="0" fontname="Default font"
+         fontsize="18" bold="0" italic="0" justification="36"/>
+  <GENERICCOMPONENT name="mapping short editor component" id="8cbc2e53072fcaa7" memberName="mappingShortEditorComponent"
+                    virtualName="" explicitFocusOrder="0" pos="120 0 328 71" class="MappingEditorComponent"
+                    params=""/>
+  <LABEL name="no settings label" id="a8fb2694ebf4280b" memberName="noSettingsLabel"
+         virtualName="" explicitFocusOrder="0" pos="208 24 150 24" edTextCol="ff000000"
+         edBkgCol="0" labelText="(no settings)" editableSingleClick="0"
+         editableDoubleClick="0" focusDiscardsChanges="0" fontname="Default font"
+         fontsize="15" bold="0" italic="0" justification="36"/>
+  <TEXTBUTTON name="delete button" id="fced502f19d4fe5b" memberName="deleteButton"
+              virtualName="" explicitFocusOrder="0" pos="456 44 80 20" buttonText="Delete..."
+              connectedEdges="0" needsCallback="1" radioGroupId="0"/>
+</JUCER_COMPONENT>
+
+END_JUCER_METADATA
+*/
+#endif
+
+
+//[EndFile] You can add extra defines here...
+#endif // TOUCHKEYS_NO_GUI
+//[/EndFile]
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/GUI/MappingListItem.h	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,93 @@
+/*
+  ==============================================================================
+
+  This is an automatically generated GUI class created by the Introjucer!
+
+  Be careful when adding custom code to these files, as only the code within
+  the "//[xyz]" and "//[/xyz]" sections will be retained when the file is loaded
+  and re-saved.
+
+  Created with Introjucer version: 3.1.0
+
+  ------------------------------------------------------------------------------
+
+  The Introjucer is part of the JUCE library - "Jules' Utility Class Extensions"
+  Copyright 2004-13 by Raw Material Software Ltd.
+
+  ==============================================================================
+*/
+
+#ifndef __JUCE_HEADER_8369B097DBF9195A__
+#define __JUCE_HEADER_8369B097DBF9195A__
+
+//[Headers]     -- You can add your own extra header files here --
+#ifndef TOUCHKEYS_NO_GUI
+
+#include "JuceHeader.h"
+#include "../Mappings/MappingFactory.h"
+
+class MappingListComponent;
+//[/Headers]
+
+
+
+//==============================================================================
+/**
+                                                                    //[Comments]
+    An auto-generated component, created by the Introjucer.
+
+    Describe your class and how it works here!
+                                                                    //[/Comments]
+*/
+class MappingListItem  : public Component,
+                         public ButtonListener
+{
+public:
+    //==============================================================================
+    MappingListItem (MappingListComponent& listComponent);
+    ~MappingListItem();
+
+    //==============================================================================
+    //[UserMethods]     -- You can add your own custom methods in this section.
+    static void alertBoxResultChosen(int result, MappingListItem *item);
+    void deleteMapping();
+
+    MappingFactory* mappingFactory() { return factory_; }
+    void setMappingFactory(MappingFactory *factory);
+    void synchronize();
+    //[/UserMethods]
+
+    void paint (Graphics& g);
+    void resized();
+    void buttonClicked (Button* buttonThatWasClicked);
+
+
+
+private:
+    //[UserVariables]   -- You can add your own custom variables in this section.
+    MappingFactory *factory_;
+    MappingListComponent& listComponent_;
+
+    ScopedPointer<MappingEditorComponent> mappingLongEditorComponent;
+    //[/UserVariables]
+
+    //==============================================================================
+    ScopedPointer<ToggleButton> bypassToggleButton;
+    ScopedPointer<TextButton> showDetailsButton;
+    ScopedPointer<Label> mappingTypeLabel;
+    ScopedPointer<MappingEditorComponent> mappingShortEditorComponent;
+    ScopedPointer<Label> noSettingsLabel;
+    ScopedPointer<TextButton> deleteButton;
+    Path internalPath1;
+    Path internalPath2;
+
+
+    //==============================================================================
+    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MappingListItem)
+};
+
+//[EndFile] You can add extra defines here...
+#endif // TOUCHKEYS_NO_GUI
+//[/EndFile]
+
+#endif   // __JUCE_HEADER_8369B097DBF9195A__
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/Main.cpp	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,258 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+ 
+  Main.cpp: main startup routines, connecting to Juce library
+*/
+
+#include "../JuceLibraryCode/JuceHeader.h"
+#include "MainApplicationController.h"
+
+#ifndef TOUCHKEYS_NO_GUI
+#include "GUI/MainWindow.h"
+#include "GUI/GraphicsDisplayWindow.h"
+#include "Display/OpenGLJuceCanvas.h"
+
+//==============================================================================
+class TouchKeysApplication  : public JUCEApplication
+{
+public:
+    //==============================================================================
+    TouchKeysApplication() {}
+
+    const String getApplicationName()       { return ProjectInfo::projectName; }
+    const String getApplicationVersion()    { return ProjectInfo::versionString; }
+    bool moreThanOneInstanceAllowed()       { return true; }
+
+    //==============================================================================
+    void initialise (const String& commandLine) {
+        // This method is where you should put your application's initialisation code..
+
+        mainWindow_ = new MainWindow(controller_);
+        keyboardDisplayWindow_ = new GraphicsDisplayWindow("TouchKeys Display", controller_);
+        
+        controller_.setKeyboardDisplayWindow(keyboardDisplayWindow_);
+    }
+
+    void shutdown() {
+        // Add your application's shutdown code here..
+        mainWindow_ = nullptr; // (deletes our window)
+        
+        controller_.setKeyboardDisplayWindow(0);    // Delete display window and disconnect from controller
+        keyboardDisplayWindow_ = nullptr;
+    }
+
+    //==============================================================================
+    void systemRequestedQuit() {
+        // This is called when the app is being asked to quit: you can ignore this
+        // request and let the app carry on running, or call quit() to allow the app to close.
+        quit();
+    }
+
+    void anotherInstanceStarted (const String& commandLine) {
+        // When another instance of the app is launched while this one is running,
+        // this method is invoked, and the commandLine parameter tells you what
+        // the other instance's command-line arguments were.
+    }
+
+private:
+    ScopedPointer<MainWindow> mainWindow_;
+    ScopedPointer<GraphicsDisplayWindow> keyboardDisplayWindow_;
+    MainApplicationController controller_;
+};
+
+//==============================================================================
+// This macro generates the main() routine that launches the app.
+START_JUCE_APPLICATION (TouchKeysApplication)
+
+#else // TOUCHKEYS_NO_GUI
+
+#include <getopt.h>
+#include <libgen.h>
+#include <signal.h>
+#include <stdlib.h>
+#include <stdio.h>
+
+bool programShouldStop_ = false;
+
+static struct option long_options[] = {
+	{"help", no_argument, NULL, 'h'},
+	{"list", no_argument, NULL, 'l'},
+	{"touchkeys", required_argument, NULL, 't'},
+    {"midi-input", required_argument, NULL, 'i'},
+    {"midi-output", required_argument, NULL, 'o'},
+    {"virtual-midi-output", no_argument, NULL, 'O'},
+	{0,0,0,0}
+};
+
+void sigint_handler(int s){
+    programShouldStop_ = true;
+}
+
+void usage(const char * processName)	// Print usage information and exit
+{
+	cerr << "Usage: " << processName << " [-h] [-l] [-t touchkeys] [-i MIDI-in] [-o MIDI-out]\n";
+	cerr << "  -h:   Print this menu\n";
+	cerr << "  -l:   List available TouchKeys and MIDI devices\n";
+	cerr << "  -t:   Specify TouchKeys device path\n";
+    cerr << "  -i:   Specify MIDI input device\n";
+    cerr << "  -o:   Specify MIDI output device\n";
+    cerr << "  -O:   Open virtual MIDI output\n";
+}
+
+void list_devices(MainApplicationController& controller)
+{
+    std::vector<std::string> touchkeysDevices(controller.availableTouchkeyDevices());
+    std::vector<std::pair<int, std::string> > midiInputDevices(controller.availableMIDIInputDevices());
+    std::vector<std::pair<int, std::string> > midiOutputDevices(controller.availableMIDIOutputDevices());
+    
+    cerr << "TouchKeys devices: \n";
+    if(touchkeysDevices.empty())
+        cerr << "  [none found]\n";
+    else {
+        for(std::vector<std::string>::iterator it = touchkeysDevices.begin(); it != touchkeysDevices.end(); ++it) {
+            cerr << "  /dev/" << *it << "\n";
+        }
+    }
+
+    cerr << "\nMIDI input devices: \n";
+    if(midiInputDevices.empty())
+        cerr << "  [none found]\n";
+    else {
+        for(std::vector<std::pair<int, std::string> >::iterator it = midiInputDevices.begin();
+            it != midiInputDevices.end();
+            ++it) {
+            cerr << "  " << it->first << ": " << it->second << "\n";
+        }
+    }
+    
+    cerr << "\nMIDI output devices: \n";
+    if(midiOutputDevices.empty())
+        cerr << "  [none found]\n";
+    else {
+        for(std::vector<std::pair<int, std::string> >::iterator it = midiOutputDevices.begin();
+            it != midiOutputDevices.end();
+            ++it) {
+            cerr << "  " << it->first << ": " << it->second << "\n";
+        }
+    }
+}
+
+int main (int argc, char* argv[])
+{
+    MainApplicationController controller;
+    int ch, option_index;
+    int midiInputNum = 0, midiOutputNum = 0;
+    bool useVirtualMidiOutput = false;
+    bool shouldStart = true;
+    string touchkeysDevicePath;
+    
+	while((ch = getopt_long(argc, argv, "hli:o:t:O", long_options, &option_index)) != -1)
+	{
+        if(ch == 'l') { // List devices
+            list_devices(controller);
+            shouldStart = false;
+            break;
+        }
+        else if(ch == 't') { // TouchKeys device
+            touchkeysDevicePath = optarg;
+        }
+        else if(ch == 'i') { // MIDI input device
+            midiInputNum = atoi(optarg);
+        }
+        else if(ch == 'o') { // MIDI output device
+            midiOutputNum = atoi(optarg);
+        }
+        else if(ch == 'O') { // Virtual MIDI output
+            useVirtualMidiOutput = true;
+        }
+        else {
+            usage(basename(argv[0]));
+            shouldStart = false;
+            break;
+		}
+	}
+    
+    if(shouldStart) {
+        // Main initialization: open TouchKeys and MIDI devices
+        try {
+            // Check whether TouchKeys device was specified. If not, take the first available one.
+            /*if(touchkeysDevicePath == "") {
+                std::vector<std::string> touchkeysDevices = controller.availableTouchkeyDevices();
+                
+                if(touchkeysDevices.empty()) {
+                    cout << "No TouchKeys devices found. Check that the TouchKeys are connected.\n";
+                    throw new exception;
+                }
+                else {
+                    touchkeysDevicePath = "/dev/";
+                    touchkeysDevicePath.append(touchkeysDevices[0]);
+                }
+            }*/
+            
+            // Open MIDI devices
+            //cout << "Opening MIDI input device " << midiInputNum << endl;
+            //controller.enableMIDIInputPort(midiInputNum);
+
+            // TODO: enable multiple keyboard segments
+            if(useVirtualMidiOutput) {
+                cout << "Opening virtual MIDI output\n";
+                controller.enableMIDIOutputVirtualPort(0, "TouchKeys");
+            }
+            else {
+                cout << "Opening MIDI output device " << midiOutputNum << endl;
+                controller.enableMIDIOutputPort(0, midiOutputNum);
+            }
+            
+            // Start the TouchKeys
+            cout << "Starting the TouchKeys on " << touchkeysDevicePath << " ... ";
+            if(!controller.touchkeyDeviceStartupSequence(touchkeysDevicePath.c_str())) {
+                cout << "failed: " << controller.touchkeyDeviceErrorMessage() << endl;
+                throw new exception;
+            }
+            else
+                cout << "succeeded!\n";
+            
+            // Set up interrupt catching so we can stop with Ctrl-C
+            /*struct sigaction sigIntHandler;
+            
+            sigIntHandler.sa_handler = sigint_handler;
+            sigemptyset(&sigIntHandler.sa_mask);
+            sigIntHandler.sa_flags = 0;
+            sigaction(SIGINT, &sigIntHandler, NULL);
+
+            while(!programShouldStop_) {
+                Thread::sleep(50);
+            }*/
+            
+            Thread::sleep(1000);
+            
+            cout << "Cleaning up...\n";
+        }
+        catch(...) {
+            
+        }
+    }
+    
+    // Clean up any MessageManager instance that JUCE creates
+    DeletedAtShutdown::deleteAll();
+    MessageManager::deleteInstance();
+    return 0;
+}
+
+#endif // TOUCHKEYS_NO_GUI
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/MainApplicationController.cpp	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,450 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+ 
+  MainApplicationController.cpp: contains the overall glue that holds
+  together the various parts of the TouchKeys code. It works together
+  with the user interface to let the user configure the hardware and 
+  manage the mappings, but it is kept separate from any particular user 
+  interface configuration.
+*/
+
+#include "MainApplicationController.h"
+#include <cstdlib>
+#include <sstream>
+
+// Strings for pitch classes (two forms for sharps), for static methods
+const char* kNoteNames[12] = {"C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"};
+const char* kNoteNamesAlternate[12] = {"C", "Db", "D ", "Eb", "E", "F", "Gb", "G", "Ab", "A", "Bb", "B"};
+
+#undef USE_TWO_SEGMENTS
+
+MainApplicationController::MainApplicationController()
+: midiInputController_(keyboardController_),
+  touchkeyController_(keyboardController_),
+  touchkeyErrorOccurred_(false),
+  touchkeyErrorMessage_(""),
+  touchkeyAutodetecting_(false),
+  touchkeyStandaloneModeEnabled_(false),
+  experimentalMappingsEnabled_(false),
+#ifndef TOUCHKEYS_NO_GUI
+  keyboardDisplayWindow_(0),
+#endif
+  segmentCounter_(0),
+  loggingActive_(false)
+{
+    // Set our OSC controller
+    setOscController(&keyboardController_);
+    oscTransmitter_.setEnabled(false);
+    //oscTransmitter_.setDebugMessages(true);
+    
+    // Initialize the links between objects
+    keyboardController_.setOscTransmitter(&oscTransmitter_);
+    keyboardController_.setMidiOutputController(&midiOutputController_);
+    keyboardController_.setGUI(&keyboardDisplay_);
+	midiInputController_.setMidiOutputController(&midiOutputController_);
+    
+    // Set the initial mode of the MIDI input controller
+    //segment = midiSegmentAdd();
+    
+	// Set the initial verbosity level of the TouchKeys devices
+	touchkeyController_.setVerboseLevel(2);
+    
+    // Set up an initial OSC transmit host/port
+    oscTransmitter_.addAddress(kDefaultOscTransmitHost, kDefaultOscTransmitPort);
+
+    // Set up default logging directory
+    loggingDirectory_ = (File::getSpecialLocation(File::userHomeDirectory).getFullPathName() + "/Desktop").toUTF8();
+    
+    // Defaults for display, until we get other information
+    keyboardDisplay_.setKeyboardRange(36, 72);
+    
+    // Add one keyboard segment at the beginning
+    midiSegmentAdd();
+}
+
+MainApplicationController::~MainApplicationController() {
+
+}
+
+bool MainApplicationController::touchkeyDeviceStartupSequence(const char * path) {
+    // Step 1: attempt to open device
+    if(!openTouchkeyDevice(path)) {
+        touchkeyErrorMessage_ = "Failed to open";
+        touchkeyErrorOccurred_ = true;
+        return false;
+    }
+    
+    // Step 2: see if a real TouchKeys device is present at the other end
+    if(!touchkeyDeviceCheckForPresence()) {
+        touchkeyErrorMessage_ = "Device not recognized";
+        touchkeyErrorOccurred_ = true;
+        return false;
+    }
+    
+    // Step 3: update the display
+    keyboardDisplay_.setKeyboardRange(touchkeyController_.lowestKeyPresentMidiNote(), touchkeyController_.highestMidiNote());
+#ifndef TOUCHKEYS_NO_GUI
+    if(keyboardDisplayWindow_ != 0) {
+        keyboardDisplayWindow_->getConstrainer()->setFixedAspectRatio(keyboardDisplay_.keyboardAspectRatio());
+        keyboardDisplayWindow_->setBoundsConstrained(keyboardDisplayWindow_->getBounds());
+    }
+#endif
+    
+    // Step 4: start data collection from the device
+    if(!startTouchkeyDevice()) {
+        touchkeyErrorMessage_ = "Failed to start";
+        touchkeyErrorOccurred_ = true;
+    }
+
+    // Success!
+    touchkeyErrorMessage_ = "";
+    touchkeyErrorOccurred_ = false;
+    return true;
+}
+
+std::string MainApplicationController::touchkeyDevicePrefix() {
+    if(SystemStats::getOperatingSystemType() == SystemStats::Linux) {
+        return "/dev/serial/by-id/";
+    }
+    else {
+        return "/dev/";
+    }
+}
+
+// Return a list of available TouchKey devices
+std::vector<std::string> MainApplicationController::availableTouchkeyDevices() {
+    std::vector<std::string> devices;
+    
+    if(SystemStats::getOperatingSystemType() == SystemStats::Linux) {
+        DirectoryIterator devDirectory(File("/dev/serial/by-id"),false,"*");
+        
+        while(devDirectory.next()) {
+            devices.push_back(string(devDirectory.getFile().getFileName().toUTF8()));
+        }
+    }
+    else {
+        DirectoryIterator devDirectory(File("/dev"),false,"cu.usbmodem*");
+        
+        while(devDirectory.next()) {
+            devices.push_back(string(devDirectory.getFile().getFileName().toUTF8()));
+        }
+    }
+    
+    return devices;
+}
+
+// Check whether a TouchKey device is present. Returns true if device found.
+bool MainApplicationController::touchkeyDeviceCheckForPresence(int waitMilliseconds, int tries) {
+    
+    int count = 0;
+    while(1) {
+        if(touchkeyController_.checkIfDevicePresent(waitMilliseconds))
+            break;
+        if(++count >= tries) {
+            return false;
+        }
+    }
+    
+    return true;
+}
+
+// Start an autodetection routine to match touch data to MIDI
+void MainApplicationController::touchkeyDeviceAutodetectLowestMidiNote() {
+    if(touchkeyAutodetecting_)
+        return;
+    
+    touchkeyAutodetecting_ = true;
+    addOscListener("/midi/noteon");
+}
+
+// Abort an autodetection routine
+void MainApplicationController::touchkeyDeviceStopAutodetecting() {
+    if(!touchkeyAutodetecting_)
+        return;
+    
+    removeOscListener("/midi/noteon");
+    touchkeyAutodetecting_ = false;
+}
+
+bool MainApplicationController::touchkeyDeviceIsAutodetecting() {
+    return touchkeyAutodetecting_;
+}
+
+// Start logging TouchKeys/MIDI data to a file. Filename is autogenerated
+// based on current time.
+void MainApplicationController::startLogging() {
+    if(loggingActive_)
+        stopLogging();
+    
+    std::stringstream out;
+    out << time(NULL);
+    std::string fileId = out.str();
+    
+
+    string midiLogFileName = "midiLog_" + fileId + ".bin";
+    string keyTouchLogFileName = "keyTouchLog_" + fileId + ".bin";
+    string analogLogFileName = "keyAngleLog_" + fileId + ".bin";
+    
+    // Create log files with these names
+    midiInputController_.createLogFile(midiLogFileName, loggingDirectory_);
+    touchkeyController_.createLogFiles(keyTouchLogFileName, analogLogFileName, loggingDirectory_);
+    
+    // Enable logging from each controller
+    midiInputController_.startLogging();
+    touchkeyController_.startLogging();
+    
+    loggingActive_ = true;
+}
+
+// Stop a currently running log.
+void MainApplicationController::stopLogging() {
+    if(!loggingActive_)
+        return;
+    
+    // stop logging data
+    midiInputController_.stopLogging();
+    touchkeyController_.stopLogging();
+    
+    // close the log files
+    midiInputController_.closeLogFile();
+    touchkeyController_.closeLogFile();
+    
+    loggingActive_ = false;
+}
+
+void MainApplicationController::setLoggingDirectory(const char *directory) {
+    loggingDirectory_ = directory;
+}
+
+// Add a new MIDI keyboard segment. This method also handles numbering of the segments
+MidiKeyboardSegment* MainApplicationController::midiSegmentAdd() {
+    // For now, the segment counter increments with each new segment. Eventually, we could
+    // consider renumbering every time a segment is removed so that we always have an index
+    // 0-N which corresponds to the indexes within MidiInputController (and also the layout
+    // of the tabs).
+    MidiKeyboardSegment *newSegment = midiInputController_.addSegment(segmentCounter_++, 12, 127);
+    
+    // Set up defaults
+    newSegment->setModePassThrough();
+    newSegment->setPolyphony(8);
+    newSegment->setVoiceStealingEnabled(false);
+    newSegment->enableAllChannels();
+    newSegment->setOutputTransposition(0);
+    newSegment->setUsesKeyboardChannelPressure(true);
+    
+    // Enable standalone mode on the new segment if generally enabled
+    if(touchkeyStandaloneModeEnabled_)
+        newSegment->enableTouchkeyStandaloneMode();
+    
+    return newSegment;
+}
+
+// Enable TouchKeys standalone mode
+void MainApplicationController::midiTouchkeysStandaloneModeEnable() {
+    touchkeyStandaloneModeEnabled_ = true;
+    // Go through all segments and enable standalone mode
+    for(int i = 0; i < midiInputController_.numSegments(); i++) {
+        midiInputController_.segment(i)->enableTouchkeyStandaloneMode();
+    }
+}
+
+void MainApplicationController::midiTouchkeysStandaloneModeDisable() {
+    touchkeyStandaloneModeEnabled_ = false;
+    // Go through all segments and disable standalone mode
+    for(int i = 0; i < midiInputController_.numSegments(); i++) {
+        midiInputController_.segment(i)->enableTouchkeyStandaloneMode();
+    }
+}
+
+// OSC handler method
+bool MainApplicationController::oscHandlerMethod(const char *path, const char *types, int numValues, lo_arg **values, void *data) {
+	if(!strcmp(path, "/midi/noteon") && touchkeyAutodetecting_ && numValues > 0) {
+        // std::cout << "/midi/noteon\n";
+        // Found a MIDI note. Look for a unique touch on this pitch class to
+        // determine which octave the keyboard is set to
+        if(types[0] != 'i')
+            return false;   // Ill-formed message
+        int midiNote = values[0]->i;
+        if(midiNote < 0 || midiNote > 127)
+            return false;
+        
+        // Go through each octave and see if a touch is present
+        int midiTestNote = midiNote % 12;
+        int count = 0;
+        int lastFoundTouchNote = 0;
+        while(midiTestNote <= 127) {
+            if(keyboardController_.key(midiTestNote) != 0) {
+                if(keyboardController_.key(midiTestNote)->touchIsActive()) {
+                    count++;
+                    lastFoundTouchNote = midiTestNote;
+                }
+            }
+            midiTestNote += 12;
+        }
+        
+        // We return success if exactly one note had a touch on this pitch class
+        if(count == 1) {
+            int noteDifference = lastFoundTouchNote - midiNote;
+            int currentMinNote = touchkeyController_.lowestMidiNote();
+
+            // std::cout << "Found difference of " << noteDifference << std::endl;
+
+            currentMinNote -= noteDifference;
+            if(currentMinNote >= 0 && currentMinNote <= 127)
+                touchkeyController_.setLowestMidiNote(currentMinNote);
+            
+            touchkeyDeviceStopAutodetecting();
+        }
+        return false; // Others may still want to handle this message
+    }
+    
+    return false;
+}
+
+// Factores to use
+const int kNumMappingFactoryTypes = 7;
+const char* kMappingFactoryNames[kNumMappingFactoryTypes] = {"Control", "Vibrato", "Pitch Bend", "Split Key", "Multi-Finger Trigger", "Onset Angle", "Release Angle"};
+
+// Return the number of mapping factory types available
+int MainApplicationController::numberOfMappingFactories() {
+    return kNumMappingFactoryTypes;
+}
+
+// Return the name of the given mapping factory type
+String MainApplicationController::mappingFactoryNameForIndex(int index) {
+    if(index < 0 || index >= kNumMappingFactoryTypes)
+        return String();
+    return kMappingFactoryNames[index];
+}
+
+// Return a new object of the given mapping factory type
+MappingFactory* MainApplicationController::createMappingFactoryForIndex(int index, MidiKeyboardSegment& segment) {
+    switch(index) {
+        case 0:
+            return new TouchkeyControlMappingFactory(keyboardController_, segment);
+        case 1:
+            return new TouchkeyVibratoMappingFactory(keyboardController_, segment);
+        case 2:
+            return new TouchkeyPitchBendMappingFactory(keyboardController_, segment);
+        case 3:
+            return new TouchkeyKeyDivisionMappingFactory(keyboardController_, segment);
+        case 4:
+            return new TouchkeyMultiFingerTriggerMappingFactory(keyboardController_, segment);
+        case 5:
+            return new TouchkeyOnsetAngleMappingFactory(keyboardController_, segment);
+        case 6:
+            return new TouchkeyReleaseAngleMappingFactory(keyboardController_, segment);
+        default:
+            return 0;
+    }
+}
+
+// Return whethera  given mapping is experimental or not
+bool MainApplicationController::mappingIsExperimental(int index) {
+    if(index > 2)
+        return true;
+    return false;
+}
+
+// Return the name of a MIDI note given its number
+std::string MainApplicationController::midiNoteName(int noteNumber) {
+    if(noteNumber < 0 || noteNumber > 127)
+        return "";
+    char name[6];
+    snprintf(name, 6, "%s%d", kNoteNames[noteNumber % 12], (noteNumber / 12) - 1);
+
+    return name;
+}
+
+// Get the number of a MIDI note given its name
+int MainApplicationController::midiNoteNumberForName(std::string const& name) {
+    // Any valid note name will have at least two characters
+    if(name.length() < 2)
+        return -1;
+    
+    // Find the pitch class first, then the octave
+    int pitchClass = -1;
+    int startIndex = 1;
+    if(!name.compare(0, 2, "C#") ||
+       !name.compare(0, 2, "c#") ||
+       !name.compare(0, 2, "Db") ||
+       !name.compare(0, 2, "db")) {
+        pitchClass = 1;
+        startIndex = 2;
+    }
+    else if(!name.compare(0, 2, "D#") ||
+            !name.compare(0, 2, "d#") ||
+            !name.compare(0, 2, "Eb") ||
+            !name.compare(0, 2, "eb")) {
+        pitchClass = 3;
+        startIndex = 2;
+    }
+    else if(!name.compare(0, 2, "F#") ||
+            !name.compare(0, 2, "f#") ||
+            !name.compare(0, 2, "Gb") ||
+            !name.compare(0, 2, "gb")){
+        pitchClass = 6;
+        startIndex = 2;
+    }
+    else if(!name.compare(0, 2, "G#") ||
+            !name.compare(0, 2, "g#") ||
+            !name.compare(0, 2, "Ab") ||
+            !name.compare(0, 2, "ab")){
+        pitchClass = 8;
+        startIndex = 2;
+    }
+    else if(!name.compare(0, 2, "A#") ||
+            !name.compare(0, 2, "a#") ||
+            !name.compare(0, 2, "Bb") ||
+            !name.compare(0, 2, "bb")){
+        pitchClass = 10;
+        startIndex = 2;
+    }
+    else if(!name.compare(0, 1, "C") ||
+            !name.compare(0, 1, "c"))
+        pitchClass = 0;
+    else if(!name.compare(0, 1, "D") ||
+            !name.compare(0, 1, "d"))
+        pitchClass = 2;
+    else if(!name.compare(0, 1, "E") ||
+            !name.compare(0, 1, "e"))
+        pitchClass = 4;
+    else if(!name.compare(0, 1, "F") ||
+            !name.compare(0, 1, "f"))
+        pitchClass = 5;
+    else if(!name.compare(0, 1, "G") ||
+            !name.compare(0, 1, "g"))
+        pitchClass = 7;
+    else if(!name.compare(0, 1, "A") ||
+            !name.compare(0, 1, "a"))
+        pitchClass = 9;
+    else if(!name.compare(0, 1, "B") ||
+            !name.compare(0, 1, "b"))
+        pitchClass = 11;
+    
+    if(pitchClass < 0) // No valid note found
+        return -1;
+    
+    int octave = atoi(name.substr(startIndex).c_str());
+    int noteNumber = (octave + 1) * 12 + pitchClass;
+    
+    if(noteNumber < 0 || noteNumber > 127)
+        return -1;
+    return noteNumber;
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/MainApplicationController.h	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,308 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+ 
+  MainApplicationController.h: contains the overall glue that holds
+  together the various parts of the TouchKeys code. It works together
+  with the user interface to let the user configure the hardware and
+  manage the mappings, but it is kept separate from any particular user
+  interface configuration.
+*/
+
+#ifndef __TouchKeys__MainApplicationController__
+#define __TouchKeys__MainApplicationController__
+
+#include <iostream>
+#include <vector>
+#include "../JuceLibraryCode/JuceHeader.h"
+#include "TouchKeys/MidiInputController.h"
+#include "TouchKeys/MidiKeyboardSegment.h"
+#include "TouchKeys/MidiOutputController.h"
+#include "TouchKeys/TouchkeyDevice.h"
+#include "TouchKeys/Osc.h"
+#include "Mappings/Vibrato/TouchkeyVibratoMappingFactory.h"
+#include "Mappings/PitchBend/TouchkeyPitchBendMappingFactory.h"
+#include "Mappings/Control/TouchkeyControlMappingFactory.h"
+#include "Mappings/ReleaseAngle/TouchkeyReleaseAngleMappingFactory.h"
+#include "Mappings/OnsetAngle/TouchkeyOnsetAngleMappingFactory.h"
+#include "Mappings/MultiFingerTrigger/TouchkeyMultiFingerTriggerMappingFactory.h"
+#include "Mappings/KeyDivision/TouchkeyKeyDivisionMappingFactory.h"
+#include "Mappings/MappingFactorySplitter.h"
+#include "TouchKeys/LogPlayback.h"
+
+const char kDefaultOscTransmitHost[] = "127.0.0.1";
+const char kDefaultOscTransmitPort[] = "8000";
+
+class InterfaceSelectorComponent;
+
+class MainApplicationController : public OscHandler {
+public:
+    // *** Constructor ***
+    MainApplicationController();
+    
+    // *** Destructor ***
+    ~MainApplicationController();
+    
+    // *** TouchKeys device methods ***
+    
+    // Return the path prefix of the TouchKeys device
+    std::string touchkeyDevicePrefix();
+    
+    // Return a list of paths to all available touchkey devices
+    std::vector<std::string> availableTouchkeyDevices();
+
+    // Run the main startup sequence: open device, check its presence,
+    // start data collection, all in one method. Returns true if successful.
+    // Will set the error message string if not
+    bool touchkeyDeviceStartupSequence(const char * path);
+    void touchkeyDeviceClearErrorMessage() {
+        touchkeyErrorMessage_ = "";
+        touchkeyErrorOccurred_ = false;
+    }
+    
+    // Select a particular touchkey device
+    bool openTouchkeyDevice(const char * path) {
+        return touchkeyController_.openDevice(path);
+    }
+    void closeTouchkeyDevice() {
+        touchkeyController_.closeDevice();
+    }
+    
+    // Check for device present
+    bool touchkeyDeviceCheckForPresence(int waitMilliseconds = 250, int tries = 10);
+    
+    // Start/stop the TouchKeys data collection
+    bool startTouchkeyDevice() {
+        return touchkeyController_.startAutoGathering();
+    }
+    void stopTouchkeyDevice() {
+        touchkeyController_.stopAutoGathering();
+    }
+    
+    // Status queries on TouchKeys
+    // Returns true if device has been opened
+    bool touchkeyDeviceIsOpen() {
+        return touchkeyController_.isOpen();
+    }
+    // Return true if device is collecting data
+    bool touchkeyDeviceIsRunning() {
+        return touchkeyController_.isAutoGathering();
+    }
+    // Returns true if an error has occurred
+    bool touchkeyDeviceErrorOccurred() {
+        return touchkeyErrorOccurred_;
+    }
+    // Return the error message if one occurred
+    std::string touchkeyDeviceErrorMessage() {
+        return touchkeyErrorMessage_;
+    }
+    // How many octaves on the current device
+    int touchkeyDeviceNumberOfOctaves() {
+        return touchkeyController_.numberOfOctaves();
+    }
+    // Return the lowest MIDI note
+    int touchkeyDeviceLowestMidiNote() {
+        return touchkeyController_.lowestMidiNote();
+    }
+    // Set the lowest MIDI note for the TouchKeys
+    void touchkeyDeviceSetLowestMidiNote(int note) {
+        keyboardDisplay_.clearAllTouches();
+        touchkeyController_.setLowestMidiNote(note);
+    }
+    // Attempt to autodetect the correct TouchKey octave from MIDI data
+    void touchkeyDeviceAutodetectLowestMidiNote();
+    void touchkeyDeviceStopAutodetecting();
+    bool touchkeyDeviceIsAutodetecting();
+    
+    // *** MIDI device methods ***
+    
+    // Return a list of IDs and paths to all available MIDI devices
+    std::vector<std::pair<int, std::string> > availableMIDIInputDevices() {
+        return midiInputController_.availableMidiDevices();
+    }
+    
+    std::vector<std::pair<int, std::string> > availableMIDIOutputDevices() {
+        return midiOutputController_.availableMidiDevices();
+    }
+    
+    // Return the number of keyboard segments
+    int midiSegmentsCount() {
+        return midiInputController_.numSegments();
+    }
+    // Return the pointer to a specific segment
+    MidiKeyboardSegment* midiSegment(int index) {
+        return midiInputController_.segment(index);
+    }
+    // Return a unique signature of segment configuration which
+    // tells any listeners whether an update has happened
+    int midiSegmentUniqueIdentifier() {
+        return midiInputController_.segmentUniqueIdentifier();
+    }
+    // Add a new segment, returning the result. Segments are
+    // stored 
+    MidiKeyboardSegment* midiSegmentAdd();
+    // Remove a segment
+    void midiSegmentRemove(MidiKeyboardSegment *segment) {
+        midiInputController_.removeSegment(segment);
+    }
+
+    // Select MIDI input/output devices
+    void enableMIDIInputPort(int portNumber) {
+        midiInputController_.enablePort(portNumber);
+    }
+    void enableAllMIDIInputPorts() {
+        midiInputController_.enableAllPorts();
+    }
+    void disableMIDIInputPort(int portNumber) {
+        midiInputController_.disablePort(portNumber);
+    }
+    void disableAllMIDIInputPorts() {
+        midiInputController_.disableAllPorts();
+    }
+    void enableMIDIOutputPort(int identifier, int deviceNumber) {
+        midiOutputController_.enablePort(identifier, deviceNumber);
+    }
+    void enableMIDIOutputVirtualPort(int identifier, const char *name) {
+        midiOutputController_.enableVirtualPort(identifier, name);
+    }
+    void disableMIDIOutputPort(int identifier) {
+        midiOutputController_.disablePort(identifier);
+    }
+    void disableAllMIDIOutputPorts() {
+        midiOutputController_.disableAllPorts();
+    }
+    
+    // Get selected MIDI input/output devices by ID
+    std::vector<int> selectedMIDIInputPorts() {
+        return midiInputController_.activePorts();
+    }
+    int selectedMIDIOutputPort(int identifier) {
+        return midiOutputController_.enabledPort(identifier);
+    }
+    
+    void midiTouchkeysStandaloneModeEnable();
+    void midiTouchkeysStandaloneModeDisable();
+    bool midiTouchkeysStandaloneModeIsEnabled() { return touchkeyStandaloneModeEnabled_; }
+    
+    // *** OSC device methods ***
+    
+    bool oscTransmitEnabled() {
+        return oscTransmitter_.enabled();
+    }
+    void oscTransmitSetEnabled(bool enable) {
+        oscTransmitter_.setEnabled(enable);
+    }
+    bool oscTransmitRawDataEnabled() {
+        return touchkeyController_.transmitRawDataEnabled();
+    }
+    void oscTransmitSetRawDataEnabled(bool enable) {
+        touchkeyController_.setTransmitRawData(enable);
+    }
+    std::vector<lo_address> oscTransmitAddresses() {
+        return oscTransmitter_.addresses();
+    }
+    int oscTransmitAddAddress(const char * host, const char * port, int proto = LO_UDP) {
+        return oscTransmitter_.addAddress(host, port, proto);
+    }
+	void oscTransmitRemoveAddress(int index) {
+        return oscTransmitter_.removeAddress(index);
+    }
+	void oscTransmitClearAddresses() {
+        return oscTransmitter_.clearAddresses();
+    }
+    
+    // *** Display methods ***
+    
+    KeyboardDisplay& keyboardDisplay() { return keyboardDisplay_; }
+#ifndef TOUCHKEYS_NO_GUI
+    void setKeyboardDisplayWindow(DocumentWindow *window) { keyboardDisplayWindow_ = window; }
+    void showKeyboardDisplayWindow() {
+        if(keyboardDisplayWindow_ != 0) {
+            keyboardDisplayWindow_->setVisible(true);
+            keyboardDisplayWindow_->toFront(true);
+        }
+    }
+#endif
+    
+    // *** Logging methods ***
+    // Logging methods which record TouchKeys and MIDI data to files for
+    // later analysis/playback
+    
+    void startLogging();
+    void stopLogging();
+    bool isLogging() { return loggingActive_; }
+    void setLoggingDirectory(const char *directory);
+    
+    // *** OSC handler method (different from OSC device selection) ***
+    
+	bool oscHandlerMethod(const char *path, const char *types, int numValues, lo_arg **values, void *data);
+    
+    
+    // *** Mapping methods ***
+    // Return the number of mapping factory types available
+    int numberOfMappingFactories();
+    
+    // Return the name of a given mapping factory type
+    String mappingFactoryNameForIndex(int index);
+    
+    // Create a new mapping factory of the given type, attached to
+    // the supplied segment
+    MappingFactory* createMappingFactoryForIndex(int index, MidiKeyboardSegment& segment);
+ 
+    // Whether experimental (not totally finished/tested) mappings are available
+    bool experimentalMappingsEnabled() { return experimentalMappingsEnabled_; }
+    void setExperimentalMappingsEnabled(bool enable) { experimentalMappingsEnabled_ = enable; }
+    
+    // Whether a given mapping is experimental
+    bool mappingIsExperimental(int index);
+    
+    // *** Static utility methods ***
+    static std::string midiNoteName(int noteNumber);
+    static int midiNoteNumberForName(std::string const& name);
+    
+private:
+    // TouchKeys objects
+    PianoKeyboard keyboardController_;
+    MidiInputController midiInputController_;
+    MidiOutputController midiOutputController_;
+    TouchkeyDevice touchkeyController_;
+    OscTransmitter oscTransmitter_;
+    
+    bool touchkeyErrorOccurred_;
+    std::string touchkeyErrorMessage_;
+    bool touchkeyAutodetecting_;
+    bool touchkeyStandaloneModeEnabled_;
+
+    // Mapping objects
+    bool experimentalMappingsEnabled_;
+    
+    // Display objects
+    KeyboardDisplay keyboardDisplay_;
+#ifndef TOUCHKEYS_NO_GUI
+    DocumentWindow *keyboardDisplayWindow_;
+#endif
+    
+    // Segment info
+    int segmentCounter_;
+    
+    // Logging info
+    bool loggingActive_;
+    std::string loggingDirectory_;
+};
+
+#endif /* defined(__TouchKeys__MainApplicationController__) */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/Mappings/Control/TouchkeyControlMapping.cpp	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,634 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+
+  TouchkeyControlMapping.cpp: per-note mapping for the TouchKeys control
+  mapping, which converts an arbitrary touch parameter into a MIDI or
+  OSC control message.
+*/
+
+#include "TouchkeyControlMapping.h"
+#include <vector>
+#include <climits>
+#include <cmath>
+#include "../MappingScheduler.h"
+
+#undef DEBUG_CONTROL_MAPPING
+
+// Class constants
+const int TouchkeyControlMapping::kDefaultMIDIChannel = 0;
+const int TouchkeyControlMapping::kDefaultFilterBufferLength = 300;
+
+const bool TouchkeyControlMapping::kDefaultIgnoresTwoFingers = false;
+const bool TouchkeyControlMapping::kDefaultIgnoresThreeFingers = false;
+const int TouchkeyControlMapping::kDefaultDirection = TouchkeyControlMapping::kDirectionPositive;
+
+// Main constructor takes references/pointers from objects which keep track
+// of touch location, continuous key position and the state detected from that
+// position. The PianoKeyboard object is strictly required as it gives access to
+// Scheduler and OSC methods. The others are optional since any given system may
+// contain only one of continuous key position or touch sensitivity
+TouchkeyControlMapping::TouchkeyControlMapping(PianoKeyboard &keyboard, MappingFactory *factory, int noteNumber, Node<KeyTouchFrame>* touchBuffer,
+                                                   Node<key_position>* positionBuffer, KeyPositionTracker* positionTracker)
+: TouchkeyBaseMapping(keyboard, factory, noteNumber, touchBuffer, positionBuffer, positionTracker),
+controlIsEngaged_(false),
+inputMin_(0.0), inputMax_(1.0), outputMin_(0.0), outputMax_(1.0), outputDefault_(0.0),
+inputParameter_(kInputParameterYPosition), inputType_(kTypeAbsolute),
+threshold_(0.0), ignoresTwoFingers_(kDefaultIgnoresTwoFingers),
+ignoresThreeFingers_(kDefaultIgnoresThreeFingers), direction_(kDefaultDirection),
+touchOnsetValue_(missing_value<float>::missing()),
+midiOnsetValue_(missing_value<float>::missing()),
+lastValue_(missing_value<float>::missing()),
+lastTimestamp_(missing_value<timestamp_type>::missing()), lastProcessedIndex_(0),
+controlEngageLocation_(missing_value<float>::missing()),
+controlScalerPositive_(missing_value<float>::missing()),
+controlScalerNegative_(missing_value<float>::missing()),
+lastControlValue_(outputDefault_),
+rawValues_(kDefaultFilterBufferLength)
+{
+    resetDetectionState();
+}
+
+TouchkeyControlMapping::~TouchkeyControlMapping() {
+#if 0
+#ifndef NEW_MAPPING_SCHEDULER
+    try {
+        disengage();
+    }
+    catch(...) {
+        std::cerr << "~TouchkeyControlMapping(): exception during disengage()\n";
+    }
+#endif
+#endif
+}
+
+// Turn on mapping of data.
+/*void TouchkeyControlMapping::engage() {
+    Mapping::engage();
+    
+    // Register for OSC callbacks on MIDI note on/off
+    addOscListener("/midi/noteon");
+	addOscListener("/midi/noteoff");
+}
+
+// Turn off mapping of data. Remove our callback from the scheduler
+void TouchkeyControlMapping::disengage(bool shouldDelete) {
+    // Remove OSC listeners first
+    removeOscListener("/midi/noteon");
+	removeOscListener("/midi/noteoff");
+    
+    // Don't send any separate message here, leave it where it was
+    
+    Mapping::disengage(shouldDelete);
+    
+    if(noteIsOn_) {
+        // TODO
+    }
+    noteIsOn_ = false;
+}*/
+
+// Reset state back to defaults
+void TouchkeyControlMapping::reset() {
+    TouchkeyBaseMapping::reset();
+    sendControlMessage(outputDefault_);
+    resetDetectionState();
+    //noteIsOn_ = false;
+}
+
+// Resend all current parameters
+void TouchkeyControlMapping::resend() {
+    sendControlMessage(lastControlValue_, true);
+}
+
+// Name for this control, used in the OSC path
+/*void TouchkeyControlMapping::setName(const std::string& name) {
+    controlName_ = name;
+}*/
+
+// Parameters for the controller handling
+// Input parameter to use for this control mapping and whether it is absolute or relative
+void TouchkeyControlMapping::setInputParameter(int parameter, int type) {
+    if(inputParameter_ >= 0 && inputParameter_ < kInputParameterMaxValue)
+        inputParameter_ = parameter;
+    if(type >= 0 && type < kTypeMaxValue)
+        inputType_ = type;
+}
+
+// Input/output range for this parameter
+void TouchkeyControlMapping::setRange(float inputMin, float inputMax, float outputMin, float outputMax, float outputDefault) {
+    inputMin_ = inputMin;
+    inputMax_ = inputMax;
+    outputMin_ = outputMin;
+    outputMax_ = outputMax;
+    outputDefault_ = outputDefault;
+}
+
+// Threshold which must be exceeded for the control to engage (for relative position), or 0 if not used
+void TouchkeyControlMapping::setThreshold(float threshold) {
+    threshold_ = threshold;
+}
+
+void TouchkeyControlMapping::setIgnoresMultipleFingers(bool ignoresTwo, bool ignoresThree) {
+    ignoresTwoFingers_ = ignoresTwo;
+    ignoresThreeFingers_ = ignoresThree;
+}
+
+void TouchkeyControlMapping::setDirection(int direction) {
+    if(direction >= 0 && direction < kDirectionMaxValue)
+        direction_ = direction;
+}
+
+// OSC handler method. Called from PianoKeyboard when MIDI data comes in.
+/*bool TouchkeyControlMapping::oscHandlerMethod(const char *path, const char *types, int numValues, lo_arg **values, void *data) {
+    if(!strcmp(path, "/midi/noteon") && !noteIsOn_ && numValues >= 1) {
+        if(types[0] == 'i' && values[0]->i == noteNumber_) {
+            // MIDI note has gone on. Set the starting location to be most recent
+            // location. It's possible there has been no touch data before this,
+            // in which case lastX and lastY will hold missing values.
+            midiOnsetValue_ = lastValue_;
+            if(!missing_value<float>::isMissing(midiOnsetValue_)) {
+                if(inputType_ == kTypeNoteOnsetRelative) {
+                    // Already have touch data. Clear the buffer here.
+                    // Clear buffer and start with default value for this point
+                    clearBuffers();
+                    
+#ifdef DEBUG_CONTROL_MAPPING
+                    std::cout << "MIDI on: starting at (" << midiOnsetValue_ << ")\n";
+#endif
+                }
+            }
+            else {
+#ifdef DEBUG_CONTROL_MAPPING
+                std::cout << "MIDI on but no touch\n";
+#endif
+            }
+            
+            noteIsOn_ = true;
+            return false;
+        }
+    }
+    else if(!strcmp(path, "/midi/noteoff") && noteIsOn_ && numValues >= 1) {
+        if(types[0] == 'i' && values[0]->i == noteNumber_) {
+            // MIDI note goes off
+            noteIsOn_ = false;
+            if(controlIsEngaged_) {
+                // TODO: should anything happen here?
+            }
+#ifdef DEBUG_CONTROL_MAPPING
+            std::cout << "MIDI off\n";
+#endif
+            return false;
+        }
+    }
+    
+    return false;
+}*/
+
+// Trigger method. This receives updates from the TouchKey data or from state changes in
+// the continuous key position (KeyPositionTracker). It will potentially change the scheduled
+// behavior of future mapping calls, but the actual OSC messages should be transmitted in a different
+// thread.
+void TouchkeyControlMapping::triggerReceived(TriggerSource* who, timestamp_type timestamp) {
+    if(who == 0)
+        return;
+    
+    if(who == touchBuffer_) {
+        if(!touchBuffer_->empty()) {
+            // New touch data is available. Find the distance from the onset location.
+            KeyTouchFrame frame = touchBuffer_->latest();
+            lastTimestamp_ = timestamp;
+            
+            if(frame.count == 0) {
+                // No touches. Last values are "missing", and we're not tracking any
+                // particular touch ID
+                lastValue_ = missing_value<float>::missing();
+                idsOfCurrentTouches_[0] = idsOfCurrentTouches_[1] = idsOfCurrentTouches_[2] = -1;
+                
+#ifdef DEBUG_CONTROL_MAPPING
+                std::cout << "Touch off\n";
+#endif
+            }
+            else {
+                //ScopedLock sl(rawValueAccessMutex_);
+                
+                // At least one touch. Check if we are already tracking an ID and, if so,
+                // use its coordinates. Otherwise grab the lowest current ID.
+                lastValue_ = getValue(frame);
+                
+                // Check that the value actually exists
+                if(!missing_value<float>::isMissing(lastValue_)) {
+                    // If we have no onset value, this is it
+                    if(missing_value<float>::isMissing(touchOnsetValue_)) {
+                        touchOnsetValue_ = lastValue_;
+                        if(inputType_ == kTypeFirstTouchRelative) {
+                            clearBuffers();
+#ifdef DEBUG_CONTROL_MAPPING
+                            std::cout << "Starting at " << lastValue_ << std::endl;
+#endif
+                        }
+                    }
+                    
+                    // If MIDI note is on and we don't previously have a value, this is it
+                    if(noteIsOn_ && missing_value<float>::isMissing(midiOnsetValue_)) {
+                        midiOnsetValue_ = lastValue_;
+                        if(inputType_ == kTypeNoteOnsetRelative) {
+                            clearBuffers();
+#ifdef DEBUG_CONTROL_MAPPING
+                            std::cout << "Starting at " << lastValue_ << std::endl;
+#endif
+                        }
+                    }
+                    
+                    if(noteIsOn_) {
+                        // Insert the latest sample into the buffer depending on how the data should be processed
+                        if(inputType_ == kTypeAbsolute) {
+                            rawValues_.insert(lastValue_, timestamp);
+                        }
+                        else if(inputType_ == kTypeFirstTouchRelative) {
+                            rawValues_.insert(lastValue_ - touchOnsetValue_, timestamp);
+                        }
+                        else if(inputType_ == kTypeNoteOnsetRelative) {
+                            rawValues_.insert(lastValue_ - midiOnsetValue_, timestamp);
+                        }
+                        
+                        // Move the current scheduled event up to the present time.
+                        // FIXME: this may be more inefficient than just doing everything in the current thread!
+#ifdef NEW_MAPPING_SCHEDULER
+                        keyboard_.mappingScheduler().scheduleNow(this);
+#else
+                        keyboard_.unscheduleEvent(this);
+                        keyboard_.scheduleEvent(this, mappingAction_, keyboard_.schedulerCurrentTimestamp());
+#endif
+                    }
+                }
+            }
+        }
+    }
+}
+
+// Mapping method. This actually does the real work of sending OSC data in response to the
+// latest information from the touch sensors or continuous key angle
+timestamp_type TouchkeyControlMapping::performMapping() {
+    //ScopedLock sl(rawValueAccessMutex_);
+    
+    timestamp_type currentTimestamp = keyboard_.schedulerCurrentTimestamp();
+    bool newSamplePresent = false;
+    
+    // Go through the filtered distance samples that are remaining to process.
+    if(lastProcessedIndex_ < rawValues_.beginIndex() + 1) {
+        // Fell off the beginning of the position buffer. Skip to the samples we have
+        // (shouldn't happen except in cases of exceptional system load, and not too
+        // consequential if it does happen).
+        lastProcessedIndex_ = rawValues_.beginIndex() + 1;
+    }
+    
+    while(lastProcessedIndex_ < rawValues_.endIndex()) {
+        float value = rawValues_[lastProcessedIndex_];
+        //timestamp_type timestamp = rawValues_.timestampAt(lastProcessedIndex_);
+        newSamplePresent = true;
+        
+        if(inputType_ == kTypeAbsolute) {
+            controlIsEngaged_ = true;
+        }
+        else if(!controlIsEngaged_) {
+            // Compare value against threshold to see if the control should engage
+            if(fabsf(value) > threshold_) {
+                float startingValue;
+                
+                controlIsEngaged_ = true;
+                controlEngageLocation_ = (value > 0 ? threshold_ : -threshold_);
+                
+#ifdef DEBUG_CONTROL_MAPPING
+                std::cout << "engaging control at distance " << controlEngageLocation_ << std::endl;
+#endif
+                
+                if(inputType_ == kTypeFirstTouchRelative)
+                    startingValue = touchOnsetValue_;
+                else
+                    startingValue = midiOnsetValue_;
+                
+                // This is how much range we would have had without the threshold
+                float distanceToPositiveEdgeWithoutThreshold = 1.0 - startingValue;
+                float distanceToNegativeEdgeWithoutThreshold = 0.0 + startingValue;
+                
+                // This is how much range we actually have with the threshold
+                float actualDistanceToPositiveEdge = 1.0 - (startingValue + controlEngageLocation_);
+                float actualDistanceToNegativeEdge = 0.0 + startingValue + controlEngageLocation_;
+                
+                // Make it so moving toward edge of key gets as far as it would have without
+                // the distance lost by the threshold
+                if(actualDistanceToPositiveEdge > 0.0)
+                    controlScalerPositive_ = (outputMax_ - outputDefault_) * distanceToPositiveEdgeWithoutThreshold / actualDistanceToPositiveEdge;
+                else
+                    controlScalerPositive_ = (outputMax_ - outputDefault_); // Sanity check
+                if(actualDistanceToNegativeEdge > 0.0)
+                    controlScalerNegative_ = (outputDefault_ - outputMin_) * distanceToNegativeEdgeWithoutThreshold / actualDistanceToNegativeEdge;
+                else
+                    controlScalerNegative_ = (outputDefault_ - outputMin_); // Sanity check
+            }
+        }
+        
+        lastProcessedIndex_++;
+    }
+    
+    if(controlIsEngaged_) {
+        // Having processed every sample individually for any detection/filtering, now send
+        // the most recent output as an OSC message
+        if(newSamplePresent) {
+            float latestValue = rawValues_.latest();
+
+            // In cases of relative values, the place the control engages will actually be where it crosses
+            // the threshold, not the onset location itself. Need to update the value accordingly.
+            if(inputType_ == kTypeFirstTouchRelative ||
+               inputType_ == kTypeNoteOnsetRelative) {
+                if(latestValue > 0) {
+                    latestValue -= threshold_;
+                    if(latestValue < 0)
+                        latestValue = 0;
+                }
+                else if(latestValue < 0) {
+                    latestValue += threshold_;
+                    if(latestValue > 0)
+                        latestValue = 0;
+                }
+            }
+
+            if(direction_ == kDirectionNegative)
+                latestValue = -latestValue;
+            else if((direction_ == kDirectionBoth) && latestValue < 0)
+                latestValue = -latestValue;
+            
+            sendControlMessage(latestValue);
+            lastControlValue_ = latestValue;
+        }
+    }
+    
+    // Register for the next update by returning its timestamp
+    nextScheduledTimestamp_ = currentTimestamp + updateInterval_;
+    return nextScheduledTimestamp_;
+}
+
+// MIDI note-on message received
+void TouchkeyControlMapping::midiNoteOnReceived(int channel, int velocity) {
+    // MIDI note has gone on. Set the starting location to be most recent
+    // location. It's possible there has been no touch data before this,
+    // in which case lastX and lastY will hold missing values.
+    midiOnsetValue_ = lastValue_;
+    if(!missing_value<float>::isMissing(midiOnsetValue_)) {
+        if(inputType_ == kTypeNoteOnsetRelative) {
+            // Already have touch data. Clear the buffer here.
+            // Clear buffer and start with default value for this point
+            clearBuffers();
+            
+#ifdef DEBUG_CONTROL_MAPPING
+            std::cout << "MIDI on: starting at (" << midiOnsetValue_ << ")\n";
+#endif
+        }
+    }
+    else {
+#ifdef DEBUG_CONTROL_MAPPING
+        std::cout << "MIDI on but no touch\n";
+#endif
+    }
+}
+
+// MIDI note-off message received
+void TouchkeyControlMapping::midiNoteOffReceived(int channel) {
+    if(controlIsEngaged_) {
+        // TODO: should anything happen here?
+    }
+}
+
+// Reset variables involved in detecting a pitch bend gesture
+void TouchkeyControlMapping::resetDetectionState() {
+    controlIsEngaged_ = false;
+    controlEngageLocation_ = missing_value<float>::missing();
+    idsOfCurrentTouches_[0] = idsOfCurrentTouches_[1] = idsOfCurrentTouches_[2] = -1;
+}
+
+// Clear the buffers that hold distance measurements
+void TouchkeyControlMapping::clearBuffers() {
+    rawValues_.clear();
+    rawValues_.insert(0.0, lastTimestamp_);
+    lastProcessedIndex_ = 0;
+}
+
+// Return the current parameter value depending on which one we are listening to
+float TouchkeyControlMapping::getValue(const KeyTouchFrame& frame) {
+    if(inputParameter_ == kInputParameterXPosition)
+        return frame.locH;
+    /*else if(inputParameter_ == kInputParameter2FingerMean ||
+            inputParameter_ == kInputParameter2FingerDistance) {
+        if(frame.count < 2)
+            return missing_value<float>::missing();
+        if(frame.count == 3 && ignoresThreeFingers_)
+            return missing_value<float>::missing();
+        
+        bool foundCurrentTouch = false;
+        float currentValue;
+        
+        // Look for the touches we were tracking last frame
+        if(idsOfCurrentTouches_[0] >= 0) {
+            for(int i = 0; i < frame.count; i++) {
+                if(frame.ids[i] == idsOfCurrentTouches_[0]) {
+                    if(inputParameter_ == kInputParameterYPosition)
+                        currentValue = frame.locs[i];
+                    else // kInputParameterTouchSize
+                        currentValue = frame.sizes[i];
+                    foundCurrentTouch = true;
+                    break;
+                }
+            }
+        }
+        
+        if(!foundCurrentTouch) {
+            // Assign a new touch to be tracked
+            int lowestRemainingId = INT_MAX;
+            int lowestIndex = 0;
+            
+            for(int i = 0; i < frame.count; i++) {
+                if(frame.ids[i] < lowestRemainingId) {
+                    lowestRemainingId = frame.ids[i];
+                    lowestIndex = i;
+                }
+            }
+            
+            idsOfCurrentTouches_[0] = lowestRemainingId;
+            if(inputParameter_ == kInputParameterYPosition)
+                currentValue = frame.locs[lowestIndex];
+            else if(inputParameter_ == kInputParameterTouchSize)
+                currentValue = frame.sizes[lowestIndex];
+            else // Shouldn't happen
+                currentValue = missing_value<float>::missing();
+            
+#ifdef DEBUG_CONTROL_MAPPING
+            std::cout << "Previous touch stopped; now ID " << idsOfCurrentTouches_[0] << " at (" << currentValue << ")\n";
+#endif
+        }
+        
+    }*/
+    else {
+        if(frame.count == 0)
+            return missing_value<float>::missing();
+        if((inputParameter_ == kInputParameter2FingerMean ||
+           inputParameter_ == kInputParameter2FingerDistance) &&
+           frame.count < 2)
+            return missing_value<float>::missing();
+        if(frame.count == 2 && ignoresTwoFingers_)
+            return missing_value<float>::missing();
+        if(frame.count == 3 && ignoresThreeFingers_)
+            return missing_value<float>::missing();
+        /*
+        // The other values are dependent on individual touches
+        bool foundCurrentTouch = false;
+        float currentValue;
+        
+        // Look for the touch we were tracking last frame
+        if(idsOfCurrentTouches_[0] >= 0) {
+            for(int i = 0; i < frame.count; i++) {
+                if(frame.ids[i] == idsOfCurrentTouches_[0]) {
+                    if(inputParameter_ == kInputParameterYPosition)
+                        currentValue = frame.locs[i];
+                    else // kInputParameterTouchSize
+                        currentValue = frame.sizes[i];
+                    foundCurrentTouch = true;
+                    break;
+                }
+            }
+        }
+        
+        if(!foundCurrentTouch) {
+            // Assign a new touch to be tracked
+            int lowestRemainingId = INT_MAX;
+            int lowestIndex = 0;
+            
+            for(int i = 0; i < frame.count; i++) {
+                if(frame.ids[i] < lowestRemainingId) {
+                    lowestRemainingId = frame.ids[i];
+                    lowestIndex = i;
+                }
+            }
+            
+            idsOfCurrentTouches_[0] = lowestRemainingId;
+            if(inputParameter_ == kInputParameterYPosition)
+                currentValue = frame.locs[lowestIndex];
+            else if(inputParameter_ == kInputParameterTouchSize)
+                currentValue = frame.sizes[lowestIndex];
+            else // Shouldn't happen
+                currentValue = missing_value<float>::missing();
+            
+#ifdef DEBUG_CONTROL_MAPPING
+            std::cout << "Previous touch stopped; now ID " << idsOfCurrentTouches_[0] << " at (" << currentValue << ")\n";
+#endif
+        }*/
+        
+        float currentValue = 0;
+        
+        int idWithinFrame0 = locateTouchId(frame, 0);
+        if(idWithinFrame0 < 0) { 
+            // Touch ID not found, start a new value
+            idsOfCurrentTouches_[0] = lowestUnassignedTouch(frame, &idWithinFrame0);
+#ifdef DEBUG_CONTROL_MAPPING
+            std::cout << "Previous touch stopped (0); now ID " << idsOfCurrentTouches_[0] << endl;
+#endif
+            if(idsOfCurrentTouches_[0] < 0) {
+                cout << "BUG: didn't find any unassigned touch!\n";
+            }
+        }
+        
+        if(inputParameter_ == kInputParameterYPosition)
+            currentValue = frame.locs[idWithinFrame0];
+        else if(inputParameter_ == kInputParameterTouchSize) // kInputParameterTouchSize
+            currentValue = frame.sizes[idWithinFrame0];
+        else if(inputParameter_ == kInputParameter2FingerMean ||
+           inputParameter_ == kInputParameter2FingerDistance) {
+            int idWithinFrame1 = locateTouchId(frame, 1);
+            if(idWithinFrame1 < 0) {
+                // Touch ID not found, start a new value
+                idsOfCurrentTouches_[1] = lowestUnassignedTouch(frame, &idWithinFrame1);
+#ifdef DEBUG_CONTROL_MAPPING
+                std::cout << "Previous touch stopped (1); now ID " << idsOfCurrentTouches_[1] << endl;
+#endif
+                if(idsOfCurrentTouches_[1] < 0) {
+                    cout << "BUG: didn't find any unassigned touch for second finger!\n";
+                }
+            }
+            
+            if(inputParameter_ == kInputParameter2FingerMean)
+                currentValue = (frame.locs[idWithinFrame0] + frame.locs[idWithinFrame1]) * 0.5;
+            else
+                currentValue = fabsf(frame.locs[idWithinFrame1] - frame.locs[idWithinFrame0]);
+        }
+        
+        return currentValue;
+    }
+}
+
+// Look for a touch index in the frame matching the given value of idsOfCurrentTouches[index]
+// Returns -1 if not found
+int TouchkeyControlMapping::locateTouchId(KeyTouchFrame const& frame, int index) {
+    if(idsOfCurrentTouches_[index] < 0)
+        return -1;
+    
+    for(int i = 0; i < frame.count; i++) {
+        if(frame.ids[i] == idsOfCurrentTouches_[index]) {
+            return i;
+        }
+    }
+    
+    return -1;
+}
+
+// Locates the lowest touch ID that is not assigned to a current touch
+// Returns -1 if no unassigned touches were found
+int TouchkeyControlMapping::lowestUnassignedTouch(KeyTouchFrame const& frame, int *indexWithinFrame) {
+    int lowestRemainingId = INT_MAX;
+    int lowestIndex = -1;
+    
+    for(int i = 0; i < frame.count; i++) {
+        if(frame.ids[i] < lowestRemainingId) {
+            bool alreadyAssigned = false;
+            for(int j = 0; j < 3; j++) {
+                if(idsOfCurrentTouches_[j] == frame.ids[i])
+                    alreadyAssigned = true;
+            }
+            
+            if(!alreadyAssigned) {
+                lowestRemainingId = frame.ids[i];
+                lowestIndex = i;
+
+            }
+        }
+    }
+    
+    if(indexWithinFrame != 0)
+        *indexWithinFrame = lowestIndex;
+    return lowestRemainingId;
+}
+
+// Send the pitch bend message of a given number of a semitones. Send by OSC,
+// which can be mapped to MIDI CC externally
+void TouchkeyControlMapping::sendControlMessage(float value, bool force) {
+    if(force || !suspended_) {
+#ifdef DEBUG_CONTROL_MAPPING
+        std::cout << "TouchkeyControlMapping: sending " << value << " for note " << noteNumber_ << std::endl;
+#endif
+        keyboard_.sendMessage(controlName_.c_str(), "if", noteNumber_, value, LO_ARGS_END);
+    }
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/Mappings/Control/TouchkeyControlMapping.h	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,176 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+
+  TouchkeyControlMapping.h: per-note mapping for the TouchKeys control
+  mapping, which converts an arbitrary touch parameter into a MIDI or
+  OSC control message.
+*/
+
+#ifndef __touchkeys__TouchkeyControlMapping__
+#define __touchkeys__TouchkeyControlMapping__
+
+#include <iostream>
+#include <map>
+#include <boost/bind.hpp>
+#include "../TouchkeyBaseMapping.h"
+#include "../../Utility/IIRFilter.h"
+
+// This class handles the implementation of a basic MIDI/OSC control message
+// based on touch data. It can use absolute or relative position in the X
+// or Y axis.
+
+class TouchkeyControlMapping : public TouchkeyBaseMapping /*public Mapping, public OscHandler*/ {
+    friend class TouchkeyControlMappingFactory;
+    
+public:
+    enum {
+        kInputParameterXPosition = 1,
+        kInputParameterYPosition,
+        kInputParameterTouchSize,
+        kInputParameter2FingerMean,
+        kInputParameter2FingerDistance,
+        kInputParameterMaxValue
+    };
+    
+    enum {
+        kTypeAbsolute = 1,
+        kTypeFirstTouchRelative,
+        kTypeNoteOnsetRelative,
+        kTypeMaxValue
+    };
+    
+    enum {
+        kDirectionPositive = 1,
+        kDirectionNegative,
+        kDirectionBoth,
+        kDirectionMaxValue
+    };
+    
+private:
+    // Useful constants for mapping MRP messages
+    static const int kDefaultMIDIChannel;
+    static const int kDefaultFilterBufferLength;
+    
+    static const bool kDefaultIgnoresTwoFingers;
+    static const bool kDefaultIgnoresThreeFingers;
+    static const int kDefaultDirection;
+    
+public:
+	// ***** Constructors *****
+	
+	// Default constructor, passing the buffer on which to trigger
+	TouchkeyControlMapping(PianoKeyboard &keyboard, MappingFactory *factory, int noteNumber, Node<KeyTouchFrame>* touchBuffer,
+                             Node<key_position>* positionBuffer, KeyPositionTracker* positionTracker);
+	
+    // ***** Destructor *****
+    
+    ~TouchkeyControlMapping();
+	
+    // ***** Modifiers *****
+    
+    // Enable mappings to be sent
+    //void engage();
+    
+    // Disable mappings from being sent
+    //void disengage(bool shouldDelete = false);
+	
+    // Reset the state back initial values
+	void reset();
+    
+    // Resend the current state of all parameters
+    void resend();
+    
+    // Name for this control, used in the OSC path
+    //void setName(const std::string& name);
+    
+    // Parameters for the controller handling
+    // Input parameter to use for this control mapping and whether it is absolute or relative
+    void setInputParameter(int parameter, int type);
+    
+    // Input/output range for this parameter
+    void setRange(float inputMin, float inputMax, float outputMin, float outputMax, float outputDefault);
+    
+    // Threshold which must be exceeded for the control to engage (for relative position), or 0 if not used
+    void setThreshold(float threshold);
+    
+    // Set whether the mapping should ignore multiple touches
+    void setIgnoresMultipleFingers(bool ignoresTwo, bool ignoresThree);
+    
+    // Set whether the mapping should use the absolute value of a relative position
+    void setDirection(int direction);
+
+	// ***** Evaluators *****
+    
+    // OSC Handler Method: called by PianoKeyboard (or other OSC source)
+	//bool oscHandlerMethod(const char *path, const char *types, int numValues, lo_arg **values, void *data);
+	
+    // This method receives triggers whenever events occur in the touch data or the
+    // continuous key position (state changes only). It alters the behavior and scheduling
+    // of the mapping but does not itself send OSC messages
+	void triggerReceived(TriggerSource* who, timestamp_type timestamp);
+	
+    // This method handles the OSC message transmission. It should be run in the Scheduler
+    // thread provided by PianoKeyboard.
+    timestamp_type performMapping();
+    
+private:
+    // ***** Private Methods *****
+    void midiNoteOnReceived(int channel, int velocity);
+    void midiNoteOffReceived(int channel);
+    
+    void resetDetectionState();
+    void clearBuffers();
+    
+    float getValue(const KeyTouchFrame& frame);
+    int locateTouchId(KeyTouchFrame const& frame, int index);
+    int lowestUnassignedTouch(KeyTouchFrame const& frame, int *indexWithinFrame);
+    void sendControlMessage(float value, bool force = false);
+    
+	// ***** Member Variables *****
+    
+    bool controlIsEngaged_;                     // Whether the control has
+    
+    float inputMin_, inputMax_;                 // Input ranges
+    float outputMin_, outputMax_;               // Output ranges
+    float outputDefault_;                       // Default value to send in absence of any input
+    int inputParameter_;                        // Parameter to be used for mapping
+    int inputType_;                             // Type of mapping (absolute, relative, etc.)
+    float threshold_;                           // Threshold that must be exceeded before mapping engages
+    bool ignoresTwoFingers_;                    // Whether this mapping supresses all messages when two
+    bool ignoresThreeFingers_;                  // or three fingers are present
+    int direction_;                             // Whether the mapping goes up, down or both directions (for relative motion)
+    
+    float touchOnsetValue_, midiOnsetValue_;    // Where the touch began initially and at MIDI note on
+    float lastValue_;                          // Where the touch was at the last frame we received
+    int idsOfCurrentTouches_[3];                // Which touch ID(s) we're currently following
+    timestamp_type lastTimestamp_;              // When the last data point arrived
+    Node<float>::size_type lastProcessedIndex_; // Index of the last filtered position sample we've handled
+    
+    float controlEngageLocation_;                  // Where the controller was engaged (i.e. where the threshold was crossed, if relative)
+    float controlScalerPositive_, controlScalerNegative_; // Translation between position and control values for upward and downward motions
+
+    float lastControlValue_;                    // The last value we sent out
+    
+    Node<float> rawValues_;                     // Most recent values
+    //CriticalSection rawValueAccessMutex_;       // Mutex protecting access to raw values buffer
+};
+
+
+
+#endif /* defined(__touchkeys__TouchkeyControlMapping__) */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/Mappings/Control/TouchkeyControlMappingFactory.cpp	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,235 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+
+  TouchkeyControlMappingFactory.cpp: factory for the TouchKeys control
+  mapping, which converts an arbitrary touch parameter into a MIDI or
+  OSC control message.
+*/
+
+#include "TouchkeyControlMappingFactory.h"
+#include "TouchkeyControlMappingShortEditor.h"
+
+const int TouchkeyControlMappingFactory::kDefaultController = 1;
+const float TouchkeyControlMappingFactory::kDefaultOutputRangeMin = 0.0;
+const float TouchkeyControlMappingFactory::kDefaultOutputRangeMax = 127.0;
+const float TouchkeyControlMappingFactory::kDefaultOutputDefault = 0.0;
+
+TouchkeyControlMappingFactory::TouchkeyControlMappingFactory(PianoKeyboard &keyboard, MidiKeyboardSegment& segment) :
+TouchkeyBaseMappingFactory<TouchkeyControlMapping>(keyboard, segment),
+inputParameter_(TouchkeyControlMapping::kInputParameterYPosition),
+inputType_(TouchkeyControlMapping::kTypeAbsolute),
+outputRangeMin_(kDefaultOutputRangeMin), outputRangeMax_(kDefaultOutputRangeMax),
+outputDefault_(kDefaultOutputDefault), threshold_(0.0),
+ignoresTwoFingers_(TouchkeyControlMapping::kDefaultIgnoresTwoFingers),
+ignoresThreeFingers_(TouchkeyControlMapping::kDefaultIgnoresThreeFingers),
+direction_(TouchkeyControlMapping::kDefaultDirection)
+{
+    setController(kDefaultController);
+}
+
+// ***** Destructor *****
+
+TouchkeyControlMappingFactory::~TouchkeyControlMappingFactory() {
+
+}
+
+// ***** Accessors / Modifiers *****
+
+void TouchkeyControlMappingFactory::setInputParameter(int inputParameter) {
+    inputParameter_ = inputParameter;
+}
+
+void TouchkeyControlMappingFactory::setInputType(int inputType) {
+    inputType_ = inputType;
+}
+
+void TouchkeyControlMappingFactory::setController(int controller) {
+    // Before changing the controller, check if we were going to or from the pitch wheel.
+    // If so, we should scale the value to or from a 14-bit value
+    if(midiControllerNumber_ == MidiKeyboardSegment::kControlPitchWheel &&
+       controller != MidiKeyboardSegment::kControlPitchWheel) {
+        outputRangeMax_ = outputRangeMax_ / 128.0;
+        if(outputRangeMax_ > 127.0)
+            outputRangeMax_ = 127.0;
+        outputRangeMin_ = outputRangeMin_ / 128.0;
+        if(outputRangeMin_ > 127.0)
+            outputRangeMin_ = 127.0;
+        outputDefault_ = outputDefault_ / 128.0;
+        if(outputDefault_ > 127.0)
+            outputDefault_ = 127.0;
+    }
+    else if(midiControllerNumber_ != MidiKeyboardSegment::kControlPitchWheel &&
+            controller == MidiKeyboardSegment::kControlPitchWheel) {
+        if(outputRangeMax_ == 127.0)
+            outputRangeMax_ = 16383.0;
+        else
+            outputRangeMax_ = outputRangeMax_ * 128.0;
+        if(outputRangeMin_ == 127.0)
+            outputRangeMin_ = 16383.0;
+        else
+            outputRangeMin_ = outputRangeMin_ * 128.0;
+        if(outputDefault_ == 127.0)
+            outputDefault_ = 16383.0;
+        else
+            outputDefault_ = outputDefault_ * 128.0;
+    }
+
+    setMidiParameters(controller, inputRangeMin_, inputRangeMax_, inputRangeCenter_,
+                      outputDefault_, outputRangeMin_, outputRangeMax_);
+    
+    // Listen to incoming controls from the keyboard too, if this is enabled
+    // in MidiKeyboardSegment
+    if(midiConverter_ != 0) {
+        midiConverter_->listenToIncomingControl(midiControllerNumber_);
+    }
+}
+
+/*void TouchkeyControlMappingFactory::setRange(float inputMin, float inputMax, float inputCenter, float outputMin, float outputMax, float outputDefault) {
+    // All possible input parameters are in the range [-1, 1],
+    // and actually in the range [0, 1] if absolute values are used
+    // (though we don't check this here)
+    if(inputMin < -1.0)
+        inputRangeMin_ = -1.0;
+    else if(inputMin > 1.0)
+        inputRangeMin_ = 1.0;
+    else
+        inputRangeMin_ = inputMin;
+    if(inputMax < -1.0)
+        inputRangeMax_ = -1.0;
+    else if(inputMax > 1.0)
+        inputRangeMax_ = 1.0;
+    else
+        inputRangeMax_ = inputMax;
+    if(inputCenter < -1.0)
+        inputRangeCenter_ = -1.0;
+    else if(inputCenter > 1.0)
+        inputRangeCenter_ = 1.0;
+    else
+        inputRangeCenter_ = inputCenter;
+    outputRangeMin_ = outputMin;
+    outputRangeMax_ = outputMax;
+    outputDefault_ = outputDefault;
+    
+    // Update control
+    if(midiConverter_ == 0)
+        return;
+    midiConverter_->removeAllControls();
+    midiConverter_->addControl(controlName_.c_str(), 1, inputRangeMin_, inputRangeMax_, inputRangeCenter_, OscMidiConverter::kOutOfRangeClip);
+}*/
+
+void TouchkeyControlMappingFactory::setRangeInputMin(float inputMin) {
+    if(inputMin < -1.0)
+        inputRangeMin_ = -1.0;
+    else if(inputMin > 1.0)
+        inputRangeMin_ = 1.0;
+    else
+        inputRangeMin_ = inputMin;
+    
+    // Update control
+    //if(midiConverter_ == 0)
+    //    return;
+    //midiConverter_->setControlMinValue(controlName_.c_str(), inputRangeMin_);
+    setMidiParameters(midiControllerNumber_, inputRangeMin_, inputRangeMax_, inputRangeCenter_,
+                      outputDefault_, outputRangeMin_, outputRangeMax_);
+}
+
+void TouchkeyControlMappingFactory::setRangeInputMax(float inputMax) {
+    if(inputMax < -1.0)
+        inputRangeMax_ = -1.0;
+    else if(inputMax > 1.0)
+        inputRangeMax_ = 1.0;
+    else
+        inputRangeMax_ = inputMax;
+    
+    // Update control
+    //if(midiConverter_ == 0)
+    //    return;
+    //midiConverter_->setControlMaxValue(controlName_.c_str(), inputRangeMax_);
+    setMidiParameters(midiControllerNumber_, inputRangeMin_, inputRangeMax_, inputRangeCenter_,
+                      outputDefault_, outputRangeMin_, outputRangeMax_);
+}
+
+void TouchkeyControlMappingFactory::setRangeInputCenter(float inputCenter) {
+    if(inputCenter < -1.0)
+        inputRangeCenter_ = -1.0;
+    else if(inputCenter > 1.0)
+        inputRangeCenter_ = 1.0;
+    else
+        inputRangeCenter_ = inputCenter;
+    
+    // Update control
+    //if(midiConverter_ == 0)
+    //    return;
+    //midiConverter_->setControlCenterValue(controlName_.c_str(), inputRangeCenter_);
+    setMidiParameters(midiControllerNumber_, inputRangeMin_, inputRangeMax_, inputRangeCenter_,
+                      outputDefault_, outputRangeMin_, outputRangeMax_);
+}
+
+void TouchkeyControlMappingFactory::setRangeOutputMin(float outputMin) {
+    outputRangeMin_ = outputMin;
+    
+    setMidiParameters(midiControllerNumber_, inputRangeMin_, inputRangeMax_, inputRangeCenter_,
+                      outputDefault_, outputRangeMin_, outputRangeMax_);
+}
+
+void TouchkeyControlMappingFactory::setRangeOutputMax(float outputMax) {
+    outputRangeMax_ = outputMax;
+    
+    setMidiParameters(midiControllerNumber_, inputRangeMin_, inputRangeMax_, inputRangeCenter_,
+                      outputDefault_, outputRangeMin_, outputRangeMax_);
+}
+
+void TouchkeyControlMappingFactory::setRangeOutputDefault(float outputDefault) {
+    outputDefault_ = outputDefault;
+    
+    setMidiParameters(midiControllerNumber_, inputRangeMin_, inputRangeMax_, inputRangeCenter_,
+                      outputDefault_, outputRangeMin_, outputRangeMax_);
+}
+
+void TouchkeyControlMappingFactory::setThreshold(float threshold) {
+    threshold_ = threshold;
+}
+
+void TouchkeyControlMappingFactory::setIgnoresTwoFingers(bool ignoresTwo) {
+    ignoresTwoFingers_ = ignoresTwo;
+}
+
+void TouchkeyControlMappingFactory::setIgnoresThreeFingers(bool ignoresThree) {
+    ignoresThreeFingers_ = ignoresThree;
+}
+
+void TouchkeyControlMappingFactory::setDirection(int direction) {
+    direction_ = direction;
+}
+
+// ***** GUI Support *****
+MappingEditorComponent* TouchkeyControlMappingFactory::createBasicEditor() {
+    return new TouchkeyControlMappingShortEditor(*this);
+}
+
+// ***** Private Methods *****
+
+void TouchkeyControlMappingFactory::initializeMappingParameters(int noteNumber, TouchkeyControlMapping *mapping) {
+    // Set parameters
+    mapping->setInputParameter(inputParameter_, inputType_);
+    mapping->setRange(inputRangeMin_, inputRangeMax_, outputRangeMin_, outputRangeMax_, outputDefault_);
+    mapping->setThreshold(threshold_);
+    mapping->setIgnoresMultipleFingers(ignoresTwoFingers_, ignoresThreeFingers_);
+    mapping->setDirection(direction_);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/Mappings/Control/TouchkeyControlMappingFactory.h	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,109 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+
+  TouchkeyControlMappingFactory.h: factory for the TouchKeys control
+  mapping, which converts an arbitrary touch parameter into a MIDI or
+  OSC control message.
+*/
+
+
+#ifndef __touchkeys__TouchkeyControlMappingFactory__
+#define __touchkeys__TouchkeyControlMappingFactory__
+
+#include <iostream>
+
+#include "../TouchkeyBaseMappingFactory.h"
+#include "TouchkeyControlMapping.h"
+
+// Factory class to produce Touchkey control messages for generic MIDI/OSC controllers
+
+class TouchkeyControlMappingFactory : public TouchkeyBaseMappingFactory<TouchkeyControlMapping> {
+private:
+    static const int kDefaultController;
+    static const float kDefaultOutputRangeMin;
+    static const float kDefaultOutputRangeMax;
+    static const float kDefaultOutputDefault;
+    
+public:
+    // ***** Constructor *****
+    
+	// Default constructor, containing a reference to the PianoKeyboard class.
+    TouchkeyControlMappingFactory(PianoKeyboard &keyboard, MidiKeyboardSegment& segment);
+	
+    // ***** Destructor *****
+    
+    ~TouchkeyControlMappingFactory();
+    
+    // ***** Accessors / Modifiers *****
+    
+    virtual const std::string factoryTypeName() { return "Control"; }
+
+    // ***** Class-Specific Methods *****
+
+    int getController() { return midiControllerNumber_; }
+    int getInputParameter() { return inputParameter_; }
+    int getInputType() { return inputType_; }
+    float getRangeInputMin() { return inputRangeMin_; }
+    float getRangeInputMax() { return inputRangeMax_; }
+    float getRangeInputCenter() { return inputRangeCenter_; }
+    float getRangeOutputMin() { return outputRangeMin_; }
+    float getRangeOutputMax() { return outputRangeMax_; }
+    float getRangeOutputDefault() { return outputDefault_; }
+    float getThreshold() { return threshold_; }
+    bool getIgnoresTwoFingers() { return ignoresTwoFingers_; }
+    bool getIgnoresThreeFingers() { return ignoresThreeFingers_; }
+    int getDirection() { return direction_; }
+    
+    void setInputParameter(int inputParameter);
+    void setInputType(int inputType);
+    void setController(int controller);
+    //void setRange(float inputMin, float inputMax, float inputCenter, float outputMin, float outputMax, float outputDefault);
+    void setRangeInputMin(float inputMin);
+    void setRangeInputMax(float inputMax);
+    void setRangeInputCenter(float inputCenter);
+    void setRangeOutputMin(float outputMin);
+    void setRangeOutputMax(float outputMax);
+    void setRangeOutputDefault(float outputDefault);
+    void setThreshold(float threshold);
+    void setIgnoresTwoFingers(bool ignoresTwo);
+    void setIgnoresThreeFingers(bool ignoresThree);
+    void setDirection(int direction);
+    
+    // ***** GUI Support *****
+    bool hasBasicEditor() { return true; }
+    MappingEditorComponent* createBasicEditor();
+    bool hasExtendedEditor() { return false; }
+    MappingEditorComponent* createExtendedEditor() { return nullptr; }
+    
+private:
+    // ***** Private Methods *****
+    void initializeMappingParameters(int noteNumber, TouchkeyControlMapping *mapping);
+
+    int inputParameter_;                                // Type of input data
+    int inputType_;                                     // Whether data is absolute or relative
+    //float inputRangeMin_, inputRangeMax_;               // Input ranges
+    float outputRangeMin_, outputRangeMax_;             // Output ranges
+    float outputDefault_;                               // Default values
+    float threshold_;                                   // Detection threshold for relative motion
+    bool ignoresTwoFingers_;                            // Whether this mapping suspends messages when two
+    bool ignoresThreeFingers_;                          // or three fingers are present
+    int direction_;                                     // Whether the mapping uses the absolute value in negative cases
+};
+
+#endif /* defined(__touchkeys__TouchkeyControlMappingFactory__) */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/Mappings/Control/TouchkeyControlMappingShortEditor.cpp	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,340 @@
+/*
+  ==============================================================================
+
+  This is an automatically generated GUI class created by the Introjucer!
+
+  Be careful when adding custom code to these files, as only the code within
+  the "//[xyz]" and "//[/xyz]" sections will be retained when the file is loaded
+  and re-saved.
+
+  Created with Introjucer version: 3.1.0
+
+  ------------------------------------------------------------------------------
+
+  The Introjucer is part of the JUCE library - "Jules' Utility Class Extensions"
+  Copyright 2004-13 by Raw Material Software Ltd.
+
+  ==============================================================================
+*/
+
+//[Headers] You can add your own extra header files here...
+//[/Headers]
+
+#include "TouchkeyControlMappingShortEditor.h"
+
+
+//[MiscUserDefs] You can add your own user definitions and misc code here...
+//[/MiscUserDefs]
+
+//==============================================================================
+TouchkeyControlMappingShortEditor::TouchkeyControlMappingShortEditor (TouchkeyControlMappingFactory& factory)
+    : factory_(factory)
+{
+    addAndMakeVisible (inputRangeLowEditor = new TextEditor ("range low text editor"));
+    inputRangeLowEditor->setMultiLine (false);
+    inputRangeLowEditor->setReturnKeyStartsNewLine (false);
+    inputRangeLowEditor->setReadOnly (false);
+    inputRangeLowEditor->setScrollbarsShown (true);
+    inputRangeLowEditor->setCaretVisible (true);
+    inputRangeLowEditor->setPopupMenuEnabled (true);
+    inputRangeLowEditor->setText (String::empty);
+
+    addAndMakeVisible (rangeLabel = new Label ("range label",
+                                               "Input Range:"));
+    rangeLabel->setFont (Font (15.00f, Font::plain));
+    rangeLabel->setJustificationType (Justification::centredLeft);
+    rangeLabel->setEditable (false, false, false);
+    rangeLabel->setColour (TextEditor::textColourId, Colours::black);
+    rangeLabel->setColour (TextEditor::backgroundColourId, Colour (0x00000000));
+
+    addAndMakeVisible (controlLabel = new Label ("control label",
+                                                 "Control:"));
+    controlLabel->setFont (Font (15.00f, Font::plain));
+    controlLabel->setJustificationType (Justification::centredLeft);
+    controlLabel->setEditable (false, false, false);
+    controlLabel->setColour (TextEditor::textColourId, Colours::black);
+    controlLabel->setColour (TextEditor::backgroundColourId, Colour (0x00000000));
+
+    addAndMakeVisible (controlComboBox = new ComboBox ("control combo box"));
+    controlComboBox->setEditableText (false);
+    controlComboBox->setJustificationType (Justification::centredLeft);
+    controlComboBox->setTextWhenNothingSelected (String::empty);
+    controlComboBox->setTextWhenNoChoicesAvailable ("(no choices)");
+    controlComboBox->addListener (this);
+
+    addAndMakeVisible (controlLabel2 = new Label ("control label",
+                                                  "Parameter:"));
+    controlLabel2->setFont (Font (15.00f, Font::plain));
+    controlLabel2->setJustificationType (Justification::centredLeft);
+    controlLabel2->setEditable (false, false, false);
+    controlLabel2->setColour (TextEditor::textColourId, Colours::black);
+    controlLabel2->setColour (TextEditor::backgroundColourId, Colour (0x00000000));
+
+    addAndMakeVisible (parameterComboBox = new ComboBox ("parameter combo box"));
+    parameterComboBox->setEditableText (false);
+    parameterComboBox->setJustificationType (Justification::centredLeft);
+    parameterComboBox->setTextWhenNothingSelected (String::empty);
+    parameterComboBox->setTextWhenNoChoicesAvailable ("(no choices)");
+    parameterComboBox->addListener (this);
+
+    addAndMakeVisible (controlLabel3 = new Label ("control label",
+                                                  "Type:"));
+    controlLabel3->setFont (Font (15.00f, Font::plain));
+    controlLabel3->setJustificationType (Justification::centredLeft);
+    controlLabel3->setEditable (false, false, false);
+    controlLabel3->setColour (TextEditor::textColourId, Colours::black);
+    controlLabel3->setColour (TextEditor::backgroundColourId, Colour (0x00000000));
+
+    addAndMakeVisible (typeComboBox = new ComboBox ("type combo box"));
+    typeComboBox->setEditableText (false);
+    typeComboBox->setJustificationType (Justification::centredLeft);
+    typeComboBox->setTextWhenNothingSelected (String::empty);
+    typeComboBox->setTextWhenNoChoicesAvailable ("(no choices)");
+    typeComboBox->addListener (this);
+
+    addAndMakeVisible (inputRangeHighEditor = new TextEditor ("range hi text editor"));
+    inputRangeHighEditor->setMultiLine (false);
+    inputRangeHighEditor->setReturnKeyStartsNewLine (false);
+    inputRangeHighEditor->setReadOnly (false);
+    inputRangeHighEditor->setScrollbarsShown (true);
+    inputRangeHighEditor->setCaretVisible (true);
+    inputRangeHighEditor->setPopupMenuEnabled (true);
+    inputRangeHighEditor->setText (String::empty);
+
+    addAndMakeVisible (rangeLabel2 = new Label ("range label",
+                                                "-"));
+    rangeLabel2->setFont (Font (15.00f, Font::plain));
+    rangeLabel2->setJustificationType (Justification::centredLeft);
+    rangeLabel2->setEditable (false, false, false);
+    rangeLabel2->setColour (TextEditor::textColourId, Colours::black);
+    rangeLabel2->setColour (TextEditor::backgroundColourId, Colour (0x00000000));
+
+
+    //[UserPreSize]
+    parameterComboBox->addItem("X Position", TouchkeyControlMapping::kInputParameterXPosition);
+    parameterComboBox->addItem("Y Position", TouchkeyControlMapping::kInputParameterYPosition);
+    parameterComboBox->addItem("Contact Area", TouchkeyControlMapping::kInputParameterTouchSize);
+    parameterComboBox->addItem("2-Finger Mean", TouchkeyControlMapping::kInputParameter2FingerMean);
+    parameterComboBox->addItem("2-Finger Distance", TouchkeyControlMapping::kInputParameter2FingerDistance);
+
+    typeComboBox->addItem("Absolute", TouchkeyControlMapping::kTypeAbsolute);
+    typeComboBox->addItem("1st Touch Relative", TouchkeyControlMapping::kTypeFirstTouchRelative);
+    typeComboBox->addItem("Note Onset Relative", TouchkeyControlMapping::kTypeNoteOnsetRelative);
+
+    for(int i = 1; i <= 119; i++) {
+        controlComboBox->addItem(String(i), i);
+    }
+    controlComboBox->addItem("Pitch Wheel", MidiKeyboardSegment::kControlPitchWheel);
+    controlComboBox->addItem("Channel Pressure", MidiKeyboardSegment::kControlChannelAftertouch);
+    controlComboBox->addItem("Poly Aftertouch", MidiKeyboardSegment::kControlPolyphonicAftertouch);
+    //[/UserPreSize]
+
+    setSize (328, 71);
+
+
+    //[Constructor] You can add your own custom stuff here..
+    inputRangeLowEditor->addListener(this);
+    inputRangeHighEditor->addListener(this);
+    //[/Constructor]
+}
+
+TouchkeyControlMappingShortEditor::~TouchkeyControlMappingShortEditor()
+{
+    //[Destructor_pre]. You can add your own custom destruction code here..
+    //[/Destructor_pre]
+
+    inputRangeLowEditor = nullptr;
+    rangeLabel = nullptr;
+    controlLabel = nullptr;
+    controlComboBox = nullptr;
+    controlLabel2 = nullptr;
+    parameterComboBox = nullptr;
+    controlLabel3 = nullptr;
+    typeComboBox = nullptr;
+    inputRangeHighEditor = nullptr;
+    rangeLabel2 = nullptr;
+
+
+    //[Destructor]. You can add your own custom destruction code here..
+    //[/Destructor]
+}
+
+//==============================================================================
+void TouchkeyControlMappingShortEditor::paint (Graphics& g)
+{
+    //[UserPrePaint] Add your own custom painting code here..
+    //[/UserPrePaint]
+
+    g.fillAll (Colours::white);
+
+    //[UserPaint] Add your own custom painting code here..
+    //[/UserPaint]
+}
+
+void TouchkeyControlMappingShortEditor::resized()
+{
+    inputRangeLowEditor->setBounds (80, 40, 40, 24);
+    rangeLabel->setBounds (0, 40, 80, 24);
+    controlLabel->setBounds (176, 8, 56, 24);
+    controlComboBox->setBounds (232, 8, 88, 24);
+    controlLabel2->setBounds (0, 8, 72, 24);
+    parameterComboBox->setBounds (72, 8, 104, 24);
+    controlLabel3->setBounds (184, 40, 56, 24);
+    typeComboBox->setBounds (232, 40, 88, 24);
+    inputRangeHighEditor->setBounds (136, 40, 40, 24);
+    rangeLabel2->setBounds (120, 40, 16, 24);
+    //[UserResized] Add your own custom resize handling here..
+    //[/UserResized]
+}
+
+void TouchkeyControlMappingShortEditor::comboBoxChanged (ComboBox* comboBoxThatHasChanged)
+{
+    //[UsercomboBoxChanged_Pre]
+    //[/UsercomboBoxChanged_Pre]
+
+    if (comboBoxThatHasChanged == controlComboBox)
+    {
+        //[UserComboBoxCode_controlComboBox] -- add your combo box handling code here..
+        int controller = controlComboBox->getSelectedId();
+        factory_.setController(controller);
+        //[/UserComboBoxCode_controlComboBox]
+    }
+    else if (comboBoxThatHasChanged == parameterComboBox)
+    {
+        //[UserComboBoxCode_parameterComboBox] -- add your combo box handling code here..
+        int param = parameterComboBox->getSelectedId();
+        factory_.setInputParameter(param);
+        //[/UserComboBoxCode_parameterComboBox]
+    }
+    else if (comboBoxThatHasChanged == typeComboBox)
+    {
+        //[UserComboBoxCode_typeComboBox] -- add your combo box handling code here..
+        int type = typeComboBox->getSelectedId();
+        factory_.setInputType(type);
+        //[/UserComboBoxCode_typeComboBox]
+    }
+
+    //[UsercomboBoxChanged_Post]
+    //[/UsercomboBoxChanged_Post]
+}
+
+
+
+//[MiscUserCode] You can add your own definitions of your custom methods or any other code here...
+
+void TouchkeyControlMappingShortEditor::textEditorReturnKeyPressed(TextEditor &editor)
+{
+    if(&editor == inputRangeLowEditor) {
+        float range = atof(inputRangeLowEditor->getText().toUTF8());
+        factory_.setRangeInputMin(range);
+        factory_.setRangeInputCenter(range);
+    }
+    else if(&editor == inputRangeHighEditor) {
+        float range = atof(inputRangeHighEditor->getText().toUTF8());
+        factory_.setRangeInputMax(range);
+    }
+}
+
+void TouchkeyControlMappingShortEditor::textEditorEscapeKeyPressed(TextEditor &editor)
+{
+
+}
+
+void TouchkeyControlMappingShortEditor::textEditorFocusLost(TextEditor &editor)
+{
+    textEditorReturnKeyPressed(editor);
+}
+
+void TouchkeyControlMappingShortEditor::synchronize()
+{
+    // Update the editors to reflect the current status
+    if(!inputRangeLowEditor->hasKeyboardFocus(true)) {
+        float value = factory_.getRangeInputMin();
+        char st[16];
+        snprintf(st, 16, "%.2f", value);
+
+        inputRangeLowEditor->setText(st);
+    }
+
+    if(!inputRangeHighEditor->hasKeyboardFocus(true)) {
+        float value = factory_.getRangeInputMax();
+        char st[16];
+        snprintf(st, 16, "%.2f", value);
+
+        inputRangeHighEditor->setText(st);
+    }
+
+    parameterComboBox->setSelectedId(factory_.getInputParameter(), dontSendNotification);
+    typeComboBox->setSelectedId(factory_.getInputType(), dontSendNotification);
+    controlComboBox->setSelectedId(factory_.getController(), dontSendNotification);
+}
+//[/MiscUserCode]
+
+
+//==============================================================================
+#if 0
+/*  -- Introjucer information section --
+
+    This is where the Introjucer stores the metadata that describe this GUI layout, so
+    make changes in here at your peril!
+
+BEGIN_JUCER_METADATA
+
+<JUCER_COMPONENT documentType="Component" className="TouchkeyControlMappingShortEditor"
+                 componentName="" parentClasses="public MappingEditorComponent, public TextEditor::Listener"
+                 constructorParams="TouchkeyControlMappingFactory&amp; factory"
+                 variableInitialisers="factory_(factory)" snapPixels="8" snapActive="1"
+                 snapShown="1" overlayOpacity="0.330000013" fixedSize="1" initialWidth="328"
+                 initialHeight="71">
+  <BACKGROUND backgroundColour="ffffffff"/>
+  <TEXTEDITOR name="range low text editor" id="db0f62c03a58af03" memberName="inputRangeLowEditor"
+              virtualName="" explicitFocusOrder="0" pos="80 40 40 24" initialText=""
+              multiline="0" retKeyStartsLine="0" readonly="0" scrollbars="1"
+              caret="1" popupmenu="1"/>
+  <LABEL name="range label" id="1ca2d422f4c37b7f" memberName="rangeLabel"
+         virtualName="" explicitFocusOrder="0" pos="0 40 80 24" edTextCol="ff000000"
+         edBkgCol="0" labelText="Input Range:" editableSingleClick="0"
+         editableDoubleClick="0" focusDiscardsChanges="0" fontname="Default font"
+         fontsize="15" bold="0" italic="0" justification="33"/>
+  <LABEL name="control label" id="f953b12999632418" memberName="controlLabel"
+         virtualName="" explicitFocusOrder="0" pos="176 8 56 24" edTextCol="ff000000"
+         edBkgCol="0" labelText="Control:" editableSingleClick="0" editableDoubleClick="0"
+         focusDiscardsChanges="0" fontname="Default font" fontsize="15"
+         bold="0" italic="0" justification="33"/>
+  <COMBOBOX name="control combo box" id="f1c84bb5fd2730fb" memberName="controlComboBox"
+            virtualName="" explicitFocusOrder="0" pos="232 8 88 24" editable="0"
+            layout="33" items="" textWhenNonSelected="" textWhenNoItems="(no choices)"/>
+  <LABEL name="control label" id="5ef7c1b78fdcf616" memberName="controlLabel2"
+         virtualName="" explicitFocusOrder="0" pos="0 8 72 24" edTextCol="ff000000"
+         edBkgCol="0" labelText="Parameter:" editableSingleClick="0" editableDoubleClick="0"
+         focusDiscardsChanges="0" fontname="Default font" fontsize="15"
+         bold="0" italic="0" justification="33"/>
+  <COMBOBOX name="parameter combo box" id="f12f6f6e31042be1" memberName="parameterComboBox"
+            virtualName="" explicitFocusOrder="0" pos="72 8 104 24" editable="0"
+            layout="33" items="" textWhenNonSelected="" textWhenNoItems="(no choices)"/>
+  <LABEL name="control label" id="9ded92e82db31777" memberName="controlLabel3"
+         virtualName="" explicitFocusOrder="0" pos="184 40 56 24" edTextCol="ff000000"
+         edBkgCol="0" labelText="Type:" editableSingleClick="0" editableDoubleClick="0"
+         focusDiscardsChanges="0" fontname="Default font" fontsize="15"
+         bold="0" italic="0" justification="33"/>
+  <COMBOBOX name="type combo box" id="82d38054016f6c4f" memberName="typeComboBox"
+            virtualName="" explicitFocusOrder="0" pos="232 40 88 24" editable="0"
+            layout="33" items="" textWhenNonSelected="" textWhenNoItems="(no choices)"/>
+  <TEXTEDITOR name="range hi text editor" id="c34ac3e87db289d1" memberName="inputRangeHighEditor"
+              virtualName="" explicitFocusOrder="0" pos="136 40 40 24" initialText=""
+              multiline="0" retKeyStartsLine="0" readonly="0" scrollbars="1"
+              caret="1" popupmenu="1"/>
+  <LABEL name="range label" id="19e0ad80306cc4c0" memberName="rangeLabel2"
+         virtualName="" explicitFocusOrder="0" pos="120 40 16 24" edTextCol="ff000000"
+         edBkgCol="0" labelText="-" editableSingleClick="0" editableDoubleClick="0"
+         focusDiscardsChanges="0" fontname="Default font" fontsize="15"
+         bold="0" italic="0" justification="33"/>
+</JUCER_COMPONENT>
+
+END_JUCER_METADATA
+*/
+#endif
+
+
+//[EndFile] You can add extra defines here...
+//[/EndFile]
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/Mappings/Control/TouchkeyControlMappingShortEditor.h	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,89 @@
+/*
+  ==============================================================================
+
+  This is an automatically generated GUI class created by the Introjucer!
+
+  Be careful when adding custom code to these files, as only the code within
+  the "//[xyz]" and "//[/xyz]" sections will be retained when the file is loaded
+  and re-saved.
+
+  Created with Introjucer version: 3.1.0
+
+  ------------------------------------------------------------------------------
+
+  The Introjucer is part of the JUCE library - "Jules' Utility Class Extensions"
+  Copyright 2004-13 by Raw Material Software Ltd.
+
+  ==============================================================================
+*/
+
+#ifndef __JUCE_HEADER_61C9A7081A6F3E0__
+#define __JUCE_HEADER_61C9A7081A6F3E0__
+
+//[Headers]     -- You can add your own extra header files here --
+#include "JuceHeader.h"
+#include "TouchkeyControlMappingFactory.h"
+//[/Headers]
+
+
+
+//==============================================================================
+/**
+                                                                    //[Comments]
+    An auto-generated component, created by the Introjucer.
+
+    Describe your class and how it works here!
+                                                                    //[/Comments]
+*/
+class TouchkeyControlMappingShortEditor  : public MappingEditorComponent,
+                                           public TextEditor::Listener,
+                                           public ComboBoxListener
+{
+public:
+    //==============================================================================
+    TouchkeyControlMappingShortEditor (TouchkeyControlMappingFactory& factory);
+    ~TouchkeyControlMappingShortEditor();
+
+    //==============================================================================
+    //[UserMethods]     -- You can add your own custom methods in this section.
+    // TextEditor listener methods
+    void textEditorTextChanged(TextEditor &editor) {}
+    void textEditorReturnKeyPressed(TextEditor &editor);
+    void textEditorEscapeKeyPressed(TextEditor &editor);
+    void textEditorFocusLost(TextEditor &editor);
+
+    void synchronize();
+    //[/UserMethods]
+
+    void paint (Graphics& g);
+    void resized();
+    void comboBoxChanged (ComboBox* comboBoxThatHasChanged);
+
+
+
+private:
+    //[UserVariables]   -- You can add your own custom variables in this section.
+    TouchkeyControlMappingFactory& factory_;
+    //[/UserVariables]
+
+    //==============================================================================
+    ScopedPointer<TextEditor> inputRangeLowEditor;
+    ScopedPointer<Label> rangeLabel;
+    ScopedPointer<Label> controlLabel;
+    ScopedPointer<ComboBox> controlComboBox;
+    ScopedPointer<Label> controlLabel2;
+    ScopedPointer<ComboBox> parameterComboBox;
+    ScopedPointer<Label> controlLabel3;
+    ScopedPointer<ComboBox> typeComboBox;
+    ScopedPointer<TextEditor> inputRangeHighEditor;
+    ScopedPointer<Label> rangeLabel2;
+
+
+    //==============================================================================
+    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (TouchkeyControlMappingShortEditor)
+};
+
+//[EndFile] You can add extra defines here...
+//[/EndFile]
+
+#endif   // __JUCE_HEADER_61C9A7081A6F3E0__
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/Mappings/KeyDivision/TouchkeyKeyDivisionMapping.cpp	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,266 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+
+  TouchkeyKeyDivisionMapping.cpp: per-note mapping for the split-key mapping
+  which triggers different actions or pitches depending on where the key
+  was struck.
+*/
+
+#include "TouchkeyKeyDivisionMapping.h"
+#include "TouchkeyKeyDivisionMappingFactory.h"
+
+#define DEBUG_KEY_DIVISION_MAPPING
+
+const int TouchkeyKeyDivisionMapping::kDefaultNumberOfSegments = 2;
+const timestamp_diff_type TouchkeyKeyDivisionMapping::kDefaultDetectionTimeout = milliseconds_to_timestamp(25.0);
+const int TouchkeyKeyDivisionMapping::kDefaultDetectionParameter = kDetectionParameterYPosition;
+const int TouchkeyKeyDivisionMapping::kDefaultRetriggerNumFrames = 2;
+
+// Main constructor takes references/pointers from objects which keep track
+// of touch location, continuous key position and the state detected from that
+// position. The PianoKeyboard object is strictly required as it gives access to
+// Scheduler and OSC methods. The others are optional since any given system may
+// contain only one of continuous key position or touch sensitivity
+TouchkeyKeyDivisionMapping::TouchkeyKeyDivisionMapping(PianoKeyboard &keyboard, MappingFactory *factory, int noteNumber, Node<KeyTouchFrame>* touchBuffer,
+                                                     Node<key_position>* positionBuffer, KeyPositionTracker* positionTracker)
+: TouchkeyBaseMapping(keyboard, factory, noteNumber, touchBuffer, positionBuffer, positionTracker),
+numberOfSegments_(kDefaultNumberOfSegments), candidateSegment_(-1), detectedSegment_(-1), defaultSegment_(0),
+detectionParameter_(kDefaultDetectionParameter), retriggerable_(false), retriggerNumFrames_(kDefaultRetriggerNumFrames),
+retriggerKeepsVelocity_(true),
+midiNoteOnTimestamp_(missing_value<timestamp_type>::missing()), timeout_(kDefaultDetectionTimeout),
+lastNumActiveTouches_(-1)
+{
+}
+
+// Reset state back to defaults
+void TouchkeyKeyDivisionMapping::reset() {
+    TouchkeyBaseMapping::reset();
+    
+    candidateSegment_ = detectedSegment_ = -1;
+    midiNoteOnTimestamp_ = missing_value<timestamp_type>::missing();
+}
+
+// Resend all current parameters
+void TouchkeyKeyDivisionMapping::resend() {
+    if(detectedSegment_ >= 0)
+        sendSegmentMessage(detectedSegment_, true);
+}
+
+// Set the pitch bend values (in semitones) for each segment. These
+// values are in relation to the pitch of this note
+void TouchkeyKeyDivisionMapping::setSegmentPitchBends(const float *bendsInSemitones, int numBends) {
+    // Clear old values and refill the vector
+    segmentBends_.clear();
+    for(int i = 0; i < numBends; i++)
+        segmentBends_.push_back(bendsInSemitones[i]);
+}
+
+// This method receives data from the touch buffer or possibly the continuous key angle (not used here)
+void TouchkeyKeyDivisionMapping::triggerReceived(TriggerSource* who, timestamp_type timestamp) {
+    if(who == touchBuffer_) {
+        // If we get here, a new touch frame has been received and there is no segment detected
+        // yet. We should come up with a candidate segment. If the MIDI note is on, activate this
+        // segment right away. Otherwise, save it for later so when the MIDI note begins, we have
+        // it ready to go.
+        if(!touchBuffer_->empty()) {
+            const KeyTouchFrame& frame = touchBuffer_->latest();
+            
+            if(detectedSegment_ < 0) {
+                int candidateBasedOnYPosition = -1, candidateBasedOnNumberOfTouches = -1;
+                
+                // Find the first touch. TODO: eventually look for the largest touch
+                float yPosition = frame.locs[0];
+                
+                // Calculate two possible segments based on touch location and based on
+                // number of touches.
+                candidateBasedOnYPosition = segmentForLocation(yPosition);
+                candidateBasedOnNumberOfTouches = segmentForNumTouches(frame.count);
+                
+                if(detectionParameter_ == kDetectionParameterYPosition)
+                    candidateSegment_ = candidateBasedOnYPosition;
+                else if(detectionParameter_ == kDetectionParameterNumberOfTouches)
+                    candidateSegment_ = candidateBasedOnNumberOfTouches;
+                else if(detectionParameter_ == kDetectionParameterYPositionAndNumberOfTouches) {
+                    // Choose the maximum segment specified by the other two methods
+                    candidateSegment_ = candidateBasedOnNumberOfTouches > candidateBasedOnYPosition ? candidateBasedOnNumberOfTouches : candidateBasedOnYPosition;
+                }
+                else // Shouldn't happen
+                    candidateSegment_ = -1;
+                
+                if(noteIsOn_) {
+                    detectedSegment_ = candidateSegment_;
+#ifdef DEBUG_KEY_DIVISION_MAPPING
+                    cout << "TouchkeyKeyDivisionMapping::triggerReceived(): detectedSegment_ = " << detectedSegment_ << endl;
+#endif
+                    sendSegmentMessage(detectedSegment_);
+                }
+            }
+            else if(retriggerable_ &&
+                    (lastNumActiveTouches_ == 1 &&
+                    frame.count >= 2) && noteIsOn_) {
+                // Here, there was one touch active before and now there are two. Look for the
+                // location of the most recently added touch, and determine whether it matches a
+                // segment different from the one we're in. If so, retrigger the MIDI note
+                // with a different pitch bend
+                
+                int newCandidate = segmentForLocation(locationOfNewestTouch(frame));
+                
+#ifdef DEBUG_KEY_DIVIOSION_MAPPING
+                cout << "TouchkeyKeyDivisionMapping: touch added with candidate segment " << newCandidate << " (current is " << detectedSegment_ << ")\n";
+#endif
+                if(newCandidate != detectedSegment_) {
+                    // Set up a new segment to retrigger and tell the scheduler to insert the mapping
+                    detectedSegment_ = newCandidate;
+                    
+                    // Find the keyboard segment, which gives us the output port
+                    int outputPort = static_cast<TouchkeyKeyDivisionMappingFactory*>(factory_)->segment().outputPort();
+                    
+                    // Send MIDI note-on on the same channel as previously
+                    int ch = keyboard_.key(noteNumber_)->midiChannel();
+                    int vel = 64;
+                    if(retriggerKeepsVelocity_)
+                        vel = keyboard_.key(noteNumber_)->midiVelocity();
+                    keyboard_.midiOutputController()->sendNoteOn(outputPort, ch, noteNumber_, vel);
+                    sendSegmentMessage(detectedSegment_);
+                }
+            }
+            
+            // Save the number of active touches for next time
+            lastNumActiveTouches_ = frame.count;
+        }
+    }
+}
+
+// Mapping method. This actually does the real work of sending OSC data in response to the
+// latest information from the touch sensors or continuous key angle
+timestamp_type TouchkeyKeyDivisionMapping::performMapping() {
+    timestamp_type currentTimestamp = keyboard_.schedulerCurrentTimestamp();
+    
+    if(detectedSegment_ >= 0) {
+        // Found segment; no need to keep sending mapping callbacks
+        nextScheduledTimestamp_ = 0;
+        return 0;
+    }
+    
+    if(currentTimestamp - midiNoteOnTimestamp_ > timeout_) {
+        // Timeout occurred. Activate default segment
+#ifdef DEBUG_KEY_DIVISION_MAPPING
+        cout << "TouchkeyKeyDivisionMapping: timeout\n";
+#endif
+        detectedSegment_ = defaultSegment_;
+        sendSegmentMessage(detectedSegment_);
+        nextScheduledTimestamp_ = 0;
+        return 0;
+    }
+    
+    // Register for the next update by returning its timestamp
+    nextScheduledTimestamp_ = currentTimestamp + updateInterval_;
+    return nextScheduledTimestamp_;
+}
+
+// MIDI note-on received. If we have a candidate segment, activate it as the actual segment
+void TouchkeyKeyDivisionMapping::midiNoteOnReceived(int channel, int velocity) {
+    midiNoteOnTimestamp_ = keyboard_.schedulerCurrentTimestamp();
+    
+    if(detectedSegment_ < 0) {
+#ifdef DEBUG_KEY_DIVISION_MAPPING
+        cout << "TouchkeyKeyDivisionMapping::midiNoteOnReceived(): candidateSegment_ = " << candidateSegment_ << endl;
+#endif
+        detectedSegment_ = candidateSegment_;
+        if(detectedSegment_ >= 0) {
+            sendSegmentMessage(detectedSegment_);
+        }
+    }
+}
+
+// MIDI note-off received. Reset back to the detecting state so we can assign the next note to a segment
+void TouchkeyKeyDivisionMapping::midiNoteOffReceived(int channel) {
+    detectedSegment_ = candidateSegment_ = -1;
+}
+
+void TouchkeyKeyDivisionMapping::sendSegmentMessage(int segment, bool force) {
+    if(force || !suspended_) {
+        keyboard_.sendMessage("/touchkeys/keysegment", "ii", noteNumber_, segment, LO_ARGS_END);
+        if(segment < segmentBends_.size() && segment >= 0) {
+#ifdef DEBUG_KEY_DIVISION_MAPPING
+            cout << "TouchkeyKeyDivisionMapping::sendSegmentMessage(): pitch bend = " << segmentBends_[segment] << endl;
+#endif
+            sendPitchBendMessage(segmentBends_[segment], force);
+        }
+        else {
+#ifdef DEBUG_KEY_DIVISION_MAPPING
+            cout << "TouchkeyKeyDivisionMapping::sendSegmentMessage(): no bend for segment " << segment << endl;
+#endif
+        }
+    }
+    else {
+#ifdef DEBUG_KEY_DIVISION_MAPPING
+        cout << "TouchkeyKeyDivisionMapping::sendSegmentMessage(): suspended, not sending segment " << segment << endl;
+#endif
+    }
+}
+
+// Send the pitch bend message of a given number of a semitones. Send by OSC,
+// which can be mapped to MIDI CC externally
+void TouchkeyKeyDivisionMapping::sendPitchBendMessage(float pitchBendSemitones, bool force) {
+    if(force || !suspended_)
+        keyboard_.sendMessage(controlName_.c_str(), "if", noteNumber_, pitchBendSemitones, LO_ARGS_END);
+}
+
+// Find the segment corresponding to a (Y) touch location
+int TouchkeyKeyDivisionMapping::segmentForLocation(float location) {
+    // Divide the key into evenly-spaced regions, and identify and candidate segment.
+    // Since the location can go up to 1.0, make sure the top value doesn't overflow
+    // the number of segments
+    int segment = floorf(location * (float)numberOfSegments_);
+    if(segment >= numberOfSegments_)
+        segment = numberOfSegments_ - 1;
+    return segment;
+}
+
+// Find the segment corresponding to a number of touches
+int TouchkeyKeyDivisionMapping::segmentForNumTouches(int numTouches) {
+    // Check the number of touches, which could divide the note into as many
+    // as three segments.
+    if(numTouches <= 0)
+        return -1;
+    
+    int segment = numTouches - 1;
+    if(segment >= numberOfSegments_)
+        segment = numberOfSegments_ - 1;
+    return segment;
+}
+
+// Return the location of the most recently added touch (indicated by the highest ID)
+float TouchkeyKeyDivisionMapping::locationOfNewestTouch(KeyTouchFrame const& frame) {
+    if(frame.count == 0)
+        return -1.0;
+    
+    // Go through the active touches and find the one with the highest id
+    int highestId = -1;
+    float location = -1.0;
+    for(int i = 0; i < frame.count; i++) {
+        if(frame.ids[i] > highestId) {
+            highestId = frame.ids[i];
+            location = frame.locs[i];
+        }
+    }
+    
+    return location;
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/Mappings/KeyDivision/TouchkeyKeyDivisionMapping.h	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,141 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+
+  TouchkeyKeyDivisionMapping.h: per-note mapping for the split-key mapping
+  which triggers different actions or pitches depending on where the key
+  was struck.
+*/
+
+
+#ifndef __TouchKeys__TouchkeyKeyDivisionMapping__
+#define __TouchKeys__TouchkeyKeyDivisionMapping__
+
+#include "../TouchkeyBaseMapping.h"
+
+// This class handles the division of a key into multiple
+// subsections, for example to handle microtonal divisions
+
+class TouchkeyKeyDivisionMapping : public TouchkeyBaseMapping {
+    friend class TouchkeyKeyDivisionMappingFactory;
+public:
+    enum {
+        kDetectionParameterYPosition,
+        kDetectionParameterNumberOfTouches,
+        kDetectionParameterYPositionAndNumberOfTouches
+    };
+    
+private:
+    // Default values
+    static const int kDefaultNumberOfSegments;
+    static const timestamp_diff_type kDefaultDetectionTimeout;
+    static const int kDefaultDetectionParameter;
+    static const int kDefaultRetriggerNumFrames;
+    
+public:
+	// ***** Constructors *****
+	
+	// Default constructor, passing the buffer on which to trigger
+	TouchkeyKeyDivisionMapping(PianoKeyboard &keyboard, MappingFactory *factory, int noteNumber, Node<KeyTouchFrame>* touchBuffer,
+                              Node<key_position>* positionBuffer, KeyPositionTracker* positionTracker);
+	
+    // ***** Modifiers *****
+    
+    // Reset the state back initial values
+    void reset();
+    
+    // Resend the current state of all parameters
+    void resend();
+    
+	// ***** Evaluators *****
+    
+    // This method receives triggers whenever events occur in the touch data or the
+    // continuous key position (state changes only). It alters the behavior and scheduling
+    // of the mapping but does not itself send OSC messages
+	void triggerReceived(TriggerSource* who, timestamp_type timestamp);
+	
+    // This method handles the OSC message transmission. It should be run in the Scheduler
+    // thread provided by PianoKeyboard.
+    timestamp_type performMapping();
+    
+    // ***** Specific Methods *****
+    
+    // Set the number of segments
+    void setNumberOfSegments(int segments) {
+        if(segments > 0)
+            numberOfSegments_ = segments;
+    }
+    
+    // Set the default segment (upon timeout)
+    void setDefaultSegment(int defaultSegment) {
+        defaultSegment_ = defaultSegment;
+    }
+    
+    // Set the pitch bend values associated with each key segment
+    void setSegmentPitchBends(const float *bendsInSemitones, int numBends);
+    
+    // Set the detection timeout value (how long from MIDI note on to touch)
+    void setTimeout(timestamp_diff_type timeout) {
+        timeout_ = timeout;
+    }
+    
+    // Set which parameter is used to detect segment
+    void setDetectionParameter(int parameter) {
+        detectionParameter_ = parameter;
+    }
+    
+    // Set whether placing a second finger in the other segment triggers a
+    // new note with that segment.
+    void setRetriggerable(bool retrigger, int numFrames, bool keepOriginalVelocity) {
+        retriggerable_ = retrigger;
+        retriggerNumFrames_ = numFrames;
+        retriggerKeepsVelocity_ = keepOriginalVelocity;
+    }
+    
+private:
+    // ***** Private Methods *****
+    
+    void midiNoteOnReceived(int channel, int velocity);
+    void midiNoteOffReceived(int channel);
+    
+    void sendSegmentMessage(int segment, bool force = false);
+    void sendPitchBendMessage(float pitchBendSemitones, bool force = false);
+    
+    int segmentForLocation(float location);
+    int segmentForNumTouches(int numTouches);
+    float locationOfNewestTouch(KeyTouchFrame const& frame);
+    
+	// ***** Member Variables *****
+    
+    int numberOfSegments_;          // How many segments to choose from total
+    int candidateSegment_;          // Which segment we would be in if the press were now
+    int detectedSegment_;           // Which segment of the key we detected
+    int defaultSegment_;            // Which segment to choose by default (on timeout)
+    int detectionParameter_;        // Which parameter is used to determine the segment
+    bool retriggerable_;            // Whether a second touch can retrigger this note
+    int retriggerNumFrames_;        // How many frames a new touch must be present to retrigger
+    bool retriggerKeepsVelocity_;   // Whether a retriggered note keeps the original velocity or a default
+    
+    timestamp_type midiNoteOnTimestamp_; // When the MIDI note went on
+    timestamp_diff_type timeout_;   // How long to wait for a touch event
+    int lastNumActiveTouches_;                  // How many touches were active before
+    
+    std::vector<float> segmentBends_; // What the pitch bend values are for each segment
+};
+
+#endif /* defined(__TouchKeys__TouchkeyKeyDivisionMapping__) */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/Mappings/KeyDivision/TouchkeyKeyDivisionMappingFactory.cpp	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,176 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+
+  TouchkeyKeyDivisionMappingFactory.cpp: factory for the split-key mapping
+  which triggers different actions or pitches depending on where the key
+  was struck.
+*/
+
+#include "TouchkeyKeyDivisionMappingFactory.h"
+
+const float TouchkeyKeyDivisionMappingFactory::kDefaultPitchWheelRangeSemitones = 2.0;
+
+// Yarman-24 Turkish microtonal tuning:
+/*      1/1	RAST		C
+ 84.360	Nim Zengule	C#/Db
+ 145.112	Zengule		Cáµ»/DÆ€
+ 192.180	Dik Zengule	Dd
+ 9/8	DUGAH		D
+ 292.180	Kurdi		D#/Eb
+ 60/49	Dik Kurdi	Dáµ»/EÆ€
+ 364.735	Nerm Segah	Ed
+ 5/4	SEGAH		E
+ 415.677	Buselik		E‡
+ 4/3	CHARGAH		F
+ 584.359	Nim Hijaz	F#/Gb
+ 635.300	Hijaz/Saba	Fáµ»/GÆ€
+ 696.090	Dik Hijaz/Saba	Gd
+ 3/2	NEVA		G
+ 788.270	Nim Hisar	G#/Ab
+ 854.924	Hisar		Gáµ»/AÆ€
+ 888.270	Dik Hisar	Ad
+ 27/16	HUSEYNI		A	440hz
+ 16/9	Ajem		A#/Bb
+ 11/6	Dik Ajem	Aáµ»/BÆ€
+ 1074.547	Nerm Evdj	Bd
+ 15/8	EVDJ		B
+ 1125.488	Mahur		B‡
+ 2/1	GERDANIYE	C */
+/*const float TouchkeyKeyDivisionMappingFactory::kDefaultTuningsCents[24] = {
+    0, 84.36, 145.112, 192.18, 203.9, 292.18, 350.62, 364.74, 386.31, 415.677, 498.04, 584.359,
+    635.3, 696.09, 701.95, 788.27, 854.92, 888.27, 905.87, 996.1, 1049.36, 1074.55, 1088.27, 1125.488
+};*/
+
+// Yarman-24c Turkish microtonal tuning:
+/*   0:          1/1           C    Dbb   unison, perfect prime    RAST ♥
+ 1:         83.059 cents   C#   Db    				nim zengule
+ 2:        143.623 cents					zengule
+ 3:        191.771 cents   C##  Dd				dik zengule
+ 4:          9/8           D    Ebb   major whole tone		DÜGAH ♥
+ 5:        292.413 cents   D#   Eb				kürdi
+ 6:        348.343 cents   D#|  Eb-				dik kürdi
+ 7:        362.503 cents					nerm segah
+ 8:        156/125 cents   E					SEGAH ♥
+ 9:        415.305 cents   E| 					Buselik
+ 10:          4/3           F    Gbb   perfect fourth		ÇARGAH ♥
+ 11:        581.382 cents   F#   Gb				nim hicaz
+ 12:        634.184 cents					hicaz
+ 13:        695.885 cents   F##  Gd				dik hicaz
+ 14:          3/2           G    Abb   perfect fifth		NEVA ♥
+ 15:        788.736 cents   G#   Ab				nim hisar
+ 16:        853.063 cents					hisar
+ 17:        887.656 cents   G##  Ad				dik hisar
+ 18:         27/16          A    Bbb   Pyth. major sixth	HÜSEYNİ ♥
+ 19:         16/9           A#   Bb    Pyth. minor seventh	acem
+ 20:       1043.623 cents   A#|  Bb-				dik acem
+ 21:       1071.942 cents					nerm eviç
+ 22:        234/125 cents   B					EVİÇ ♥
+ 23:       1124.744 cents   B|					mahur
+ 24:          2/1           C    Dbb   octave			GERDANİYE ♥
+*/
+
+/*const float TouchkeyKeyDivisionMappingFactory::kDefaultTuningsCents[24] = {
+    0, 83.059, 143.623, 191.771, 203.9, 292.413, 348.343, 362.503, 383.54, 415.305, 498.04, 581.382,
+    634.184, 695.885, 701.95, 788.736, 853.063, 887.656, 905.87, 996.1, 1043.623, 1071.942, 1085.49, 1124.744
+};*/
+
+/* Yarman-24c as above but arranged for performance */
+const float TouchkeyKeyDivisionMappingFactory::kDefaultTuningsCents[24] = {
+    0, (1124.744 - 1200.0), 83.059, 143.623, 203.9, 191.771, 292.413, 348.343,
+    383.54, 362.503, 498.04, 415.305, 581.382, 634.184, 695.885, 648.682,
+    788.736, 853.063, 905.87, 887.656, 996.1, 1043.623, 1085.49, 1071.942,
+};
+
+/*const float TouchkeyKeyDivisionMappingFactory::kDefaultTuningsCents[24] = {
+    0, 50, 100, 150, 200, 250, 300, 350, 400, 450, 500, 550, 600, 650, 700,
+    750, 800, 850, 900, 950, 1000, 1050, 1100, 1150
+};*/
+
+/*const float TouchkeyKeyDivisionMappingFactory::kDefaultTuningsCents[36] = {
+    0, 33.3, 66.6, 100, 133.3, 166.6, 200, 233.3, 266.6, 300, 333.3, 366.6,
+    400, 433.3, 466.6, 500, 533.3, 566.6, 600, 633.3, 666.6, 700, 733.3, 766.6,
+    800, 833.3, 866.6, 900, 933.3, 966.6, 1000, 1033.3, 1066.6, 1100, 1133.3, 1166.6
+};*/
+
+/* As arranged:
+ *
+ *   B|  Db/  Dd  Eb/  Ed  E|  Gb/  Gd  Ab/  Ad  Bb/  Bd
+ *   C   C#   D   D#   E   F   F#   G   G#   A   A#   B
+ */
+
+TouchkeyKeyDivisionMappingFactory::TouchkeyKeyDivisionMappingFactory(PianoKeyboard &keyboard, MidiKeyboardSegment& segment)
+: TouchkeyBaseMappingFactory<TouchkeyKeyDivisionMapping>(keyboard, segment),
+  pitchWheelRangeSemitones_(kDefaultPitchWheelRangeSemitones),
+  numSegmentsPerKey_(TouchkeyKeyDivisionMapping::kDefaultNumberOfSegments),
+  timeout_(TouchkeyKeyDivisionMapping::kDefaultDetectionTimeout),
+  detectionParameter_(TouchkeyKeyDivisionMapping::kDefaultDetectionParameter),
+  retriggerable_(false),
+  retriggerNumFrames_(TouchkeyKeyDivisionMapping::kDefaultRetriggerNumFrames),
+  retriggerKeepsVelocity_(true),
+  referenceNote_(0), globalOffsetCents_(0)
+{
+    //setName("/touchkeys/segmentpitch");
+    setBendParameters();
+}
+
+void TouchkeyKeyDivisionMappingFactory::setMIDIPitchWheelRange(float maxBendSemitones) {
+    if(maxBendSemitones <= 0)
+        return;
+    pitchWheelRangeSemitones_ = maxBendSemitones;
+    
+    setBendParameters();
+}
+
+void TouchkeyKeyDivisionMappingFactory::setName(const string& name) {
+    TouchkeyBaseMappingFactory<TouchkeyKeyDivisionMapping>::setName(name);
+    setBendParameters();
+}
+
+// Set the initial parameters for a new mapping
+void TouchkeyKeyDivisionMappingFactory::initializeMappingParameters(int noteNumber, TouchkeyKeyDivisionMapping *mapping) {
+    // KLUDGE: testing Maqam tunings. Go from absolute tunings in cents to pitch bends in semitones
+    float tunings[2];
+    int index = (noteNumber + 12 - referenceNote_) % 12;
+    float standardTuning = (float)index * 100.0;
+    tunings[0] = (kDefaultTuningsCents[index*2] - standardTuning + globalOffsetCents_) * .01;
+    tunings[1] = (kDefaultTuningsCents[index*2 + 1] - standardTuning + globalOffsetCents_) * .01;
+    mapping->setSegmentPitchBends(tunings, 2);
+    /*float tunings[3];
+    int index = (noteNumber + 12 - referenceNote_) % 12;
+    float standardTuning = (float)index * 100.0;
+    tunings[0] = (kDefaultTuningsCents[index*3] - standardTuning + globalOffsetCents_) * .01;
+    tunings[1] = (kDefaultTuningsCents[index*3 + 1] - standardTuning + globalOffsetCents_) * .01;
+    tunings[2] = (kDefaultTuningsCents[index*3 + 2] - standardTuning + globalOffsetCents_) * .01;
+    mapping->setSegmentPitchBends(tunings, 3);*/
+
+    
+    mapping->setNumberOfSegments(numSegmentsPerKey_);
+    mapping->setTimeout(timeout_);
+    mapping->setDetectionParameter(detectionParameter_);
+    mapping->setRetriggerable(retriggerable_, retriggerNumFrames_, retriggerKeepsVelocity_);
+}
+
+void TouchkeyKeyDivisionMappingFactory::setBendParameters() {
+    setMidiParameters(MidiKeyboardSegment::kControlPitchWheel, -pitchWheelRangeSemitones_, pitchWheelRangeSemitones_, 0.0);
+    
+    if(midiConverter_ != 0) {
+        midiConverter_->setMidiPitchWheelRange(pitchWheelRangeSemitones_);
+        midiConverter_->listenToIncomingControl(MidiKeyboardSegment::kControlPitchWheel);
+    }
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/Mappings/KeyDivision/TouchkeyKeyDivisionMappingFactory.h	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,106 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+
+  TouchkeyKeyDivisionMappingFactory.h: factory for the split-key mapping
+  which triggers different actions or pitches depending on where the key
+  was struck.
+*/
+
+#ifndef __TouchKeys__TouchkeyKeyDivisionMappingFactory__
+#define __TouchKeys__TouchkeyKeyDivisionMappingFactory__
+
+#include "../TouchkeyBaseMappingFactory.h"
+#include "TouchkeyKeyDivisionMapping.h"
+
+class TouchkeyKeyDivisionMappingFactory : public TouchkeyBaseMappingFactory<TouchkeyKeyDivisionMapping> {
+private:
+    static const float kDefaultPitchWheelRangeSemitones;
+    static const float kDefaultTuningsCents[];
+    
+public:
+    // ***** Constructor *****
+    
+	// Default constructor, containing a reference to the PianoKeyboard class.
+    TouchkeyKeyDivisionMappingFactory(PianoKeyboard &keyboard, MidiKeyboardSegment& segment);
+	
+    // ***** Destructor *****
+    
+    ~TouchkeyKeyDivisionMappingFactory() {}
+
+    // ***** Accessors / Modifiers *****
+    
+    virtual const std::string factoryTypeName() { return "Split\nKeys"; }
+
+    void setMIDIPitchWheelRange(float maxBendSemitones); // Set the range for the MIDI pitch wheel
+    void setName(const string& name);
+    
+    // ***** Specific Methods *****
+    
+    void setNumberOfSegments(int segments) {
+        if(segments > 0)
+            numSegmentsPerKey_ = segments;
+    }
+    
+    // Set the detection timeout value (how long from MIDI note on to touch)
+    void setTimeout(timestamp_diff_type timeout) {
+        timeout_ = timeout;
+    }
+    
+    // Set the detection parameter for choosing a segment
+    void setDetectionParameter(int detectionParameter) {
+        detectionParameter_ = detectionParameter;
+    }
+    
+    // Set whether placing a second finger in the other segment triggers a
+    // new note with that segment.
+    void setRetriggerable(bool retrigger, int numFrames, bool keepOriginalVelocity) {
+        retriggerable_ = retrigger;
+        retriggerNumFrames_ = numFrames;
+        retriggerKeepsVelocity_ = keepOriginalVelocity;
+    }
+    
+    // Set the note that acts as the reference point in a microtonal scale
+    void setReferenceNote(int note) {
+        if(note >= 0)
+            referenceNote_ = note % 12;
+    }
+    
+    void setGlobalOffset(float offsetCents) {
+        globalOffsetCents_ = offsetCents;
+    }
+    
+
+    
+private:
+    // ***** Private Methods *****
+    void initializeMappingParameters(int noteNumber, TouchkeyKeyDivisionMapping *mapping);
+    void setBendParameters();
+    
+    float pitchWheelRangeSemitones_;                    // Range of the MIDI pitch wheel (different than vibrato range)
+    int numSegmentsPerKey_;                             // How many segments per key
+    timestamp_diff_type timeout_;                       // How long before timeout activates default segment
+    int detectionParameter_;                            // Which parameter separates it into segments
+    bool retriggerable_;                                // Whether a second touch can retrigger this note
+    int retriggerNumFrames_;                            // How many frames a new touch must be present to retrigger
+    bool retriggerKeepsVelocity_;                       // Whether a retriggered note keeps the original velocity or a default
+    int referenceNote_;                                 // Which note acts as the reference point
+    float globalOffsetCents_;                           // Offset of every note in cents
+};
+
+#endif /* defined(__TouchKeys__TouchkeyKeyDivisionMappingFactory__) */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/Mappings/MIDIKeyPositionMapping.cpp	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,235 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+
+  MIDIKeyPositionMapping.cpp: handles generating MIDI data out of continuous
+  key position.
+*/
+
+#include "MIDIKeyPositionMapping.h"
+#include "../TouchKeys/MidiOutputController.h"
+
+// Class constants
+const int MIDIKeyPositionMapping::kDefaultMIDIChannel = 0;
+const float MIDIKeyPositionMapping::kDefaultAftertouchScaler = 127.0 / 0.03;   // Default aftertouch sensitivity: MIDI 127 = 0.03
+const float MIDIKeyPositionMapping::kMinimumAftertouchPosition = 0.99;         // Position at which aftertouch messages start
+const float MIDIKeyPositionMapping::kDefaultPercussivenessScaler = 1.0 / 300.0; // Default scaler from percussiveness feature to MIDI
+const key_velocity MIDIKeyPositionMapping::kPianoKeyVelocityForMaxMIDI = scale_key_velocity(40.0);           // Press velocity for MIDI 127
+const key_velocity MIDIKeyPositionMapping::kPianoKeyReleaseVelocityForMaxMIDI = scale_key_velocity(-50.0);   // Release velocity for MIDI 127
+
+
+// Main constructor takes references/pointers from objects which keep track
+// of touch location, continuous key position and the state detected from that
+// position. The PianoKeyboard object is strictly required as it gives access to
+// Scheduler and OSC methods. The others are optional since any given system may
+// contain only one of continuous key position or touch sensitivity
+MIDIKeyPositionMapping::MIDIKeyPositionMapping(PianoKeyboard &keyboard, MappingFactory *factory, int noteNumber, Node<KeyTouchFrame>* touchBuffer,
+                       Node<key_position>* positionBuffer, KeyPositionTracker* positionTracker)
+: Mapping(keyboard, factory, noteNumber, touchBuffer, positionBuffer, positionTracker), noteIsOn_(false),
+  midiChannel_(kDefaultMIDIChannel), lastAftertouchValue_(0), midiPercussivenessChannel_(-1)
+{
+    setAftertouchSensitivity(1.0);
+}
+
+// Copy constructor
+MIDIKeyPositionMapping::MIDIKeyPositionMapping(MIDIKeyPositionMapping const& obj)
+: Mapping(obj), noteIsOn_(obj.noteIsOn_), aftertouchScaler_(obj.aftertouchScaler_), 
+  midiChannel_(obj.midiChannel_), lastAftertouchValue_(obj.lastAftertouchValue_),
+  midiPercussivenessChannel_(obj.midiPercussivenessChannel_)
+{
+    
+}
+
+MIDIKeyPositionMapping::~MIDIKeyPositionMapping() {
+    try {
+        disengage();
+    }
+    catch(...) {
+        std::cerr << "~MIDIKeyPositionMapping(): exception during disengage()\n";
+    }
+}
+
+// Turn off mapping of data. Remove our callback from the scheduler
+void MIDIKeyPositionMapping::disengage() {
+    Mapping::disengage();
+    if(noteIsOn_) {
+        generateMidiNoteOff();
+    }
+    noteIsOn_ = false;
+}
+
+// Reset state back to defaults
+void MIDIKeyPositionMapping::reset() {
+    Mapping::reset();
+    noteIsOn_ = false;
+}
+
+// Set the aftertouch sensitivity on continuous key position
+// 0 means no aftertouch, 1 means default sensitivity, upward
+// from there
+void MIDIKeyPositionMapping::setAftertouchSensitivity(float sensitivity) {
+    if(sensitivity <= 0)
+        aftertouchScaler_ = 0;
+    else
+        aftertouchScaler_ = kDefaultAftertouchScaler * sensitivity;
+}
+
+// Trigger method. This receives updates from the TouchKey data or from state changes in
+// the continuous key position (KeyPositionTracker). It will potentially change the scheduled
+// behavior of future mapping calls, but the actual OSC messages should be transmitted in a different
+// thread.
+void MIDIKeyPositionMapping::triggerReceived(TriggerSource* who, timestamp_type timestamp) {
+    if(who == 0)
+        return;
+    if(who == positionTracker_) {
+        if(!positionTracker_->empty()) {
+            KeyPositionTrackerNotification notification = positionTracker_->latest();
+            
+            // New message from the key position tracker. Might be time to start or end MIDI note.
+            if(notification.type == KeyPositionTrackerNotification::kNotificationTypeFeatureAvailableVelocity && !noteIsOn_) {
+                cout << "Key " << noteNumber_ << " velocity available\n";
+                generateMidiNoteOn();
+                noteIsOn_ = true;
+            }
+            else if(notification.type == KeyPositionTrackerNotification::kNotificationTypeFeatureAvailableReleaseVelocity && noteIsOn_) {
+                cout << "Key " << noteNumber_ << " release velocity available\n";
+                generateMidiNoteOff();
+                noteIsOn_ = false;
+            }
+            else if(notification.type == KeyPositionTrackerNotification::kNotificationTypeFeatureAvailablePercussiveness) {
+                cout << "Key " << noteNumber_ << " percussiveness available\n";
+                generateMidiPercussivenessNoteOn();
+            }
+        }
+    }
+    else if(who == touchBuffer_) {
+        // TODO: New touch data is available from the keyboard
+    }
+}
+
+// Mapping method. This actually does the real work of sending OSC data in response to the
+// latest information from the touch sensors or continuous key angle
+timestamp_type MIDIKeyPositionMapping::performMapping() {
+    if(!engaged_)
+        return 0;
+    
+    timestamp_type currentTimestamp = keyboard_.schedulerCurrentTimestamp();
+
+    // Calculate the output features as a function of input sensor data
+    if(positionBuffer_ == 0) {
+        // No buffer -> all 0
+    }
+    else if(positionBuffer_->empty()) {
+        // No samples -> all 0
+    }
+    else if(noteIsOn_) {
+        // Generate aftertouch messages based on key position, if the note is on and
+        // if the position exceeds the aftertouch threshold. Note on and note off are
+        // handled directly by the trigger thread.
+        key_position latestPosition = positionBuffer_->latest();
+        int aftertouchValue;
+        
+        if(latestPosition < kMinimumAftertouchPosition)
+            aftertouchValue = 0;
+        else {
+            aftertouchValue = (int)((key_position_to_float(latestPosition) - kMinimumAftertouchPosition) * aftertouchScaler_);
+            if(aftertouchValue < 0)
+                aftertouchValue = 0;
+            if(aftertouchValue > 127)
+                aftertouchValue = 127;
+        }
+        
+        if(aftertouchValue != lastAftertouchValue_) {
+            if(keyboard_.midiOutputController() != 0) {
+                keyboard_.midiOutputController()->sendAftertouchPoly(0, midiChannel_, noteNumber_, aftertouchValue);
+            }
+        }
+        
+        lastAftertouchValue_ = aftertouchValue;
+    }
+
+    // Register for the next update by returning its timestamp
+    nextScheduledTimestamp_ = currentTimestamp + updateInterval_;
+    return nextScheduledTimestamp_;
+}
+
+// Generate a MIDI Note On from continuous key data
+void MIDIKeyPositionMapping::generateMidiNoteOn() {
+    if(positionTracker_ == 0)
+        return;
+    
+    std::pair<timestamp_type, key_velocity> velocityInfo = positionTracker_->pressVelocity();
+    
+    // MIDI Velocity now available. Send a MIDI message if relevant.
+    if(keyboard_.midiOutputController() != 0) {
+        float midiVelocity = 0.5;
+        if(!missing_value<key_velocity>::isMissing(velocityInfo.second))
+            midiVelocity = (float)velocityInfo.second / (float)kPianoKeyVelocityForMaxMIDI;
+        if(midiVelocity < 0.0)
+            midiVelocity = 0.0;
+        if(midiVelocity > 1.0)
+            midiVelocity = 1.0;
+        keyboard_.midiOutputController()->sendNoteOn(0, midiChannel_, noteNumber_, (unsigned char)(midiVelocity * 127.0));
+    }
+}
+
+// Generate a MIDI Note Off from continuous key data
+void MIDIKeyPositionMapping::generateMidiNoteOff() {
+    if(positionTracker_ == 0)
+        return;
+    
+    std::pair<timestamp_type, key_velocity> velocityInfo = positionTracker_->releaseVelocity();
+    
+    // MIDI release velocity now available. Send a MIDI message if relevant
+    if(keyboard_.midiOutputController() != 0) {
+        float midiReleaseVelocity = 0.5;
+        if(!missing_value<key_velocity>::isMissing(velocityInfo.second))
+            midiReleaseVelocity = (float)velocityInfo.second / (float)kPianoKeyReleaseVelocityForMaxMIDI;
+        if(midiReleaseVelocity < 0.0)
+            midiReleaseVelocity = 0.0;
+        if(midiReleaseVelocity > 1.0)
+            midiReleaseVelocity = 1.0;
+        keyboard_.midiOutputController()->sendNoteOff(0, midiChannel_, noteNumber_, (unsigned char)(midiReleaseVelocity * 127.0));
+        
+        // Also turn off percussiveness note if enabled
+        if(midiPercussivenessChannel_ >= 0)
+            keyboard_.midiOutputController()->sendNoteOff(0, midiPercussivenessChannel_, noteNumber_, (unsigned char)(midiReleaseVelocity * 127.0));
+    }
+    
+    lastAftertouchValue_ = 0;
+}
+
+void MIDIKeyPositionMapping::generateMidiPercussivenessNoteOn() {
+    if(positionTracker_ == 0 || midiPercussivenessChannel_ < 0)
+        return;
+    
+    KeyPositionTracker::PercussivenessFeatures features = positionTracker_->pressPercussiveness();
+    std::cout << "found percussiveness value of " << features.percussiveness << std::endl;
+    
+    // MIDI Velocity now available. Send a MIDI message if relevant.
+    if(keyboard_.midiOutputController() != 0) {
+        float midiPercVelocity = 0.0;
+        if(!missing_value<key_velocity>::isMissing(features.percussiveness))
+            midiPercVelocity = features.percussiveness * kDefaultPercussivenessScaler;
+        if(midiPercVelocity < 0.0)
+            midiPercVelocity = 0.0;
+        if(midiPercVelocity > 1.0)
+            midiPercVelocity = 1.0;
+        keyboard_.midiOutputController()->sendNoteOn(0, midiPercussivenessChannel_, noteNumber_, (unsigned char)(midiPercVelocity * 127.0));
+    }
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/Mappings/MIDIKeyPositionMapping.h	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,124 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+
+  MIDIKeyPositionMapping.h: handles generating MIDI data out of continuous
+  key position.
+*/
+
+#ifndef __touchkeys__MIDIKeyPositionMapping__
+#define __touchkeys__MIDIKeyPositionMapping__
+
+#include <map>
+#include <boost/bind.hpp>
+#include "../TouchKeys/KeyTouchFrame.h"
+#include "../TouchKeys/KeyPositionTracker.h"
+#include "../TouchKeys/PianoKeyboard.h"
+#include "Mapping.h"
+
+// This class handles the mapping from continuous key position to
+// MIDI messages: note on, note off, aftertouch.
+
+class MIDIKeyPositionMapping : public Mapping {
+private:
+    /*const int kDefaultMIDIChannel = 0;
+    const float kDefaultAftertouchScaler = 127.0 / 0.03;   // Default aftertouch sensitivity: MIDI 127 = 0.03
+    const float kMinimumAftertouchPosition = 0.99;         // Position at which aftertouch messages start
+    const float kDefaultPercussivenessScaler = 1.0 / 300.0; // Default scaler from percussiveness feature to MIDI
+    const key_velocity kPianoKeyVelocityForMaxMIDI = scale_key_velocity(40.0);           // Press velocity for MIDI 127
+    const key_velocity kPianoKeyReleaseVelocityForMaxMIDI = scale_key_velocity(-50.0);   // Release velocity for MIDI 127*/
+    static const int kDefaultMIDIChannel;
+    static const float kDefaultAftertouchScaler;  // Default aftertouch sensitivity: MIDI 127 = 0.03
+    static const float kMinimumAftertouchPosition; // Position at which aftertouch messages start
+    static const float kDefaultPercussivenessScaler; // Default scaler from percussiveness feature to MIDI
+    static const key_velocity kPianoKeyVelocityForMaxMIDI;           // Press velocity for MIDI 127
+    static const key_velocity kPianoKeyReleaseVelocityForMaxMIDI;    // Release velocity for MIDI 127
+    
+public:
+	// ***** Constructors *****
+	
+	// Default constructor, passing the buffer on which to trigger
+	MIDIKeyPositionMapping(PianoKeyboard &keyboard, MappingFactory *factory, int noteNumber, Node<KeyTouchFrame>* touchBuffer,
+               Node<key_position>* positionBuffer, KeyPositionTracker* positionTracker);
+	
+	// Copy constructor
+	MIDIKeyPositionMapping(MIDIKeyPositionMapping const& obj);
+    
+    // ***** Destructor *****
+    
+    ~MIDIKeyPositionMapping();
+	
+    // ***** Modifiers *****
+    
+    // Disable mappings from being sent
+    void disengage();
+	
+    // Reset the state back initial values
+	void reset();
+    
+    // Set the aftertouch sensitivity on continuous key position
+    // 0 means no aftertouch, 1 means default sensitivity, upward
+    // from there
+    void setAftertouchSensitivity(float sensitivity);
+    
+    // Get or set the MIDI channel (0-15)
+    int midiChannel() { return midiChannel_; }
+    void setMIDIChannel(int ch) {
+        if(ch >= 0 && ch < 16)
+            midiChannel_ = ch;
+    }
+    
+    // Get or set the MIDI channel for percussiveness messages
+    int percussivenessMIDIChannel() { return midiPercussivenessChannel_; }
+    void setPercussivenessMIDIChannel(int ch) {
+        if(ch >= 0 && ch < 16)
+            midiPercussivenessChannel_ = ch;
+        else
+            midiPercussivenessChannel_ = -1;
+    }
+    void disableMIDIPercussiveness() { midiPercussivenessChannel_ = -1; }
+    
+	// ***** Evaluators *****
+	
+    // This method receives triggers whenever events occur in the touch data or the
+    // continuous key position (state changes only). It alters the behavior and scheduling
+    // of the mapping but does not itself send OSC messages
+	void triggerReceived(TriggerSource* who, timestamp_type timestamp);
+	
+    // This method handles the OSC message transmission. It should be run in the Scheduler
+    // thread provided by PianoKeyboard.
+    timestamp_type performMapping();
+    
+private:
+    // ***** Private Methods *****
+
+    void generateMidiNoteOn();
+    void generateMidiNoteOff();
+    void generateMidiPercussivenessNoteOn();
+    
+	// ***** Member Variables *****
+    
+    bool noteIsOn_;                             // Whether the MIDI note is active or not
+    float aftertouchScaler_;                    // Scaler which affects aftertouch sensitivity
+    int midiChannel_;                           // Channel on which to transmit MIDI messages
+    int lastAftertouchValue_;                   // Value of the last aftertouch message
+    int midiPercussivenessChannel_;             // Whether and where to transmit percussiveness messages
+};
+
+
+#endif /* defined(__touchkeys__MIDIKeyPositionMapping__) */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/Mappings/MRPMapping.cpp	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,588 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+
+  MRPMapping.cpp: mapping class for magnetic resonator piano using continuous
+  key position.
+*/
+
+#include "MRPMapping.h"
+#include <vector>
+
+// Class constants
+// Useful constants for mapping MRP messages
+const int MRPMapping::kMIDINoteOnMessage = 0x90;
+const int MRPMapping::kDefaultMIDIChannel = 15;
+const float MRPMapping::kDefaultAftertouchScaler = 100.0;
+
+// Parameters for vibrato detection and mapping
+const key_velocity MRPMapping::kVibratoVelocityThreshold = scale_key_velocity(2.0);
+const timestamp_diff_type MRPMapping::kVibratoMinimumPeakSpacing = microseconds_to_timestamp(60000);
+const timestamp_diff_type MRPMapping::kVibratoTimeout = microseconds_to_timestamp(500000);
+const int MRPMapping::kVibratoMinimumOscillations = 4;
+const float MRPMapping::kVibratoRateScaler = 0.005;
+
+// Main constructor takes references/pointers from objects which keep track
+// of touch location, continuous key position and the state detected from that
+// position. The PianoKeyboard object is strictly required as it gives access to
+// Scheduler and OSC methods. The others are optional since any given system may
+// contain only one of continuous key position or touch sensitivity
+MRPMapping::MRPMapping(PianoKeyboard &keyboard, MappingFactory *factory, int noteNumber, Node<KeyTouchFrame>* touchBuffer,
+                     Node<key_position>* positionBuffer, KeyPositionTracker* positionTracker)
+: Mapping(keyboard, factory, noteNumber, touchBuffer, positionBuffer, positionTracker),
+  noteIsOn_(false), lastIntensity_(missing_value<float>::missing()),
+  lastBrightness_(missing_value<float>::missing()), lastPitch_(missing_value<float>::missing()),
+  lastHarmonic_(missing_value<float>::missing()),
+  shouldLookForPitchBends_(true), rawVelocity_(kMRPMappingVelocityBufferLength),
+  filteredVelocity_(kMRPMappingVelocityBufferLength, rawVelocity_), lastCalculatedVelocityIndex_(0),
+  vibratoActive_(false), vibratoVelocityPeakCount_(0), vibratoLastPeakTimestamp_(missing_value<timestamp_type>::missing())
+{
+    setAftertouchSensitivity(1.0);
+    
+    // Initialize the filter coefficients for filtered key velocity (used for vibrato detection)
+    std::vector<double> bCoeffs, aCoeffs;
+    designSecondOrderLowpass(bCoeffs, aCoeffs, 15.0, 0.707, 1000.0);
+    std::vector<float> bCf(bCoeffs.begin(), bCoeffs.end()), aCf(aCoeffs.begin(), aCoeffs.end());
+    filteredVelocity_.setCoefficients(bCf, aCf);
+}
+
+// Copy constructor
+/*MRPMapping::MRPMapping(MRPMapping const& obj)
+: Mapping(obj), lastIntensity_(obj.lastIntensity_), lastBrightness_(obj.lastBrightness_),
+aftertouchScaler_(obj.aftertouchScaler_), noteIsOn_(obj.noteIsOn_), lastPitch_(obj.lastPitch_),
+lastHarmonic_(obj.lastHarmonic_),
+shouldLookForPitchBends_(obj.shouldLookForPitchBends_), activePitchBends_(obj.activePitchBends_),
+rawVelocity_(obj.rawVelocity_), filteredVelocity_(obj.filteredVelocity_),
+lastCalculatedVelocityIndex_(obj.lastCalculatedVelocityIndex_), vibratoActive_(obj.vibratoActive_),
+vibratoVelocityPeakCount_(obj.vibratoVelocityPeakCount_), vibratoLastPeakTimestamp_(obj.vibratoLastPeakTimestamp_) {
+
+}*/
+
+MRPMapping::~MRPMapping() {
+    //std::cerr << "~MRPMapping(): " << this << std::endl;
+    
+    try {
+        disengage();
+    }
+    catch(...) {
+        std::cerr << "~MRPMapping(): exception during disengage()\n";
+    }
+    
+    //std::cerr << "~MRPMapping(): done\n";
+}
+
+// Turn off mapping of data. Remove our callback from the scheduler
+void MRPMapping::disengage() {
+    Mapping::disengage();
+    if(noteIsOn_) {
+        int newNoteNumber = noteNumber_;
+        //int newNoteNumber = ((noteNumber_ - 21) * 25)%88 + 21;
+        keyboard_.sendMessage("/mrp/midi",
+                              "iii", (int)(kMIDINoteOnMessage + kDefaultMIDIChannel), (int)newNoteNumber, (int)0, LO_ARGS_END);
+       // if(!touchBuffer_->empty())
+       //     keyboard_.testLog_ << touchBuffer_->latestTimestamp() << " /mrp/midi iii " << (kMIDINoteOnMessage + kDefaultMIDIChannel) << " " << newNoteNumber << " " << 0 << endl;
+        
+        // Reset qualities
+        lastPitch_ = lastHarmonic_ = lastBrightness_ = lastIntensity_ = missing_value<float>::missing();
+    }
+    noteIsOn_ = false;
+    shouldLookForPitchBends_ = true;
+}
+
+// Reset state back to defaults
+void MRPMapping::reset() {
+    Mapping::reset();
+    noteIsOn_ = false;
+    shouldLookForPitchBends_ = true;
+}
+
+// Set the aftertouch sensitivity on continuous key position
+// 0 means no aftertouch, 1 means default sensitivity, upward
+// from there
+void MRPMapping::setAftertouchSensitivity(float sensitivity) {
+    if(sensitivity <= 0)
+        aftertouchScaler_ = 0;
+    else
+        aftertouchScaler_ = kDefaultAftertouchScaler * sensitivity;
+}
+
+// This is called by another MRPMapping when it finds a pitch bend starting.
+// Add the sending note to our list of bends, with the sending note marked
+// as controlling the bend
+void MRPMapping::enablePitchBend(int toNote, Node<key_position>* toPositionBuffer,
+                                KeyPositionTracker *toPositionTracker) {
+    if(toPositionBuffer == 0 || toPositionTracker == 0)
+        return;
+    
+    std::cout << "enablePitchBend(): this note = " << noteNumber_ << " note = " << toNote << " posBuf = " << toPositionBuffer << " posTrack = " << toPositionTracker << "\n";
+    PitchBend newBend = {toNote, true, false, toPositionBuffer, toPositionTracker};
+    activePitchBends_.push_back(newBend);
+}
+
+// Trigger method. This receives updates from the TouchKey data or from state changes in
+// the continuous key position (KeyPositionTracker). It will potentially change the scheduled
+// behavior of future mapping calls, but the actual OSC messages should be transmitted in a different
+// thread.
+void MRPMapping::triggerReceived(TriggerSource* who, timestamp_type timestamp) {
+    if(who == 0)
+        return;
+    if(who == positionTracker_) {
+        // The state of the key (based on continuous position) just changed.
+        // Might want to alter our mapping strategy.
+    }
+    else if(who == touchBuffer_) {
+        // TouchKey data is available
+    }
+}
+
+// Mapping method. This actually does the real work of sending OSC data in response to the
+// latest information from the touch sensors or continuous key angle
+timestamp_type MRPMapping::performMapping() {
+    if(!engaged_)
+        return 0;
+    
+    timestamp_type currentTimestamp = keyboard_.schedulerCurrentTimestamp();
+    float intensity = 0;
+    float brightness = 0;
+    float pitch = 0;
+    float harmonic = 0;
+
+    // Calculate the output features as a function of input sensor data
+    if(positionBuffer_ == 0) {
+        // No buffer -> all 0
+    }
+    else if(positionBuffer_->empty()) {
+        // No samples -> all 0
+    }
+    else {
+        // TODO: IIR filter on the position data before mapping it
+        key_position latestPosition = positionBuffer_->latest();
+        int trackerState = kPositionTrackerStateUnknown;
+        if(positionTracker_ != 0)
+            trackerState = positionTracker_->currentState();
+        
+        // Get the latest velocity measurements
+        key_velocity latestVelocity = updateVelocityMeasurements();
+        
+        // Every time we enter a state of PartialPress, check whether this key
+        // is part of a multi-key pitch bend gesture with another key that's already
+        // down. Only do this once, though, since keys that go down after we enter
+        // PartialPress state are not part of such a gesture.
+        if(shouldLookForPitchBends_) {
+            if(trackerState == kPositionTrackerStatePartialPressAwaitingMax ||
+               trackerState == kPositionTrackerStatePartialPressFoundMax) {
+                // Look for a pitch bend gesture by searching for neighboring
+                // keys which are in the Down state and reached that state before
+                // this one reached PartialPress state.
+                for(int neighborNote = noteNumber_ - 2; neighborNote < noteNumber_; neighborNote++) {
+                    // If one of the lower keys is in the Down state, then this note should bend it up
+                    MRPMapping *neighborMapper = dynamic_cast<MRPMapping*>(keyboard_.mapping(neighborNote));
+                    if(neighborMapper == 0)
+                        continue;
+                    if(neighborMapper->positionTracker_ != 0) {
+                        int neighborState = neighborMapper->positionTracker_->currentState();
+                        if(neighborState == kPositionTrackerStateDown) {
+                            // Here we've found a neighboring note in the Down state. But did it precede our transition?
+                            timestamp_type timeOfDownTransition = neighborMapper->positionTracker_->latestTimestamp();
+                            timestamp_type timeOfOurPartialActivation = findTimestampOfPartialPress();
+                            
+                            cout << "Found key " << neighborNote << " in Down state\n";
+                            
+                            if(!missing_value<timestamp_type>::isMissing(timeOfOurPartialActivation)) {
+                                if(timeOfOurPartialActivation > timeOfDownTransition) {
+                                    // The neighbor note went down before us; pitch bend should engage
+                                    cout << "Found pitch bend: " << noteNumber_ << " to " << neighborNote << endl;
+                                    
+                                    // Insert the details for the neighboring note into our buffer. The bend
+                                    // is controlled by our own key, and the target is the neighbor note.
+                                    PitchBend newBend = {neighborNote, false, false, neighborMapper->positionBuffer_,
+                                        neighborMapper->positionTracker_};
+                                    activePitchBends_.push_back(newBend);
+                                
+                                    // Tell the other note to bend its pitch based on our position
+                                    neighborMapper->enablePitchBend(noteNumber_, positionBuffer_, positionTracker_);
+                                }
+                            }
+                        }
+                    }
+                }
+                for(int neighborNote = noteNumber_ + 1; neighborNote < noteNumber_ + 3; neighborNote++) {
+                    // If one of the upper keys is in the Down state, then this note should bend it down
+                    MRPMapping *neighborMapper = dynamic_cast<MRPMapping*>(keyboard_.mapping(neighborNote));
+                    if(neighborMapper == 0)
+                        continue;
+                    if(neighborMapper->positionTracker_ != 0) {
+                        int neighborState = neighborMapper->positionTracker_->currentState();
+                        if(neighborState == kPositionTrackerStateDown) {
+                            // Here we've found a neighboring note in the Down state. But did it precede our transition?
+                            timestamp_type timeOfDownTransition = neighborMapper->positionTracker_->latestTimestamp();
+                            timestamp_type timeOfOurPartialActivation = findTimestampOfPartialPress();
+                            
+                            cout << "Found key " << neighborNote << " in Down state\n";
+                            
+                            if(!missing_value<timestamp_type>::isMissing(timeOfOurPartialActivation)) {
+                                if(timeOfOurPartialActivation > timeOfDownTransition) {
+                                    // The neighbor note went down before us; pitch bend should engage
+                                    cout << "Found pitch bend: " << noteNumber_ << " to " << neighborNote << endl;
+                                    
+                                    // Insert the details for the neighboring note into our buffer. The bend
+                                    // is controlled by our own key, and the target is the neighbor note.
+                                    PitchBend newBend = {neighborNote, false, false, neighborMapper->positionBuffer_,
+                                        neighborMapper->positionTracker_};
+                                    activePitchBends_.push_back(newBend);
+                                    
+                                    // Tell the other note to bend its pitch based on our position
+                                    neighborMapper->enablePitchBend(noteNumber_, positionBuffer_, positionTracker_);
+                                }
+                            }
+                        }
+                    }
+                }
+                
+                shouldLookForPitchBends_ = false;
+            }
+        }
+        
+        if(trackerState == kPositionTrackerStatePartialPressAwaitingMax ||
+           trackerState == kPositionTrackerStatePartialPressFoundMax) {
+            // Look for active vibrato gestures which are defined as oscillating
+            // motion in the key velocity. They could conceivably occur at a variety
+            // of raw key positions, as long as the key is not yet down
+            
+            if(missing_value<timestamp_type>::isMissing(vibratoLastPeakTimestamp_))
+                vibratoLastPeakTimestamp_ = currentTimestamp;
+            
+            if(vibratoVelocityPeakCount_ % 2 == 0) {
+                if(latestVelocity > kVibratoVelocityThreshold && currentTimestamp - vibratoLastPeakTimestamp_ > kVibratoMinimumPeakSpacing) {
+                    std::cout << "Vibrato count = " << vibratoVelocityPeakCount_ << std::endl;
+                    vibratoVelocityPeakCount_++;
+                    vibratoLastPeakTimestamp_ = currentTimestamp;
+                }
+            }
+            else {
+                if(latestVelocity < -kVibratoVelocityThreshold && currentTimestamp - vibratoLastPeakTimestamp_ > kVibratoMinimumPeakSpacing) {
+                    std::cout << "Vibrato count = " << vibratoVelocityPeakCount_ << std::endl;
+                    vibratoVelocityPeakCount_++;
+                    vibratoLastPeakTimestamp_ = currentTimestamp;
+                }
+            }
+            
+            if(vibratoVelocityPeakCount_ >= kVibratoMinimumOscillations) {
+                vibratoActive_ = true;
+            }
+            
+            
+            if(vibratoActive_) {
+                // Update the harmonic parameter, which increases linearly with the absolute
+                // value of velocity. The value will accumulate over the course of a vibrato
+                // gesture and retain its value when the vibrato finishes. It reverts to minimum
+                // when the note finishes.
+                if(missing_value<float>::isMissing(lastHarmonic_))
+                    lastHarmonic_ = 0.0;
+                harmonic = lastHarmonic_ + fabsf(latestVelocity) * kVibratoRateScaler;
+                std::cout << "harmonic = " << harmonic << std::endl;
+                
+                // Check whether the current vibrato has timed out
+                if(currentTimestamp - vibratoLastPeakTimestamp_ > kVibratoTimeout) {
+                    std::cout << "Vibrato timed out\n";
+                    vibratoActive_ = false;
+                    vibratoVelocityPeakCount_ = 0;
+                    vibratoLastPeakTimestamp_ = currentTimestamp;
+                }
+            }
+        }
+        else {
+            // Vibrato can't be active in these states
+            //std::cout << "Vibrato finished from state change\n";
+            vibratoActive_ = false;
+            vibratoVelocityPeakCount_ = 0;
+            vibratoLastPeakTimestamp_ = currentTimestamp;
+        }
+        
+        if(trackerState != kPositionTrackerStateReleaseFinished) {
+            // For all active states except post-release, calculate
+            // Intensity and Brightness parameters based on key position
+            
+            if(latestPosition > 1.0) {
+                intensity = 1.0;
+                brightness = (latestPosition - 1.0) * aftertouchScaler_;
+            }
+            else if(latestPosition < 0.0) {
+                intensity = 0.0;
+                brightness = 0.0;
+            }
+            else {
+                intensity = latestPosition;
+                brightness = 0.0;
+            }
+            
+            if(!activePitchBends_.empty()) {
+                // Look for active multi-key pitch bend gestures
+                std::vector<PitchBend>::iterator it = activePitchBends_.begin();
+                pitch = 0.0;
+                
+                for(it = activePitchBends_.begin(); it != activePitchBends_.end(); it++) {
+                    PitchBend& bend(*it);
+
+                    if(bend.isControllingBend) {
+                        // First find out of the bending key is still in a PartialPress state
+                        // If not, remove it and move on
+                        if((bend.positionTracker->currentState() != kPositionTrackerStatePartialPressAwaitingMax &&
+                           bend.positionTracker->currentState() != kPositionTrackerStatePartialPressFoundMax)
+                           || !bend.positionTracker->engaged()) {
+                            cout << "Removing bend from note " << bend.note << endl;
+                            bend.isFinished = true;
+                            continue;
+                        }
+                        
+                        // This is the case where the other note is controlling our pitch
+                        if(bend.positionBuffer->empty()) {
+                            continue;
+                        }
+                        
+                        float noteDifference = (float)(bend.note - noteNumber_);
+                        key_position latestBenderPosition = bend.positionBuffer->latest();
+                        
+                        // Key position at 0 = 0 pitch bend; key position at max = most pitch bend
+                        float bendAmount = key_position_to_float(latestBenderPosition - kPianoKeyDefaultIdlePositionThreshold*2) /
+                                                key_position_to_float(1.0 - kPianoKeyDefaultIdlePositionThreshold*2);
+                        if(bendAmount < 0)
+                            bendAmount = 0;
+                        pitch += noteDifference * bendAmount;
+                    }
+                    else {
+                        // This is the case where we're controlling the other note's pitch. Our own
+                        // pitch is the inverse of what we're sending to the neighboring note.
+                        // Compared to the above case, we know a few things since we're using our own
+                        // position: the buffer isn't empty and the tracker is engaged.
+                        
+                        if(trackerState != kPositionTrackerStatePartialPressAwaitingMax &&
+                           trackerState != kPositionTrackerStatePartialPressFoundMax) {
+                            cout << "Removing our bend on note " << bend.note << endl;
+                            bend.isFinished = true;
+                            continue;
+                        }
+  
+                        float noteDifference = (float)(bend.note - noteNumber_);
+                        
+                        // Key position at 0 = 0 pitch bend; key position at max = most pitch bend
+                        float bendAmount = key_position_to_float(latestPosition - kPianoKeyDefaultIdlePositionThreshold*2) /
+                                            key_position_to_float(1.0 - kPianoKeyDefaultIdlePositionThreshold*2);
+                        if(bendAmount < 0)
+                            bendAmount = 0;
+                        pitch += noteDifference * (1.0 - bendAmount);
+                    }
+                }
+                
+                // Now reiterate to remove any of them that have finished
+                it = activePitchBends_.begin();
+                
+                while(it != activePitchBends_.end()) {
+                    if(it->isFinished) {
+                        // Go back to beginning and look again after erasing each one
+                        // This isn't very efficient but there will never be more than 4 elements anyway
+                        activePitchBends_.erase(it);
+                        it = activePitchBends_.begin();
+                    }
+                    else
+                        it++;
+                }
+                
+                std::cout << "pitch = " << pitch << std::endl;
+            }
+            else
+                pitch = 0.0;
+        }
+        else {
+            intensity = 0.0;
+            brightness = 0.0;
+            if(noteIsOn_) {
+                        int newNoteNumber = noteNumber_;
+                //int newNoteNumber = ((noteNumber_ - 21) * 25)%88 + 21;
+                keyboard_.sendMessage("/mrp/midi",
+                                      "iii", (int)(kMIDINoteOnMessage + kDefaultMIDIChannel), (int)newNoteNumber, (int)0, LO_ARGS_END);
+                //keyboard_.testLog_ << currentTimestamp << " /mrp/midi iii " << (kMIDINoteOnMessage + kDefaultMIDIChannel) << " " << newNoteNumber << " " << 0 << endl;
+            }
+            noteIsOn_ = false;
+            shouldLookForPitchBends_ = true;
+        }
+    }
+    
+    // TODO: TouchKeys mapping
+    
+    // Send OSC message with these parameters unless they are unchanged from before
+    if(!noteIsOn_ && intensity > 0.0) {
+                int newNoteNumber = noteNumber_;
+        //int newNoteNumber = ((noteNumber_ - 21) * 25)%88 + 21;
+        keyboard_.sendMessage("/mrp/midi",
+                              "iii", (int)(kMIDINoteOnMessage + kDefaultMIDIChannel), (int)newNoteNumber, (int)127, LO_ARGS_END);
+        //keyboard_.testLog_ << currentTimestamp << " /mrp/midi iii " << (kMIDINoteOnMessage + kDefaultMIDIChannel) << " " << newNoteNumber << " " << 127 << endl;
+        noteIsOn_ = true;
+    }
+    
+    // Set key LED color according to key parameters
+    // Partial press --> green of varying intensity
+    // Aftertouch (brightness) --> green moving to red depending on brightness parameter
+    // Pitch bend --> note bends toward blue as pitch value departs from center
+    // Harmonic glissando --> cycle through hues with whitish tint (lower saturation)
+    if(intensity != lastIntensity_ || brightness != lastBrightness_ || pitch != lastPitch_ || harmonic != lastHarmonic_) {
+        if(harmonic != 0.0) {
+            float hue = fmodf(harmonic, 1.0);
+            keyboard_.setKeyLEDColorHSV(noteNumber_, hue, 0.25, 0.5);
+        }
+        else if(intensity >= 1.0) {
+            if(pitch != 0.0)
+                keyboard_.setKeyLEDColorHSV(noteNumber_, 0.33 + 0.33 * fabsf(pitch) - (brightness * 0.2), 1.0, intensity);
+            else
+                keyboard_.setKeyLEDColorHSV(noteNumber_, 0.33 - (brightness * 0.2), 1.0, 1.0);
+        }
+        else {
+            if(pitch != 0.0)
+                keyboard_.setKeyLEDColorHSV(noteNumber_, 0.33 + 0.33 * fabsf(pitch), 1.0, intensity);
+            else
+                keyboard_.setKeyLEDColorHSV(noteNumber_, 0.33, 1.0, intensity);
+        }
+    }
+        
+    if(intensity != lastIntensity_) {
+                int newNoteNumber = noteNumber_;
+        //int newNoteNumber = ((noteNumber_ - 21) * 25)%88 + 21;
+        keyboard_.sendMessage("/mrp/quality/intensity",
+                              "iif", (int)kDefaultMIDIChannel, (int)newNoteNumber, (float)intensity, LO_ARGS_END);
+        //keyboard_.testLog_ << currentTimestamp << " /mrp/quality/intensity iif " << kDefaultMIDIChannel << " " << newNoteNumber << " " << intensity << endl;
+    }
+    if(brightness != lastBrightness_) {
+                int newNoteNumber = noteNumber_;
+        //int newNoteNumber = ((noteNumber_ - 21) * 25)%88 + 21;
+        keyboard_.sendMessage("/mrp/quality/brightness",
+                              "iif", (int)kDefaultMIDIChannel, (int)newNoteNumber, (float)brightness, LO_ARGS_END);
+        //keyboard_.testLog_ << currentTimestamp << " /mrp/quality/brightness iif " << kDefaultMIDIChannel << " " << newNoteNumber << " " << brightness << endl;
+    }
+    if(pitch != lastPitch_) {
+                int newNoteNumber = noteNumber_;
+        //int newNoteNumber = ((noteNumber_ - 21) * 25)%88 + 21;
+        keyboard_.sendMessage("/mrp/quality/pitch",
+                              "iif", (int)kDefaultMIDIChannel, (int)newNoteNumber, (float)pitch, LO_ARGS_END);
+        //keyboard_.testLog_ << currentTimestamp << " /mrp/quality/pitch iif " << kDefaultMIDIChannel << " " << newNoteNumber << " " << pitch << endl;
+    }
+    if(harmonic != lastHarmonic_) {
+        int newNoteNumber = noteNumber_;
+        //int newNoteNumber = ((noteNumber_ - 21) * 25)%88 + 21;
+        keyboard_.sendMessage("/mrp/quality/harmonic",
+                              "iif", (int)kDefaultMIDIChannel, (int)newNoteNumber, (float)harmonic, LO_ARGS_END);
+        //keyboard_.testLog_ << currentTimestamp << " /mrp/quality/harmonic iif " << kDefaultMIDIChannel << " " << newNoteNumber << " " << harmonic << endl;
+    }
+    
+    lastIntensity_ = intensity;
+    lastBrightness_ = brightness;
+    lastPitch_ = pitch;
+    lastHarmonic_ = harmonic;
+    
+    // Register for the next update by returning its timestamp
+    nextScheduledTimestamp_ = currentTimestamp + updateInterval_;
+    return nextScheduledTimestamp_;
+}
+
+// Helper function that brings the velocity buffer up to date with the latest
+// samples. Velocity is not updated on every new position sample since it's not
+// efficient to run that many triggers all the time. Instead, it's brought up to
+// date on an as-needed basis during performMapping().
+key_velocity MRPMapping::updateVelocityMeasurements() {
+    positionBuffer_->lock_mutex();
+    
+    // Need at least 2 samples to calculate velocity (first difference)
+    if(positionBuffer_->size() < 2) {
+        positionBuffer_->unlock_mutex();
+        return missing_value<key_velocity>::missing();
+    }
+    
+    if(lastCalculatedVelocityIndex_ < positionBuffer_->beginIndex() + 1) {
+        // Fell off the beginning of the position buffer. Reset calculations.
+        filteredVelocity_.clear();
+        rawVelocity_.clear();
+        lastCalculatedVelocityIndex_ = positionBuffer_->beginIndex() + 1;
+    }
+    
+    while(lastCalculatedVelocityIndex_ < positionBuffer_->endIndex()) {
+        // Calculate the velocity and add to buffer
+        key_position diffPosition = (*positionBuffer_)[lastCalculatedVelocityIndex_] - (*positionBuffer_)[lastCalculatedVelocityIndex_ - 1];
+        timestamp_diff_type diffTimestamp = positionBuffer_->timestampAt(lastCalculatedVelocityIndex_) - positionBuffer_->timestampAt(lastCalculatedVelocityIndex_ - 1);
+        key_velocity vel;
+        
+        if(diffTimestamp != 0)
+            vel = calculate_key_velocity(diffPosition, diffTimestamp);
+        else
+            vel = 0; // Bad measurement: replace with 0 so as not to mess up IIR calculations
+        
+        // Add the raw velocity to the buffer
+        rawVelocity_.insert(vel, positionBuffer_->timestampAt(lastCalculatedVelocityIndex_));
+        lastCalculatedVelocityIndex_++;
+    }
+    
+    positionBuffer_->unlock_mutex();
+    
+    // Bring the filtered velocity up to date
+    key_velocity filteredVel = filteredVelocity_.calculate();
+    //std::cout << "Key " << noteNumber_ << " velocity " << filteredVel << std::endl;
+    return filteredVel;
+}
+
+// Helper function that locates the timestamp at which this key entered the
+// PartialPress (i.e. first non-idle) state. Returns missing value if the
+// state can't be located.
+timestamp_type MRPMapping::findTimestampOfPartialPress() {
+    if(positionTracker_ == 0)
+        return missing_value<timestamp_type>::missing();
+    if(positionTracker_->empty())
+        return missing_value<timestamp_type>::missing();
+    //Node<int>::reverse_iterator it = positionTracker_->rbegin();
+    Node<int>::size_type index = positionTracker_->endIndex() - 1;
+    bool foundPartialPressState = false;
+    timestamp_type earliestPartialPressTimestamp;
+
+    // Search backwards from present
+    while(index >= positionTracker_->beginIndex()/*it != positionTracker_->rend()*/) {
+        if((*positionTracker_)[index].state == kPositionTrackerStatePartialPressAwaitingMax ||
+           (*positionTracker_)[index].state == kPositionTrackerStatePartialPressFoundMax) {
+            cout << "index " << index << " state " << (*positionTracker_)[index].state << endl;
+            foundPartialPressState = true;
+            earliestPartialPressTimestamp = positionTracker_->timestampAt(index);
+        }
+        else {
+            // This state is not a PartialPress state. Two cases: either
+            // we haven't yet encountered a partial press or we have found
+            // a state before the partial press, in which case the previous
+            // state we found was the first.
+                        cout << "index " << index << " state " << (*positionTracker_)[index].state << endl;
+            if(foundPartialPressState) {
+                return earliestPartialPressTimestamp;
+            }
+        }
+        
+        // Step backwards one sample, but stop if we hit the beginning index
+        if(index == 0)
+            break;
+        index--;
+    }
+    
+    if(foundPartialPressState)
+        return earliestPartialPressTimestamp;
+    
+    // Didn't find anything if we get here
+    return missing_value<timestamp_type>::missing();
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/Mappings/MRPMapping.h	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,151 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+
+  MRPMapping.h: mapping class for magnetic resonator piano using continuous
+  key position.
+*/
+
+#ifndef __touchkeys__MRPMapping__
+#define __touchkeys__MRPMapping__
+
+#include <map>
+#include <boost/bind.hpp>
+#include "../TouchKeys/KeyTouchFrame.h"
+#include "../TouchKeys/KeyPositionTracker.h"
+#include "../TouchKeys/PianoKeyboard.h"
+#include "Mapping.h"
+#include "../Utility/IIRFilter.h"
+
+// How many velocity samples to save in the buffer. Make sure this is
+// enough to cover the frequency of updates.
+const int kMRPMappingVelocityBufferLength = 30;
+
+// This class handles the mapping from key position and, optionally,
+// touch information to OSC messages which control the magnetic resonator
+// piano. One copy of the object is created for each active note, and
+// all objects use the PianoKeyboard Scheduler facility to request timed
+// updates.
+
+class MRPMapping : public Mapping {
+private:
+    /*// Useful constants for mapping MRP messages
+    const int kMIDINoteOnMessage = 0x90;
+    const int kDefaultMIDIChannel = 15;
+    const float kDefaultAftertouchScaler = 100.0;
+    
+    // Parameters for vibrato detection and mapping
+    const key_velocity kVibratoVelocityThreshold = scale_key_velocity(2.0);
+    const timestamp_diff_type kVibratoMinimumPeakSpacing = microseconds_to_timestamp(60000);
+    const timestamp_diff_type kVibratoTimeout = microseconds_to_timestamp(500000);
+    const int kVibratoMinimumOscillations = 4;
+    const float kVibratoRateScaler = 0.005;*/
+    // Useful constants for mapping MRP messages
+    static const int kMIDINoteOnMessage;
+    static const int kDefaultMIDIChannel;
+    static const float kDefaultAftertouchScaler;
+    
+    // Parameters for vibrato detection and mapping
+    static const key_velocity kVibratoVelocityThreshold;
+    static const timestamp_diff_type kVibratoMinimumPeakSpacing;
+    static const timestamp_diff_type kVibratoTimeout;
+    static const int kVibratoMinimumOscillations;
+    static const float kVibratoRateScaler;
+    
+    struct PitchBend {
+        int note;                               // Note number of the bending key
+        bool isControllingBend;                 // True if the note in this structure
+                                                // is the one controlling bend (false if it's us)
+        bool isFinished;                        // True if the bend should finish after this cycle
+        Node<key_position>* positionBuffer;     // Key position for bending key
+        KeyPositionTracker* positionTracker;    // Key states for bending key
+    };
+    
+public:
+	// ***** Constructors *****
+	
+	// Default constructor, passing the buffer on which to trigger
+	MRPMapping(PianoKeyboard &keyboard, MappingFactory *factory, int noteNumber, Node<KeyTouchFrame>* touchBuffer,
+              Node<key_position>* positionBuffer, KeyPositionTracker* positionTracker);
+	
+	// Copy constructor
+	//MRPMapping(MRPMapping const& obj);
+    
+    // ***** Destructor *****
+    
+    ~MRPMapping();
+	
+    // ***** Modifiers *****
+    
+    // Disable mappings from being sent
+    void disengage();
+	
+    // Reset the state back initial values
+	void reset();
+    
+    // Set the aftertouch sensitivity on continuous key position
+    // 0 means no aftertouch, 1 means default sensitivity, upward
+    // from there
+    void setAftertouchSensitivity(float sensitivity);
+    
+    // Engage a pitch bend from a different key, based on its position and state
+    void enablePitchBend(int toNote, Node<key_position>* toPositionBuffer,
+                         KeyPositionTracker *toPositionTracker);
+    
+	// ***** Evaluators *****
+	
+    // This method receives triggers whenever events occur in the touch data or the
+    // continuous key position (state changes only). It alters the behavior and scheduling
+    // of the mapping but does not itself send OSC messages
+	void triggerReceived(TriggerSource* who, timestamp_type timestamp);
+	
+    // This method handles the OSC message transmission. It should be run in the Scheduler
+    // thread provided by PianoKeyboard.
+    timestamp_type performMapping();
+    
+private:
+    // ***** Private Methods *****
+    
+    // Bring velocity calculations up to date
+    key_velocity updateVelocityMeasurements();
+    
+    // Find the timestamp of the first transition into a PartialPress state
+    timestamp_type findTimestampOfPartialPress();
+    
+	// ***** Member Variables *****
+    
+    bool noteIsOn_;                             // Whether the MIDI note is active or not
+    float aftertouchScaler_;                    // Scaler which affects aftertouch sensitivity
+    float lastIntensity_, lastBrightness_;      // Cached values for mapping qualities
+    float lastPitch_, lastHarmonic_;
+    
+    bool shouldLookForPitchBends_;              // Whether to search for adjacent keys to start a pitch bend
+    std::vector<PitchBend> activePitchBends_;   // Which keys are involved in a pitch bend
+    
+    Node<key_velocity> rawVelocity_;            // History of key velocity measurements
+    IIRFilterNode<key_velocity> filteredVelocity_;  // Filtered key velocity information
+    Node<key_position>::size_type lastCalculatedVelocityIndex_; // Keep track of how many velocity samples we've calculated
+    
+    bool vibratoActive_;                        // Whether a vibrato gesture is currently detected
+    int vibratoVelocityPeakCount_;              // Counter for tracking velocity oscillations
+    timestamp_type vibratoLastPeakTimestamp_;   // When the last velocity peak took place
+    
+    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(MRPMapping)
+};
+
+#endif /* defined(__touchkeys__MRPMapping__) */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/Mappings/Mapping.cpp	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,128 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+
+  Mapping.cpp: base class for a single-note mapping. The mapping will take in
+  MIDI, touch and (optionally) continuous key position data, and generate
+  specific OSC or MIDI messages. TouchKeys-specific mappings generally
+  inherit from the TouchkeyBaseMapping subclass, which provides other
+  useful generic methods.
+*/
+
+#include "Mapping.h"
+#include "MappingFactory.h"
+#include "MappingScheduler.h"
+
+// Class constants
+const timestamp_diff_type Mapping::kDefaultUpdateInterval = microseconds_to_timestamp(5500);
+
+// Main constructor takes references/pointers from objects which keep track
+// of touch location, continuous key position and the state detected from that
+// position. The PianoKeyboard object is strictly required as it gives access to
+// Scheduler and OSC methods. The others are optional since any given system may
+// contain only one of continuous key position or touch sensitivity
+Mapping::Mapping(PianoKeyboard &keyboard, MappingFactory *factory, int noteNumber, Node<KeyTouchFrame>* touchBuffer,
+                       Node<key_position>* positionBuffer, KeyPositionTracker* positionTracker)
+: keyboard_(keyboard), factory_(factory), noteNumber_(noteNumber), touchBuffer_(touchBuffer),
+positionBuffer_(positionBuffer), positionTracker_(positionTracker), engaged_(false),
+suspended_(false), updateInterval_(kDefaultUpdateInterval),
+nextScheduledTimestamp_(0)
+{
+    // Create a statically bound call to the performMapping() method that
+    // we use each time we schedule a new mapping
+    mappingAction_ = boost::bind(&Mapping::performMapping, this);
+}
+
+// Copy constructor
+Mapping::Mapping(Mapping const& obj) : keyboard_(obj.keyboard_), factory_(obj.factory_), noteNumber_(obj.noteNumber_),
+touchBuffer_(obj.touchBuffer_), positionBuffer_(obj.positionBuffer_), positionTracker_(obj.positionTracker_),
+engaged_(obj.engaged_), updateInterval_(obj.updateInterval_),
+nextScheduledTimestamp_(obj.nextScheduledTimestamp_)
+{
+    // Create a statically bound call to the performMapping() method that
+    // we use each time we schedule a new mapping
+    mappingAction_ = boost::bind(&Mapping::performMapping, this);
+    
+    // Register ourself if already engaged since the scheduler won't have a copy of this object
+    if(engaged_) {
+#ifdef NEW_MAPPING_SCHEDULER
+        keyboard_.mappingScheduler().scheduleNow(this);
+#else
+        keyboard_.scheduleEvent(this, mappingAction_, keyboard_.schedulerCurrentTimestamp());
+#endif
+    }
+}
+
+// Destructor. IMPORTANT NOTE: any derived class of Mapping() needs to call disengage() in its
+// own destructor. It can't be called here, or there is a risk that the scheduled action will be
+// called between the destruction of the derived class and the destruction of Mapping. This
+// will result in a pure virtual function call and a crash.
+Mapping::~Mapping() {
+    //std::cerr << "~Mapping(): " << this << std::endl;
+}
+
+// Turn on mapping of data. Register for a callback and set a flag so
+// we continue to receive updates
+void Mapping::engage() {
+    engaged_ = true;
+    
+    //cout << "Mapping::engage(): before TS " << keyboard_.schedulerCurrentTimestamp() << std::endl;
+    // Register for trigger updates from touch data and state updates if either one is present.
+    // Don't register for triggers on each new key sample
+    if(touchBuffer_ != 0)
+        registerForTrigger(touchBuffer_);
+    if(positionTracker_ != 0)
+        registerForTrigger(positionTracker_);
+    nextScheduledTimestamp_ = keyboard_.schedulerCurrentTimestamp();
+    //cout << "Mapping::engage(): mid TS " << keyboard_.schedulerCurrentTimestamp() << std::endl;
+#ifdef NEW_MAPPING_SCHEDULER
+    keyboard_.mappingScheduler().registerMapping(this);
+    keyboard_.mappingScheduler().scheduleNow(this);
+#else
+    keyboard_.scheduleEvent(this, mappingAction_, nextScheduledTimestamp_);
+#endif
+    //cout << "Mapping::engage(): after TS " << keyboard_.schedulerCurrentTimestamp() << std::endl;
+}
+
+// Turn off mapping of data. Remove our callback from the scheduler
+void Mapping::disengage(bool shouldDelete) {
+    //std::cerr << "Mapping::disengage(): " << this << std::endl;
+    
+    engaged_ = false;
+#ifndef NEW_MAPPING_SCHEDULER
+    keyboard_.unscheduleEvent(this/*, nextScheduledTimestamp_*/);
+#endif
+    // Unregister for updates from touch data
+    if(touchBuffer_ != 0)
+        unregisterForTrigger(touchBuffer_);
+    if(positionTracker_ != 0)
+        unregisterForTrigger(positionTracker_);
+    //std::cerr << "Mapping::disengage(): done\n";
+    
+#ifdef NEW_MAPPING_SCHEDULER
+    if(shouldDelete)
+        keyboard_.mappingScheduler().unregisterAndDelete(this);
+    else
+        keyboard_.mappingScheduler().unregisterMapping(this);
+#endif
+}
+
+// Reset state back to defaults
+void Mapping::reset() {
+    updateInterval_ = kDefaultUpdateInterval;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/Mappings/Mapping.h	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,131 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+
+  Mapping.h: base class for a single-note mapping. The mapping will take in
+  MIDI, touch and (optionally) continuous key position data, and generate
+  specific OSC or MIDI messages. TouchKeys-specific mappings generally
+  inherit from the TouchkeyBaseMapping subclass, which provides other
+  useful generic methods.
+*/
+
+#ifndef touchkeys_Mapping_h
+#define touchkeys_Mapping_h
+
+
+#include <map>
+#include <boost/bind.hpp>
+#include "../TouchKeys/KeyTouchFrame.h"
+#include "../TouchKeys/KeyPositionTracker.h"
+#include "../TouchKeys/PianoKeyboard.h"
+
+#define NEW_MAPPING_SCHEDULER
+
+class MappingFactory;
+
+// This virtual base class defines a mapping from keyboard data to OSC or
+// other output information. Specific behavior is implemented by subclasses.
+
+class Mapping : public TriggerDestination {
+protected:
+    // Default frequency of mapping data, in the absence of other triggers
+    //const timestamp_diff_type kDefaultUpdateInterval = microseconds_to_timestamp(5500);
+    static const timestamp_diff_type kDefaultUpdateInterval;
+    
+public:
+	// ***** Constructors *****
+	
+	// Default constructor, passing the buffer on which to trigger
+    Mapping(PianoKeyboard &keyboard, MappingFactory *factory, int noteNumber, Node<KeyTouchFrame>* touchBuffer,
+                       Node<key_position>* positionBuffer, KeyPositionTracker* positionTracker);
+	
+	// Copy constructor
+    Mapping(Mapping const& obj);
+    
+    // ***** Destructor *****
+    
+    virtual ~Mapping();
+	
+    // ***** Modifiers *****
+    
+    // Enable mappings to be sent
+    virtual void engage();
+    
+    // Disable mappings from being sent
+    virtual void disengage(bool shouldDelete = false);
+	
+    // Reset the state back initial values
+	virtual void reset();
+    
+    // Set the interval between mapping actions
+    virtual void setUpdateInterval(timestamp_diff_type interval) {
+        if(interval <= 0)
+            return;
+        updateInterval_ = interval;
+    }
+    
+    // Suspend any further messages from this mapping
+    virtual void suspend() { suspended_ = true; }
+    
+    // Resume sending messages, optionally re-sending the current state
+    virtual void resume(bool resendCurrentState) {
+        if(resendCurrentState)
+            resend();
+        suspended_ = false;
+    }
+    
+    // Resend the current state of all the managed parameters
+    virtual void resend() {}
+
+	// ***** Evaluators *****
+	// These are the main mapping functions, and they need to be implemented
+    // specifically in any subclass.
+    
+    // This method receives triggers whenever events occur in the touch data or the
+    // continuous key position (state changes only).
+	virtual void triggerReceived(TriggerSource* who, timestamp_type timestamp) = 0;
+	
+    // This method is run periodically the Scheduler provided by PianoKeyboard and
+    // handles the actual work of performing the mapping.
+    virtual timestamp_type performMapping() = 0;
+    
+    // Indicate whether the mapping has finished all of its processing and can be deleted.
+    // By default a mapping can be deleted whenever the factory wants to, but some
+    // may persist beyond note release for a limited period of time.
+    virtual bool requestFinish() { return true; }
+    
+protected:
+    
+	// ***** Member Variables *****
+	
+    PianoKeyboard& keyboard_;                   // Reference to the main keyboard controller
+    MappingFactory *factory_;                   // Factory that created this mapping
+    int noteNumber_;                            // MIDI note number for this key
+    Node<KeyTouchFrame>* touchBuffer_;          // Key touch location history
+	Node<key_position>* positionBuffer_;		// Raw key position data
+    KeyPositionTracker* positionTracker_;       // Object which manages states of key
+    
+    bool engaged_;                              // Whether we're actively mapping
+    bool suspended_;                            // Whether we're suppressing messages
+    timestamp_diff_type updateInterval_;        // How long between mapping calls
+    timestamp_type nextScheduledTimestamp_;     // When we've asked for the next callback
+    Scheduler::action mappingAction_;           // Action function which calls performMapping()
+};
+
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/Mappings/MappingFactory.h	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,146 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+
+  MappingFactory.h: base class for creating mappings. A factory is a singular
+  object, attached to a particular keyboard segment, which in turn allocates
+  and destroys individual Mapping objects for each active note. The factory
+  also is the usual point at which parameter changes are made.
+*/
+
+#ifndef __touchkeys__MappingFactory__
+#define __touchkeys__MappingFactory__
+
+#include <map>
+#include <boost/bind.hpp>
+#include "Mapping.h"
+#include "../GUI/MappingEditorComponent.h"
+
+// This virtual base class defines a singular factory object from which individual
+// instances of mapping objects can be created and destroyed. How the mappings are
+// allocated and when is up to the factory, which also keeps track of which ones
+// are active. The PianoKey class will call into any active factories when certain
+// events occur: touch on/off, MIDI on/off, key idle/active.
+
+class MappingFactory {
+public:
+    // States for bypass status
+    enum {
+        kBypassOff = 0,
+        kBypassOn,
+        kBypassMixed
+    };
+    
+    // ***** Constructor *****
+    
+	// Default constructor, containing a reference to the PianoKeyboard class.
+    MappingFactory(PianoKeyboard &keyboard) : keyboard_(keyboard) {}
+	
+    // ***** Destructor *****
+    
+    virtual ~MappingFactory() {}
+    
+    // ***** Accessors / Modifiers *****
+    
+    // Generic name for this type of factory
+    virtual const std::string factoryTypeName() { return "Unknown\nMapping"; }
+    
+    // Specific name for this particular factory
+    virtual string const getName() { return ""; }
+    virtual void setName(const string& name) {}
+    
+    virtual Mapping* mapping(int noteNumber) = 0;      // Look up a mapping with the given note number
+    virtual std::vector<int> activeMappings() = 0;     // Return a list of all active notes
+    
+    virtual void removeAllMappings() = 0;              // Remove all active mappings
+    virtual void mappingFinished(int noteNumber) = 0;  // Callback from mapping to say it's done
+    
+    // Suspending mappings is a state managed internally by the TouchKeys
+    // controllers, for example to turn off a mapping of an older note in monophonic
+    // mode. By contrast, bypassing a mapping is intended to be manipulated from
+    // an external UI.
+    
+    virtual void suspendMapping(int noteNumber) = 0;    // Suspend messages from a particular note
+    virtual void suspendAllMappings() = 0;              // ... or all notes
+    virtual void resumeMapping(int noteNumber, bool resend) = 0;  // Resume messages from a particular note
+    virtual void resumeAllMappings(bool resend) = 0;              // ... or all notes
+    
+    virtual int bypassed() = 0;                     // Whether this mapping is bypassed
+    virtual void setBypassed(bool bypass) = 0;      // Set whether the mapping is bypassed or not
+    
+    // ***** State Updaters *****
+    
+    // These are called by PianoKey whenever certain events occur that might
+    // merit the start and stop of a mapping. What is done with them depends on
+    // the particular factory subclass. The relevant buffers are passed in each
+    // time so the factory or the mapping can make use of them
+    
+    // Touch becomes active on a key where it wasn't previously
+    virtual void touchBegan(int noteNumber, bool midiNoteIsOn, bool keyMotionActive,
+                            Node<KeyTouchFrame>* touchBuffer,
+                            Node<key_position>* positionBuffer,
+                            KeyPositionTracker* positionTracker) = 0;
+    // Touch ends on a key where it wasn't previously
+    virtual void touchEnded(int noteNumber, bool midiNoteIsOn, bool keyMotionActive,
+                            Node<KeyTouchFrame>* touchBuffer,
+                            Node<key_position>* positionBuffer,
+                            KeyPositionTracker* positionTracker) = 0;
+    // MIDI note on for a key
+    virtual void midiNoteOn(int noteNumber, bool touchIsOn, bool keyMotionActive,
+                            Node<KeyTouchFrame>* touchBuffer,
+                            Node<key_position>* positionBuffer,
+                            KeyPositionTracker* positionTracker) = 0;
+    // MIDI note off for a key
+    virtual void midiNoteOff(int noteNumber, bool touchIsOn, bool keyMotionActive,
+                             Node<KeyTouchFrame>* touchBuffer,
+                             Node<key_position>* positionBuffer,
+                             KeyPositionTracker* positionTracker) = 0;
+    // Key goes active from continuous key position
+    virtual void keyMotionActive(int noteNumber, bool midiNoteIsOn, bool touchIsOn,
+                                 Node<KeyTouchFrame>* touchBuffer,
+                                 Node<key_position>* positionBuffer,
+                                 KeyPositionTracker* positionTracker) = 0;
+    // Key goes idle from continuous key position
+    virtual void keyMotionIdle(int noteNumber, bool midiNoteIsOn, bool touchIsOn,
+                               Node<KeyTouchFrame>* touchBuffer,
+                               Node<key_position>* positionBuffer,
+                               KeyPositionTracker* positionTracker) = 0;
+    
+    // Notification from key that a note is about to be sent out
+    virtual void noteWillBegin(int noteNumber, int midiChannel, int midiVelocity) = 0;
+    
+    // ***** GUI Support *****
+    // There are two types of editors for a mapping: one is a small editor that fits in the
+    // list view for adjusting the most important parameters, the other goes in a window of
+    // its own to adjust every parameter.
+    
+    virtual bool hasBasicEditor() { return false; }
+    virtual MappingEditorComponent* createBasicEditor() { return nullptr; }
+    virtual bool hasExtendedEditor() { return false; }
+    virtual MappingEditorComponent* createExtendedEditor() { return nullptr; }
+    
+protected:
+	// ***** Member Variables *****
+	
+    PianoKeyboard& keyboard_;                   // Reference to the main keyboard controller
+    
+private:
+    //JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MappingFactory)
+};
+
+#endif /* defined(__touchkeys__MappingFactory__) */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/Mappings/MappingFactorySplitter.cpp	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,231 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+
+  MappingFactorySplitter.cpp: MappingFactory subclass which in turn contains
+  several factories, routing all incoming method calls to each of them.
+*/
+
+#include "MappingFactorySplitter.h"
+
+// Look up a mapping with the given note number
+Mapping* MappingFactorySplitter::mapping(int noteNumber) {
+    // TODO: find a mapping in any of the factories
+    if(factories_.empty())
+        return 0;
+    return (*factories_.begin())->mapping(noteNumber);
+}
+
+// Return a list of all active notes
+std::vector<int> MappingFactorySplitter::activeMappings() {
+    // TODO: merge all active factories
+    if(factories_.empty()) {
+        return std::vector<int>();
+    }
+    return (*factories_.begin())->activeMappings();
+}
+
+// Remove all active mappings
+void MappingFactorySplitter::removeAllMappings() {
+    std::list<MappingFactory*>::iterator it;
+    
+    for(it = factories_.begin(); it != factories_.end(); ++it) {
+        (*it)->removeAllMappings();
+    }
+}
+
+// Suspend messages from a particular note
+void MappingFactorySplitter::suspendMapping(int noteNumber) {
+    std::list<MappingFactory*>::iterator it;
+    
+    for(it = factories_.begin(); it != factories_.end(); ++it) {
+        (*it)->suspendMapping(noteNumber);
+    }
+}
+
+// Suspend messages from all notes
+void MappingFactorySplitter::suspendAllMappings() {
+    std::list<MappingFactory*>::iterator it;
+    
+    for(it = factories_.begin(); it != factories_.end(); ++it) {
+        (*it)->suspendAllMappings();
+    }
+}
+
+// Resume messages from a particular note
+void MappingFactorySplitter::resumeMapping(int noteNumber, bool resend) {
+    std::list<MappingFactory*>::iterator it;
+    
+    for(it = factories_.begin(); it != factories_.end(); ++it) {
+        (*it)->resumeMapping(noteNumber, resend);
+    }
+}
+
+// Resume messages from all notes
+void MappingFactorySplitter::resumeAllMappings(bool resend) {
+    std::list<MappingFactory*>::iterator it;
+    
+    for(it = factories_.begin(); it != factories_.end(); ++it) {
+        (*it)->resumeAllMappings(resend);
+    }
+}
+
+// Whether the component parts of this mapping are bypassed
+int MappingFactorySplitter::bypassed() {
+    if(factories_.empty())
+        return bypassNewMappings_;
+    bool bypassOn = false, bypassOff = false;
+    
+    std::list<MappingFactory*>::iterator it;
+    for(it = factories_.begin(); it != factories_.end(); ++it) {
+        if((*it)->bypassed())
+            bypassOn = true;
+        else
+            bypassOff = true;
+    }
+    
+    // Three states: all on, all off, or some on + some off (mixed)
+    if(bypassOn && !bypassOff)
+        return kBypassOn;
+    if(bypassOff && !bypassOn)
+        return kBypassOff;
+    return kBypassMixed;
+}
+
+// Set whether the component parts are bypassed
+void MappingFactorySplitter::setBypassed(bool bypass) {
+    std::list<MappingFactory*>::iterator it;
+    for(it = factories_.begin(); it != factories_.end(); ++it) {
+        (*it)->setBypassed(bypass);
+    }
+    bypassNewMappings_ = bypass;
+}
+
+// Add a factory to the list
+void MappingFactorySplitter::addFactory(MappingFactory* factory) {
+    if(bypassNewMappings_)
+        factory->setBypassed(true);
+    factories_.push_back(factory);
+}
+
+// Remove a factory from the list
+void MappingFactorySplitter::removeFactory(MappingFactory* factory) {
+    // Assume allocation/deallocation takes place elsewhere
+    std::list<MappingFactory*>::iterator it = factories_.begin();
+    
+    while(it != factories_.end()) {
+        if(*it == factory)
+            factories_.erase(it++);
+        else
+            it++;
+    }
+}
+
+// Remove all factories from the list
+void MappingFactorySplitter::removeAllFactories() {
+    // Assume allocation/deallocation takes place elsewhere
+    factories_.clear();
+}
+
+// Touch becomes active on a key where it wasn't previously
+void MappingFactorySplitter::touchBegan(int noteNumber, bool midiNoteIsOn, bool keyMotionActive,
+                Node<KeyTouchFrame>* touchBuffer,
+                Node<key_position>* positionBuffer,
+                KeyPositionTracker* positionTracker) {
+    std::list<MappingFactory*>::iterator it;
+    
+    for(it = factories_.begin(); it != factories_.end(); ++it) {
+        (*it)->touchBegan(noteNumber, midiNoteIsOn, keyMotionActive,
+                          touchBuffer, positionBuffer, positionTracker);
+    }
+}
+
+// Touch ends on a key where it wasn't previously
+void MappingFactorySplitter::touchEnded(int noteNumber, bool midiNoteIsOn, bool keyMotionActive,
+                Node<KeyTouchFrame>* touchBuffer,
+                Node<key_position>* positionBuffer,
+                KeyPositionTracker* positionTracker) {
+    std::list<MappingFactory*>::iterator it;
+    
+    for(it = factories_.begin(); it != factories_.end(); ++it) {
+        (*it)->touchEnded(noteNumber, midiNoteIsOn, keyMotionActive,
+                          touchBuffer, positionBuffer, positionTracker);
+    }
+}
+
+// MIDI note on for a key
+void MappingFactorySplitter::midiNoteOn(int noteNumber, bool touchIsOn, bool keyMotionActive,
+                Node<KeyTouchFrame>* touchBuffer,
+                Node<key_position>* positionBuffer,
+                KeyPositionTracker* positionTracker) {
+    
+    std::list<MappingFactory*>::iterator it;
+    
+    for(it = factories_.begin(); it != factories_.end(); ++it) {
+        (*it)->midiNoteOn(noteNumber, touchIsOn, keyMotionActive,
+                          touchBuffer, positionBuffer, positionTracker);
+    }
+}
+
+// MIDI note off for a key
+void MappingFactorySplitter::midiNoteOff(int noteNumber, bool touchIsOn, bool keyMotionActive,
+                 Node<KeyTouchFrame>* touchBuffer,
+                 Node<key_position>* positionBuffer,
+                 KeyPositionTracker* positionTracker) {
+    std::list<MappingFactory*>::iterator it;
+    
+    for(it = factories_.begin(); it != factories_.end(); ++it) {
+        (*it)->midiNoteOff(noteNumber, touchIsOn, keyMotionActive,
+                           touchBuffer, positionBuffer, positionTracker);
+    }
+}
+
+// Key goes active from continuous key position
+void MappingFactorySplitter::keyMotionActive(int noteNumber, bool midiNoteIsOn, bool touchIsOn,
+                     Node<KeyTouchFrame>* touchBuffer,
+                     Node<key_position>* positionBuffer,
+                     KeyPositionTracker* positionTracker) {
+    std::list<MappingFactory*>::iterator it;
+    
+    for(it = factories_.begin(); it != factories_.end(); ++it) {
+        (*it)->keyMotionActive(noteNumber, midiNoteIsOn, touchIsOn,
+                           touchBuffer, positionBuffer, positionTracker);
+    }
+}
+
+// Key goes idle from continuous key position
+void MappingFactorySplitter::keyMotionIdle(int noteNumber, bool midiNoteIsOn, bool touchIsOn,
+                   Node<KeyTouchFrame>* touchBuffer,
+                   Node<key_position>* positionBuffer,
+                   KeyPositionTracker* positionTracker) {
+    std::list<MappingFactory*>::iterator it;
+    
+    for(it = factories_.begin(); it != factories_.end(); ++it) {
+        (*it)->keyMotionIdle(noteNumber, midiNoteIsOn, touchIsOn,
+                               touchBuffer, positionBuffer, positionTracker);
+    }
+}
+
+// MIDI note about to begin
+void MappingFactorySplitter::noteWillBegin(int noteNumber, int midiChannel, int midiVelocity) {
+    std::list<MappingFactory*>::iterator it;
+    
+    for(it = factories_.begin(); it != factories_.end(); ++it) {
+        (*it)->noteWillBegin(noteNumber, midiChannel, midiVelocity);
+    }
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/Mappings/MappingFactorySplitter.h	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,117 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+
+  MappingFactorySplitter.h: MappingFactory subclass which in turn contains
+  several factories, routing all incoming method calls to each of them.
+*/
+
+#ifndef __touchkeys__TouchkeyMappingFactorySplitter__
+#define __touchkeys__TouchkeyMappingFactorySplitter__
+
+#include <iostream>
+#include <list>
+#include "MappingFactory.h"
+
+// Factory class to produce Touchkey vibrato (pitch-bend) mappings
+// This class keeps track of all the active mappings and responds
+// whenever touches or notes begin or end
+
+class MappingFactorySplitter : public MappingFactory {
+public:
+    // ***** Constructor *****
+    
+	// Default constructor, containing a reference to the PianoKeyboard class.
+    MappingFactorySplitter(PianoKeyboard &keyboard) : MappingFactory(keyboard), bypassNewMappings_(false) {}
+	
+    // ***** Destructor *****
+    
+    ~MappingFactorySplitter() {
+        removeAllFactories();
+    }
+    
+    // ***** Accessors / Modifiers *****
+    
+    Mapping* mapping(int noteNumber);                  // Look up a mapping with the given note number
+    std::vector<int> activeMappings();                 // Return a list of all active notes
+    
+    void removeAllMappings();                          // Remove all active mappings
+    void mappingFinished(int noteNumber) {}            // Callback from a mapping to say it's finished.
+                                                       // Nobody should ever call this since we don't have our own mappings
+    
+    void suspendMapping(int noteNumber);                // Suspend messages from a particular note
+    void suspendAllMappings();                          // ... or all notes
+    void resumeMapping(int noteNumber, bool resend);    // Resume messages from a particular note
+    void resumeAllMappings(bool resend);                // ... or all notes
+    
+    int bypassed();                                     // Whether this mapping is bypassed
+    void setBypassed(bool bypass);                      // Set whether the mapping is bypassed or not
+    
+    // ***** Specific Methods *****
+    
+    void addFactory(MappingFactory* factory);
+    void removeFactory(MappingFactory* factory);
+    void removeAllFactories();
+
+    // ***** State Updaters *****
+    
+    // These are called by PianoKey whenever certain events occur that might
+    // merit the start and stop of a mapping. What is done with them depends on
+    // the particular factory subclass.
+    
+    // Touch becomes active on a key where it wasn't previously
+    void touchBegan(int noteNumber, bool midiNoteIsOn, bool keyMotionActive,
+                    Node<KeyTouchFrame>* touchBuffer,
+                    Node<key_position>* positionBuffer,
+                    KeyPositionTracker* positionTracker);
+    // Touch ends on a key where it wasn't previously
+    void touchEnded(int noteNumber, bool midiNoteIsOn, bool keyMotionActive,
+                    Node<KeyTouchFrame>* touchBuffer,
+                    Node<key_position>* positionBuffer,
+                    KeyPositionTracker* positionTracker);
+    // MIDI note on for a key
+    void midiNoteOn(int noteNumber, bool touchIsOn, bool keyMotionActive,
+                    Node<KeyTouchFrame>* touchBuffer,
+                    Node<key_position>* positionBuffer,
+                    KeyPositionTracker* positionTracker);
+    // MIDI note off for a key
+    void midiNoteOff(int noteNumber, bool touchIsOn, bool keyMotionActive,
+                     Node<KeyTouchFrame>* touchBuffer,
+                     Node<key_position>* positionBuffer,
+                     KeyPositionTracker* positionTracker);
+    // Key goes active from continuous key position
+    void keyMotionActive(int noteNumber, bool midiNoteIsOn, bool touchIsOn,
+                         Node<KeyTouchFrame>* touchBuffer,
+                         Node<key_position>* positionBuffer,
+                         KeyPositionTracker* positionTracker);
+    // Key goes idle from continuous key position
+    void keyMotionIdle(int noteNumber, bool midiNoteIsOn, bool touchIsOn,
+                       Node<KeyTouchFrame>* touchBuffer,
+                       Node<key_position>* positionBuffer,
+                       KeyPositionTracker* positionTracker);
+    // MIDI note about to begin
+    void noteWillBegin(int noteNumber, int midiChannel, int midiVelocity);
+
+private:
+    
+    // State variables
+    std::list<MappingFactory*> factories_;              // List of child factories
+    bool bypassNewMappings_;                            // Whether to bypass mappings that are added
+};
+
+#endif /* defined(__touchkeys__TouchkeyMappingFactorySplitter__) */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/Mappings/MappingScheduler.cpp	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,371 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+
+  MappingScheduler.cpp: implements a thread in which mapping actions are
+  performed. Each Mapping object implements a triggerReceived() method
+  which is called by the hardware I/O thread. This method should do a
+  minimal amount of work but pass the real work off to the performMapping()
+  method which is called by the MappingScheduler thread. The scheduler
+  also allows mapping calls to be performed in the absence of received data,
+  for example to cause a parameter to ramp down over time if no touch data
+  is received.
+*/
+
+#include "MappingScheduler.h"
+#include "Mapping.h"
+
+#undef DEBUG_MAPPING_SCHEDULER
+
+using std::cout;
+
+const timestamp_diff_type MappingScheduler::kAllowableAdvanceExecutionTime = milliseconds_to_timestamp(1.0);
+
+// Destructor
+MappingScheduler::~MappingScheduler() {
+    // Stop the thread
+    stop();
+    
+    // Now go through and delete any mappings awaiting deletion
+    // so these objects don't leak
+    MappingAction nextAction;
+    
+    while(actionsNow_.Consume(nextAction)) {
+        if(nextAction.who != 0 && nextAction.action == kActionUnregisterAndDelete) {
+#ifdef DEBUG_MAPPING_SCHEDULER
+            std::cout << "~MappingScheduler(): Deleting mapping " << who << " (actionsNow)\n";
+#endif
+            delete nextAction.who;
+        }
+    }
+    
+    while(!actionsLater_.empty()) {
+        nextAction = actionsLater_.begin()->second;
+        
+        if(nextAction.who != 0 && nextAction.action == kActionUnregisterAndDelete) {
+#ifdef DEBUG_MAPPING_SCHEDULER
+            std::cout << "~MappingScheduler(): Deleting mapping " << who << " (actionsLater)\n";
+#endif
+            delete nextAction.who;
+        }
+        actionsLater_.erase(actionsLater_.begin());
+    }
+}
+
+// Start the thread handling the scheduling.
+void MappingScheduler::start() {
+	if(isRunning_)
+		return;
+    startThread();
+}
+
+// Stop the scheduler thread if it is currently running.
+void MappingScheduler::stop() {
+	if(!isRunning_)
+		return;
+    
+    // Tell the thread to quit and signal the event it waits on
+    signalThreadShouldExit();
+    waitableEvent_.signal();
+    stopThread(-1);
+    
+	isRunning_ = false;
+}
+
+// Register a mapping to be called by the scheduler
+void MappingScheduler::registerMapping(Mapping *who) {
+    // Lock the mutex for insertions to ensure that only a single
+    // thread can act as producer at any given time.
+    ScopedLock sl(actionsInsertionMutex_);
+    
+    actionsNow_.Produce(MappingAction(who, counter_, kActionRegister));
+    
+    // Increment the counter so each insertion gets a unique label
+    counter_++;
+    
+    // Wake up the consumer thread
+    waitableEvent_.signal();
+}
+
+// Schedule a mapping action to happen as soon as possible
+void MappingScheduler::scheduleNow(Mapping *who) {
+    // Lock the mutex for insertions to ensure that only a single
+    // thread can act as producer at any given time.
+    ScopedLock sl(actionsInsertionMutex_);
+    
+    actionsNow_.Produce(MappingAction(who, counter_, kActionPerformMapping));
+    
+    // Increment the counter so each insertion gets a unique label
+    counter_++;
+    
+    // Wake up the consumer thread
+    waitableEvent_.signal();
+}
+
+// Schedule a mapping action to happen in the future at a specified timestamp
+void MappingScheduler::scheduleLater(Mapping *who, timestamp_type timestamp) {
+    ScopedLock sl(actionsLaterMutex_);
+    ScopedLock sl2(actionsInsertionMutex_);
+    
+    bool newActionWillComeFirst = false;
+    if(actionsLater_.empty())
+        newActionWillComeFirst = true;
+    else if(timestamp < actionsLater_.begin()->first)
+        newActionWillComeFirst = true;
+    
+    actionsLater_.insert(std::pair<timestamp_type, MappingAction>(timestamp,
+                                                                  MappingAction(who,
+                                                                                counter_,
+                                                                                kActionPerformMapping)));
+    
+    // Increment the counter so each insertion gets a unique label
+    counter_++;
+    
+    // Wake up the consumer thread if what we inserted is the next
+    // upcoming event
+    if(newActionWillComeFirst)
+        waitableEvent_.signal();
+}
+
+// Unschedule any further mappings from this object. Immediate mappings
+// already in the queue may still be executed.
+void MappingScheduler::unschedule(Mapping *who) {
+    // Unscheduling works by inserting an action in the "now" queue
+    // which preempts any further actions by this object.
+    ScopedLock sl(actionsInsertionMutex_);
+    
+    actionsNow_.Produce(MappingAction(who, counter_, kActionUnschedule));
+    
+    // Increment the counter to indicate we're at another cycle
+    counter_++;
+    
+    // Wake up the consumer thread
+    waitableEvent_.signal();
+}
+
+// Unregister a mapping which prevents it from being called by future events
+void MappingScheduler::unregisterMapping(Mapping *who) {
+    // Lock the mutex for insertions to ensure that only a single
+    // thread can act as producer at any given time.
+    ScopedLock sl(actionsInsertionMutex_);
+    
+    actionsNow_.Produce(MappingAction(who, counter_, kActionUnregister));
+    
+    // Increment the counter so each insertion gets a unique label
+    counter_++;
+    
+    // Wake up the consumer thread
+    waitableEvent_.signal();
+}
+
+
+// Unschedule any further mappings from this object. Once any currently
+// scheduled "now" mappings have been executed, delete the object in question.
+void MappingScheduler::unregisterAndDelete(Mapping *who) {
+    // Unscheduling works by inserting an action in the "now" queue
+    // which preempts any further actions by this object. Deletion
+    // will be handled by the consumer thread.
+    ScopedLock sl(actionsInsertionMutex_);
+    
+    actionsNow_.Produce(MappingAction(who, counter_, kActionUnregisterAndDelete));
+    
+    // Increment the counter to indicate we're at another cycle
+    counter_++;
+    
+    // Wake up the consumer thread
+    waitableEvent_.signal();
+}
+
+// This function runs in its own thread (from the Juce::Thread parent class). Every time
+// it is signaled, it executes all the Mapping actions in the actionsNow_ category and then
+// looks for the next delayed action.
+
+void MappingScheduler::run() {
+	isRunning_ = true;
+	
+    // This will run until the thread is interrupted (in the stop() method)
+    while(!threadShouldExit()) {
+        MappingAction nextAction;
+        
+        // Go through the accumulated actions in the "now" queue
+        while(actionsNow_.Consume(nextAction)) {
+            if(nextAction.who != 0) {
+#ifdef DEBUG_MAPPING_SCHEDULER
+                std::cout << "Performing immediate mapping\n";
+#endif
+                performAction(nextAction);
+            }
+        }
+        
+        // Next, grab the first upcoming action in the later category
+        bool foundAction = true;
+        timestamp_diff_type timeToNextAction = 0;
+        
+        while(foundAction) {
+            // Lock the future actions mutex to examine the contents
+            // of the future actions collection
+            actionsLaterMutex_.enter();
+            foundAction = false;
+            
+            if(!actionsLater_.empty()) {
+                std::multimap<timestamp_type, MappingAction>::iterator it = actionsLater_.begin();
+                timestamp_type t = it->first;
+                
+                timeToNextAction = t - keyboard_.schedulerCurrentTimestamp();
+                if(timeToNextAction <= 0) {
+                    // If we get here, we have a non-empty collection fo future actions, the first
+                    // of which should happen by now. Copy the action, erase it from the collection
+                    // and unlock the mutex before proceeding.
+                    nextAction = it->second;
+                    actionsLater_.erase(it);
+                    foundAction = true;
+                }
+            }
+            else
+                timeToNextAction = 0;
+            
+            actionsLaterMutex_.exit();
+        
+            if(foundAction) {
+                // If this is set, we found a future action which is supposed to happen by now.
+                // Execute it and check the next one.
+#ifdef DEBUG_MAPPING_SCHEDULER
+                std::cout << "Performing delayed mapping\n";
+#endif
+                performAction(nextAction);
+            }
+            else {
+#ifdef DEBUG_MAPPING_SCHEDULER
+                std::cout << "Found no further actions\n";
+#endif
+            }
+        }
+        
+        if(timeToNextAction > 0) {
+            // If we complete the above loop with timeToNextAction set greater than 0, it means
+            // we found an action that's supposed to happen in the future, but isn't ready yet.
+            // The alternative is that there were no further actions, in which case the loop will
+            // terminate with timeToNextAction set to 0.
+            
+#ifdef DEBUG_MAPPING_SCHEDULER
+            std::cout << "Waiting for next action in " << timestamp_to_milliseconds(timeToNextAction) << "ms\n";
+#endif
+            // Wait for the next action to arrive (unless signaled)
+            waitableEvent_.wait(timestamp_to_milliseconds(timeToNextAction));
+        }
+        else {
+            // No future actions found; wait for a signal
+            
+#ifdef DEBUG_MAPPING_SCHEDULER
+            std::cout << "Waiting for next action\n";
+#endif
+            waitableEvent_.wait();
+        }
+        
+        waitableEvent_.reset();             // Clear the signal
+    }
+}
+
+// Perform a mapping action: either execute the mapping or unschedule it,
+// depending on the contents of the MappingAction object.
+void MappingScheduler::performAction(MappingAction const& mappingAction) {
+    Mapping *who = mappingAction.who;
+    bool skip = true;
+    
+    
+    // Check if this mapping action has been superseded by another
+    // one already executed which was scheduled at the same time or later.
+    // For example, if multiple actions have the same counter and the same
+    // object, only the first one will run.
+    if(countersForMappings_.count(who) != 0) {
+        if(countersForMappings_[who] < mappingAction.counter) {
+#ifdef DEBUG_MAPPING_SCHEDULER
+            std::cout << "Found counter " << countersForMappings_[who] << " for mapping " << who << std::endl;
+#endif
+            skip = false;
+        }
+    }
+    else if(mappingAction.action == kActionRegister) {
+        // Registration can happen if there is no previous
+        // counter for the object. This is in fact the expected case.
+#ifdef DEBUG_MAPPING_SCHEDULER
+        std::cout << "No counter for mapping " << who << " but allowing for registration\n";
+#endif
+        skip = false;
+    }
+    
+    if(!skip) {
+        // Update the last counter for this object
+        countersForMappings_[who] = mappingAction.counter;
+        
+        if(mappingAction.action == kActionRegister) {
+#ifdef DEBUG_MAPPING_SCHEDULER
+            std::cout << "Registering object " << mappingAction.who << " with counter " << mappingAction.counter << std::endl;
+#endif
+        }
+        else if(mappingAction.action == kActionPerformMapping) {
+#ifdef DEBUG_MAPPING_SCHEDULER
+            std::cout << "Performing mapping for object " << mappingAction.who << " with counter " << mappingAction.counter << std::endl;
+#endif
+            timestamp_type nextTimestamp = who->performMapping();
+            
+            // Reschedule for later if next timestamp isn't 0
+            if(nextTimestamp != 0) {
+#ifdef DEBUG_MAPPING_SCHEDULER
+                std::cout << "Rescheduling object " << mappingAction.who << " for timestamp " << nextTimestamp << std::endl;
+#endif
+                scheduleLater(who, nextTimestamp);
+            }
+        }
+        else if(mappingAction.action == kActionUnschedule) {
+#ifdef DEBUG_MAPPING_SCHEDULER
+            std::cout << "Unscheduling object " << who << " with counter " << mappingAction.counter << std::endl;
+#endif
+            // Nothing to do in fact; updating the counter will cause all future actions on this
+            // object to be ignored.
+        }
+        else if(mappingAction.action == kActionUnregister) {
+#ifdef DEBUG_MAPPING_SCHEDULER
+            std::cout << "Unregistering and deleting object " << who << " with counter " << mappingAction.counter << std::endl;
+#endif
+            // Remove the object from the counter registry
+            countersForMappings_.erase(who);
+        }
+        else if(mappingAction.action == kActionUnregisterAndDelete) {
+#ifdef DEBUG_MAPPING_SCHEDULER
+            std::cout << "Unregistering and deleting object " << who << " with counter " << mappingAction.counter << std::endl;
+#endif
+            // Remove the object from the counter registry
+            countersForMappings_.erase(who);
+            
+            // Delete this object
+            delete mappingAction.who;
+        }
+        else {
+            // Shouldn't happen
+#ifdef DEBUG_MAPPING_SCHEDULER
+            std::cout << "Unknown action " << mappingAction.action << " for object " << who << " with counter " << mappingAction.counter << std::endl;
+#endif
+        }
+    }
+    else {
+#ifdef DEBUG_MAPPING_SCHEDULER
+        std::cout << "Skipping action " << mappingAction.action << " for object " << who << " with counter " << mappingAction.counter << std::endl;
+#endif
+    }
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/Mappings/MappingScheduler.h	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,193 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+
+  MappingScheduler.h: implements a thread in which mapping actions are
+  performed. Each Mapping object implements a triggerReceived() method
+  which is called by the hardware I/O thread. This method should do a
+  minimal amount of work but pass the real work off to the performMapping()
+  method which is called by the MappingScheduler thread. The scheduler
+  also allows mapping calls to be performed in the absence of received data,
+  for example to cause a parameter to ramp down over time if no touch data
+  is received.
+*/
+
+#ifndef __TouchKeys__MappingScheduler__
+#define __TouchKeys__MappingScheduler__
+
+#include <iostream>
+#include <map>
+#include <list>
+#include "../JuceLibraryCode/JuceHeader.h"
+#include "Mapping.h"
+
+/*
+ * LockFreeQueue
+ *
+ * Placeholder implementation for a ring buffer. Will have issues with
+ * instruction reordering, but should serve the purpose for testing for now.
+ */
+
+template <typename T>
+struct LockFreeQueue
+{
+    LockFreeQueue()
+    {
+        list.push_back(T());
+        iHead = list.begin();
+        iTail = list.end();
+    }
+    
+    void Produce(const T& t)
+    {
+        list.push_back(t);
+        iTail = list.end();
+        list.erase(list.begin(), iHead);
+    }
+    
+    bool Consume(T& t)
+    {
+        typename TList::iterator iNext = iHead;
+        ++iNext;
+        if (iNext != iTail)
+        {
+            iHead = iNext;
+            t = *iHead;
+            return true;
+        }
+        return false;
+    }
+    
+    T Consume()
+    {
+        T tmp;
+        while (!Consume(tmp))
+        {
+            ;
+        }
+        return tmp;
+    }
+    
+private:
+    typedef std::list<T> TList;
+    TList list;
+    typename std::list<T>::iterator iHead, iTail;
+};
+
+/*
+ * MappingScheduler
+ *
+ * This class manages the timing and execution of Mapping objects. It performs a function
+ * similar to Scheduler but optimized specifically for running the performMapping() method
+ * of objects inheriting from Mapping. It maintains facilities to run mappings either now
+ * or in the future, including the ability to preempt future mapping calls with more immediate
+ * ones.
+ */
+
+class MappingScheduler : public Thread {
+private:
+    static const timestamp_diff_type kAllowableAdvanceExecutionTime;
+	
+    enum {
+        kActionUnknown = 0,
+        kActionRegister,
+        kActionPerformMapping,
+        kActionUnschedule,
+        kActionUnregister,
+        kActionUnregisterAndDelete
+    };
+    
+    struct MappingAction {
+    public:
+        MappingAction() : who(0), counter(0), action(kActionUnknown) {}
+        MappingAction(Mapping *x, unsigned long y, int z) :
+          who(x), counter(y), action(z) {}
+        
+        Mapping *who;
+        unsigned long counter;
+        int action;
+    };
+    
+public:
+	// ***** Constructor *****
+	//
+	// Note: This class is not copy-constructable.
+	
+	MappingScheduler(PianoKeyboard& keyboard, String threadName = "MappingScheduler") : Thread(threadName), keyboard_(keyboard),
+      waitableEvent_(true), isRunning_(false), counter_(0) {}
+	
+	// ***** Destructor *****
+	
+	~MappingScheduler();
+	
+	// ***** Thread Methods *****
+	//
+	// These start and stop the thread that handles the scheduling of events.
+	
+	void start();
+	void stop();
+	
+	bool isRunning() { return isRunning_; }
+	
+    // The main Juce::Thread run loop
+	void run();
+    
+	// ***** Event Management Methods *****
+	//
+	// This interface provides the ability to schedule and unschedule events for
+	// current or future times.
+    
+    void registerMapping(Mapping *who);
+    
+    void scheduleNow(Mapping *who);
+    void scheduleLater(Mapping *who, timestamp_type timestamp);
+    
+    void unschedule(Mapping *who);
+    
+    void unregisterMapping(Mapping *who);
+    void unregisterAndDelete(Mapping *who);
+    
+private:
+    // ***** Private Methods *****
+    void performAction(MappingAction const& mappingAction);
+    
+    // Reference to the main PianoKeyboard object which holds the master timestamp
+    PianoKeyboard& keyboard_;
+    
+	// These variables keep track of the status of the separate thread running the events
+    CriticalSection actionsInsertionMutex_;
+    CriticalSection actionsLaterMutex_;
+    
+    WaitableEvent waitableEvent_;
+	bool isRunning_;
+    
+    // This counter keeps track of the sequence of insertions and executions
+    // of mappings. It is incremented whenever the scheduler finishes the "now"
+    // set of actions, and can be used to figure out whether an event has been
+    // duplicated or preempted.
+    unsigned long counter_;
+    std::map<Mapping*, unsigned long> countersForMappings_;
+    
+    // These variables hold a ring buffer of actions to happen as soon as possible and a
+    // lock-synchronized collection of events that happen at later timestamps
+    LockFreeQueue<MappingAction> actionsNow_;
+    std::multimap<timestamp_type, MappingAction> actionsLater_;
+};
+
+
+#endif /* defined(__TouchKeys__MappingScheduler__) */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/Mappings/MultiFingerTrigger/TouchkeyMultiFingerTriggerMapping.cpp	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,226 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+
+  TouchkeyMultiFingerTriggerMapping.cpp: per-note mapping for the multiple-
+  finger trigger mapping, which performs actions when two or more fingers
+  are added or removed from the key.
+*/
+
+#include "TouchkeyMultiFingerTriggerMapping.h"
+#include "../../TouchKeys/MidiOutputController.h"
+
+// Class constants
+const int TouchkeyMultiFingerTriggerMapping::kDefaultFilterBufferLength = 30;
+const int TouchkeyMultiFingerTriggerMapping::kDefaultNumTouchesForTrigger = 2;
+const int TouchkeyMultiFingerTriggerMapping::kDefaultNumFramesForTrigger = 2;
+const int TouchkeyMultiFingerTriggerMapping::kDefaultNumConsecutiveTapsForTrigger = 1;
+const timestamp_diff_type TouchkeyMultiFingerTriggerMapping::kDefaultMaxTapSpacing = milliseconds_to_timestamp(500.0);
+
+// Main constructor takes references/pointers from objects which keep track
+// of touch location, continuous key position and the state detected from that
+// position. The PianoKeyboard object is strictly required as it gives access to
+// Scheduler and OSC methods. The others are optional since any given system may
+// contain only one of continuous key position or touch sensitivity
+TouchkeyMultiFingerTriggerMapping::TouchkeyMultiFingerTriggerMapping(PianoKeyboard &keyboard, MappingFactory *factory, int noteNumber, Node<KeyTouchFrame>* touchBuffer,
+                                                         Node<key_position>* positionBuffer, KeyPositionTracker* positionTracker)
+: TouchkeyBaseMapping(keyboard, factory, noteNumber, touchBuffer, positionBuffer, positionTracker),
+numTouchesForTrigger_(kDefaultNumTouchesForTrigger), numFramesForTrigger_(kDefaultNumFramesForTrigger),
+numConsecutiveTapsForTrigger_(kDefaultNumConsecutiveTapsForTrigger), maxTapSpacing_(kDefaultMaxTapSpacing),
+needsMidiNoteOn_(true), pastSamples_(kDefaultFilterBufferLength)
+{
+    reset();
+}
+
+// Reset state back to defaults
+void TouchkeyMultiFingerTriggerMapping::reset() {
+    ScopedLock sl(sampleBufferMutex_);
+    
+    TouchkeyBaseMapping::reset();
+    pastSamples_.clear();
+    
+    lastNumActiveTouches_ = 0;
+    lastActiveTouchLocations_[0] = lastActiveTouchLocations_[1] = lastActiveTouchLocations_[2] = 0;
+    framesCount_ = 0;
+    tapsCount_ = 0;
+    hasGeneratedTap_ = false;
+    lastTapStartTimestamp_ = missing_value<timestamp_type>::missing();
+    hasTriggered_ = false;
+}
+
+// Resend all current parameters
+void TouchkeyMultiFingerTriggerMapping::resend() {
+    // Message is only sent at release; resend may not apply here.
+}
+
+// This method receives data from the touch buffer or possibly the continuous key angle (not used here)
+void TouchkeyMultiFingerTriggerMapping::triggerReceived(TriggerSource* who, timestamp_type timestamp) {
+    if(needsMidiNoteOn_ && !noteIsOn_) {
+        framesCount_ = 0;
+        hasGeneratedTap_ = false;
+        return;
+    }
+    
+    if(who == touchBuffer_) {
+        if(!touchBuffer_->empty()) {
+            // Find the current number of touches
+            KeyTouchFrame frame  = touchBuffer_->latest();
+            int count = frame.count;
+            
+            if(count < numTouchesForTrigger_) {
+                framesCount_ = 0;
+                hasGeneratedTap_ = false;
+                if(hasTriggered_) {
+                    generateTriggerOff(timestamp);
+                    hasTriggered_ = false;
+                }
+            }
+            else if(count == numTouchesForTrigger_) {
+                framesCount_++;
+                if(framesCount_ >= numFramesForTrigger_ && !hasGeneratedTap_) {
+                    // Enough frames have elapsed to consider this a tap
+                    // Figure out if it is a multiple consecutive tap or the first
+                    // of a set.
+                    if(!missing_value<timestamp_diff_type>::isMissing(lastTapStartTimestamp_)) {
+                        if(timestamp - lastTapStartTimestamp_ < maxTapSpacing_) {
+                            tapsCount_++;
+                        }
+                        else
+                            tapsCount_ = 1;
+                    }
+                    else
+                        tapsCount_ = 1;
+                    
+                    std::cout << "Tap " << tapsCount_ << std::endl;
+                    
+                    // Check if the right number of taps has elapsed
+                    if(tapsCount_ >= numConsecutiveTapsForTrigger_ && !hasTriggered_) {
+                        hasTriggered_ = true;
+                        
+                        // Find the ID of the newest touch and compare its location
+                        // to the immediately preceding touch(es) to find the distance
+                        int newest = 0, oldest = 0, newestId = -1, oldestId = 1000000;
+                        for(int i = 0; i < count; i++) {
+                            if(frame.ids[i] > newestId) {
+                                newest = i;
+                                newestId = frame.ids[i];
+                            }
+                            if(frame.ids[i] < oldestId) {
+                                oldest = i;
+                                oldestId = frame.ids[i];
+                            }
+                        }
+
+                        // Find the distance between the point before this tap and the
+                        // point that was added to create the tap. If this is a 3-touch
+                        // tap, find the distance between the farthest two points, with
+                        // the direction determined by which end is older.
+                        float distance = frame.locs[newest] - frame.locs[oldest];
+                        if(count == 3) {
+                            if(fabsf(frame.locs[2] - frame.locs[0]) > fabsf(distance)) {
+                                if(frame.ids[2] > frame.ids[0])
+                                    distance = frame.locs[2] - frame.locs[0];
+                                else
+                                    distance = frame.locs[0] - frame.locs[2];
+                            }
+                        }
+                        
+                        // Generate the trigger. If a multi-tap gesture, also indicate the timing
+                        if(numConsecutiveTapsForTrigger_ <= 1)
+                            generateTriggerOn(timestamp, 0, distance);
+                        else
+                            generateTriggerOn(timestamp, timestamp - lastTapStartTimestamp_, distance);
+                    }
+                    
+                    hasGeneratedTap_ = true;
+                    lastTapStartTimestamp_ = timestamp;
+                }
+            }
+        
+            // Save the count locations for next time
+            lastNumActiveTouches_ = frame.count;
+            for(int i = 0; i < count; i++) {
+                lastActiveTouchLocations_[i] = frame.locs[i];
+            }
+        }
+    }
+}
+
+// Mapping method. This actually does the real work of sending OSC data in response to the
+// latest information from the touch sensors or continuous key angle
+timestamp_type TouchkeyMultiFingerTriggerMapping::performMapping() {
+    // Nothing to do here until note is released.
+    // Register for the next update by returning its timestamp
+    // TODO: do we even need this? Check Mapping::engage() and Mapping::disengage()
+    timestamp_type currentTimestamp = keyboard_.schedulerCurrentTimestamp();
+    nextScheduledTimestamp_ = currentTimestamp + updateInterval_;
+    return nextScheduledTimestamp_;
+}
+
+void TouchkeyMultiFingerTriggerMapping::generateTriggerOn(timestamp_type timestamp, timestamp_diff_type timeBetweenTaps, float distanceBetweenPoints) {
+    std::cout << "Trigger distance = " << distanceBetweenPoints << " timing = " << timeBetweenTaps << std::endl;
+    // KLUDGE
+    if(!suspended_) {
+#if 0
+        if(distanceBetweenPoints > 0.35) {
+            //keyboard_.sendMessage("/touchkeys/pitchbend", "if", noteNumber_, 2.0, LO_ARGS_END);
+            int ch = keyboard_.key(noteNumber_)->midiChannel();
+            int vel = keyboard_.key(noteNumber_)->midiVelocity();
+            keyboard_.midiOutputController()->sendNoteOn(0, ch, noteNumber_ + 14, vel);
+            //keyboard_.midiOutputController()->sendNoteOff(0, ch, noteNumber_ + 12, vel);
+        }
+        else {
+            //keyboard_.sendMessage("/touchkeys/pitchbend", "if", noteNumber_, 1.0, LO_ARGS_END);
+            int ch = keyboard_.key(noteNumber_)->midiChannel();
+            int vel = keyboard_.key(noteNumber_)->midiVelocity();
+            keyboard_.midiOutputController()->sendNoteOn(0, ch, noteNumber_ + 13, vel);
+            //keyboard_.midiOutputController()->sendNoteOff(0, ch, noteNumber_ + 12, vel);
+        }
+#elif 0
+        int ch = keyboard_.key(noteNumber_)->midiChannel();
+        keyboard_.midiOutputController()->sendControlChange(0, ch, 73, 127);
+#else
+        keyboard_.midiOutputController()->sendNoteOn(0, keyboard_.key(noteNumber_)->midiChannel(), noteNumber_, 127);
+#endif
+    }
+}
+
+void TouchkeyMultiFingerTriggerMapping::generateTriggerOff(timestamp_type timestamp) {
+    std::cout << "Trigger off\n";
+    if(!suspended_) {
+#if 0
+        //eyboard_.sendMessage("/touchkeys/pitchbend", "if", noteNumber_, 0.0, LO_ARGS_END);
+        int ch = keyboard_.key(noteNumber_)->midiChannel();
+        int vel = keyboard_.key(noteNumber_)->midiVelocity();
+        keyboard_.midiOutputController()->sendNoteOn(0, ch, noteNumber_ + 12, vel);
+        //keyboard_.midiOutputController()->sendNoteOff(0, ch, noteNumber_ + 13, vel);
+        //keyboard_.midiOutputController()->sendNoteOff(0, ch, noteNumber_ + 14, vel);
+#elif 0
+        int ch = keyboard_.key(noteNumber_)->midiChannel();
+        keyboard_.midiOutputController()->sendControlChange(0, ch, 73, 0);
+#else
+        keyboard_.midiOutputController()->sendNoteOn(0, keyboard_.key(noteNumber_)->midiChannel(), noteNumber_, 127);
+#endif
+    }
+}
+
+// MIDI note-off message received
+void TouchkeyMultiFingerTriggerMapping::midiNoteOffReceived(int channel) {
+    int ch = keyboard_.key(noteNumber_)->midiChannel();
+    keyboard_.midiOutputController()->sendControlChange(0, ch, 73, 0);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/Mappings/MultiFingerTrigger/TouchkeyMultiFingerTriggerMapping.h	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,106 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+
+  TouchkeyMultiFingerTriggerMapping.h: per-note mapping for the multiple-
+  finger trigger mapping, which performs actions when two or more fingers
+  are added or removed from the key.
+*/
+
+#ifndef __TouchKeys__TouchkeyMultiFingerTriggerMapping__
+#define __TouchKeys__TouchkeyMultiFingerTriggerMapping__
+
+#include "../TouchkeyBaseMapping.h"
+
+// This class handles the detection of finger motion specifically at
+// note release, which can be used to trigger specific release effects.
+
+class TouchkeyMultiFingerTriggerMapping : public TouchkeyBaseMapping {
+private:
+    // Default values
+    /*constexpr static const int kDefaultFilterBufferLength = 30;
+    constexpr static const int kDefaultNumTouchesForTrigger = 2;
+    constexpr static const int kDefaultNumFramesForTrigger = 2;
+    constexpr static const int kDefaultNumConsecutiveTapsForTrigger = 1;
+    constexpr static const timestamp_diff_type kDefaultMaxTapSpacing = milliseconds_to_timestamp(500.0);*/
+    static const int kDefaultFilterBufferLength;
+    static const int kDefaultNumTouchesForTrigger;
+    static const int kDefaultNumFramesForTrigger;
+    static const int kDefaultNumConsecutiveTapsForTrigger;
+    static const timestamp_diff_type kDefaultMaxTapSpacing;
+
+public:
+	// ***** Constructors *****
+	
+	// Default constructor, passing the buffer on which to trigger
+	TouchkeyMultiFingerTriggerMapping(PianoKeyboard &keyboard, MappingFactory *factory, int noteNumber, Node<KeyTouchFrame>* touchBuffer,
+                                      Node<key_position>* positionBuffer, KeyPositionTracker* positionTracker);
+	
+    // ***** Modifiers *****
+    
+    // Reset the state back initial values
+    void reset();
+    
+    // Resend the current state of all parameters
+    void resend();
+    
+	// ***** Evaluators *****
+    
+    // This method receives triggers whenever events occur in the touch data or the
+    // continuous key position (state changes only). It alters the behavior and scheduling
+    // of the mapping but does not itself send OSC messages
+	void triggerReceived(TriggerSource* who, timestamp_type timestamp);
+	
+    // This method handles the OSC message transmission. It should be run in the Scheduler
+    // thread provided by PianoKeyboard.
+    timestamp_type performMapping();
+    
+    // ***** Specific Methods *****
+
+    
+private:
+    // ***** Private Methods *****
+    // Generate the multi-finger trigger
+    void generateTriggerOn(timestamp_type timestamp, timestamp_diff_type timeBetweenTaps, float distanceBetweenPoints);
+    void generateTriggerOff(timestamp_type timestamp);
+    
+    void midiNoteOffReceived(int channel);
+    
+	// ***** Member Variables *****
+    
+    // Parameters
+    int numTouchesForTrigger_;                  // How many touches are needed for a trigger
+    int numFramesForTrigger_;                   // How many consecutive frames with these touches are needed to trigger
+    int numConsecutiveTapsForTrigger_;          // How many taps with this number of touches are needed to trigger
+    timestamp_diff_type maxTapSpacing_;         // How far apart the taps can come and be considered a multi-tap gesture
+    bool needsMidiNoteOn_;                      // Whether the MIDI note has to be on for this gesture to trigger
+    
+    int lastNumActiveTouches_;                  // How many touches were active before
+    float lastActiveTouchLocations_[3];         // Where (Y coord.) the active touches were last frame
+    int framesCount_;                           // How many frames have met the current number of active touches
+    int tapsCount_;                             // How many taps we've registered so far
+    bool hasGeneratedTap_;                      // Whether we've generated a tap with this number of touches yet
+    timestamp_type lastTapStartTimestamp_;      // When the last tap ended
+    bool hasTriggered_;                         // Whether we've generated a trigger
+    
+    Node<KeyTouchFrame> pastSamples_;           // Locations of touch
+    CriticalSection sampleBufferMutex_;         // Mutex to protect threaded access to sample buffer
+};
+
+
+#endif /* defined(__TouchKeys__TouchkeyMultiFingerTriggerMapping__) */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/Mappings/MultiFingerTrigger/TouchkeyMultiFingerTriggerMappingFactory.cpp	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,25 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+
+  TouchkeyMultiFingerTriggerMappingFactory.cpp: factory for the multiple-
+  finger trigger mapping, which performs actions when two or more fingers
+  are added or removed from the key.
+*/
+
+#include "TouchkeyMultiFingerTriggerMappingFactory.h"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/Mappings/MultiFingerTrigger/TouchkeyMultiFingerTriggerMappingFactory.h	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,51 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+
+  TouchkeyMultiFingerTriggerMappingFactory.h: factory for the multiple-
+  finger trigger mapping, which performs actions when two or more fingers
+  are added or removed from the key.
+*/
+
+
+#ifndef __TouchKeys__TouchkeyMultiFingerTriggerMappingFactory__
+#define __TouchKeys__TouchkeyMultiFingerTriggerMappingFactory__
+
+#include "../TouchkeyBaseMappingFactory.h"
+#include "TouchkeyMultiFingerTriggerMapping.h"
+
+class TouchkeyMultiFingerTriggerMappingFactory : public TouchkeyBaseMappingFactory<TouchkeyMultiFingerTriggerMapping> {
+private:
+  
+public:
+    // ***** Constructor *****
+    
+	// Default constructor, containing a reference to the PianoKeyboard class.
+    TouchkeyMultiFingerTriggerMappingFactory(PianoKeyboard &keyboard, MidiKeyboardSegment& segment)
+    : TouchkeyBaseMappingFactory<TouchkeyMultiFingerTriggerMapping>(keyboard, segment) {}
+	
+    // ***** Destructor *****
+    
+    ~TouchkeyMultiFingerTriggerMappingFactory() {}
+    
+    // ***** Accessors / Modifiers *****
+    
+    virtual const std::string factoryTypeName() { return "Multi-Finger\nTrigger"; }
+};
+
+#endif /* defined(__TouchKeys__TouchkeyMultiFingerTriggerMappingFactory__) */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/Mappings/OnsetAngle/TouchkeyOnsetAngleMapping.cpp	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,201 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+
+  TouchkeyOnsetAngleMapping.cpp: per-note mapping for the onset angle mapping,
+  which measures the speed of finger motion along the key surface at the
+  time of MIDI note onset.
+*/
+
+#include "TouchkeyOnsetAngleMapping.h"
+#include "../MappingFactory.h"
+
+#define DEBUG_NOTE_ONSET_MAPPING
+
+// Class constants
+const int TouchkeyOnsetAngleMapping::kDefaultFilterBufferLength = 30;
+const timestamp_diff_type TouchkeyOnsetAngleMapping::kDefaultMaxLookbackTime = milliseconds_to_timestamp(100);
+const int TouchkeyOnsetAngleMapping::kDefaultMaxLookbackSamples = 3;
+
+// Main constructor takes references/pointers from objects which keep track
+// of touch location, continuous key position and the state detected from that
+// position. The PianoKeyboard object is strictly required as it gives access to
+// Scheduler and OSC methods. The others are optional since any given system may
+// contain only one of continuous key position or touch sensitivity
+TouchkeyOnsetAngleMapping::TouchkeyOnsetAngleMapping(PianoKeyboard &keyboard, MappingFactory *factory, int noteNumber, Node<KeyTouchFrame>* touchBuffer,
+                                                         Node<key_position>* positionBuffer, KeyPositionTracker* positionTracker)
+: TouchkeyBaseMapping(keyboard, factory, noteNumber, touchBuffer, positionBuffer, positionTracker),
+pastSamples_(kDefaultFilterBufferLength), maxLookbackTime_(kDefaultMaxLookbackTime),
+startingPitchBendSemitones_(0), lastPitchBendSemitones_(0),
+rampBeginTime_(missing_value<timestamp_type>::missing()), rampLength_(0)
+{
+}
+
+// Reset state back to defaults
+void TouchkeyOnsetAngleMapping::reset() {
+    ScopedLock sl(sampleBufferMutex_);
+    
+    TouchkeyBaseMapping::reset();
+    pastSamples_.clear();
+}
+
+// Resend all current parameters
+void TouchkeyOnsetAngleMapping::resend() {
+    // Message is only sent at release; resend may not apply here.
+}
+
+// This method receives data from the touch buffer or possibly the continuous key angle (not used here)
+void TouchkeyOnsetAngleMapping::triggerReceived(TriggerSource* who, timestamp_type timestamp) {
+    if(who == touchBuffer_) {
+        ScopedLock sl(sampleBufferMutex_);
+        
+        // Save the latest frame, even if it is an empty touch (we need to know what happened even
+        // after the touch ends since the MIDI off may come later)
+        if(!touchBuffer_->empty())
+            pastSamples_.insert(touchBuffer_->latest(), touchBuffer_->latestTimestamp());
+    }
+}
+
+// Mapping method. This actually does the real work of sending OSC data in response to the
+// latest information from the touch sensors or continuous key angle
+timestamp_type TouchkeyOnsetAngleMapping::performMapping() {
+    timestamp_type currentTimestamp = keyboard_.schedulerCurrentTimestamp();
+    
+    if(rampLength_ != 0 && currentTimestamp <= rampBeginTime_ + rampLength_) {
+        float rampValue = 1.0 - (float)(currentTimestamp - rampBeginTime_)/(float)rampLength_;
+        
+        lastPitchBendSemitones_ = startingPitchBendSemitones_ * rampValue;
+#ifdef DEBUG_NOTE_ONSET_MAPPING
+        std::cout << "onset pitch = " << lastPitchBendSemitones_ << endl;
+#endif
+        sendPitchBendMessage(lastPitchBendSemitones_);
+    }
+    else if(lastPitchBendSemitones_ != 0) {
+        lastPitchBendSemitones_ = 0;
+#ifdef DEBUG_NOTE_ONSET_MAPPING
+        std::cout << "onset pitch = " << lastPitchBendSemitones_ << endl;
+#endif
+        sendPitchBendMessage(lastPitchBendSemitones_);
+    }
+        
+    // Register for the next update by returning its timestamp
+    nextScheduledTimestamp_ = currentTimestamp + updateInterval_;
+    return nextScheduledTimestamp_;
+}
+
+void TouchkeyOnsetAngleMapping::processOnset(timestamp_type timestamp) {
+
+    sampleBufferMutex_.enter();
+    
+    // Look backwards from the current timestamp to find the velocity
+    float calculatedVelocity = missing_value<float>::missing();
+    bool touchWasOn = false;
+    int sampleCount = 0;
+    
+#ifdef DEBUG_NOTE_ONSET_MAPPING
+    std::cout << "processOnset begin = " << pastSamples_.beginIndex() << " end = " << pastSamples_.endIndex() << "\n";
+#endif
+    
+    if(!pastSamples_.empty()) {
+        Node<KeyTouchFrame>::size_type index = pastSamples_.endIndex() - 1;
+        Node<KeyTouchFrame>::size_type mostRecentTouchPresentIndex = pastSamples_.endIndex() - 1;
+        while(index >= pastSamples_.beginIndex()) {
+#ifdef DEBUG_NOTE_ONSET_MAPPING
+            std::cout << "examining sample " << index << " with " << pastSamples_[index].count << " touches and time diff " << timestamp - pastSamples_.timestampAt(index) << "\n";
+#endif
+            if(timestamp - pastSamples_.timestampAt(index) >= maxLookbackTime_)
+                break;
+            if(pastSamples_[index].count == 0) {
+                if(touchWasOn) {
+                    // We found a break in the touch; stop here. But don't stop
+                    // if the first frames we consider have no touches.
+                    if(index < pastSamples_.endIndex() - 1)
+                        index++;
+                    break;
+                }
+            }
+            else if(!touchWasOn) {
+                mostRecentTouchPresentIndex = index;
+                touchWasOn = true;
+            }
+            if(sampleCount++ >= kDefaultMaxLookbackSamples)
+                break;
+            
+            // Can't decrement past 0 in an unsigned type
+            if(index == 0)
+                break;
+            index--;
+        }
+        
+        // If we fell off the beginning of the buffer, back up.
+        if(index < pastSamples_.beginIndex())
+            index =  pastSamples_.beginIndex();
+        
+        // Need at least two points for this calculation to work
+        timestamp_type endingTimestamp = pastSamples_.timestampAt(mostRecentTouchPresentIndex);
+        timestamp_type startingTimestamp = pastSamples_.timestampAt(index);
+        if(endingTimestamp - startingTimestamp > 0) {
+            float endingPosition = pastSamples_[mostRecentTouchPresentIndex].locs[0];
+            float startingPosition = pastSamples_[index].locs[0];
+            calculatedVelocity = (endingPosition - startingPosition) / (endingTimestamp - startingTimestamp);
+        }
+        else { // DEBUG
+#ifdef DEBUG_NOTE_ONSET_MAPPING
+            std::cout << "Found 0 timestamp difference on key onset (indices " << index << " and " << pastSamples_.endIndex() - 1 << "\n";
+#endif
+        }
+    }
+    else {
+#ifdef DEBUG_NOTE_ONSET_MAPPING
+        std::cout << "Found empty touch buffer on key onset\n";
+#endif
+    }
+    
+    sampleBufferMutex_.exit();
+    
+    if(!missing_value<float>::isMissing(calculatedVelocity)) {
+#ifdef DEBUG_NOTE_ONSET_MAPPING
+        std::cout << "Found onset velocity " << calculatedVelocity << " on note " << noteNumber_ << std::endl;
+#endif
+        if(calculatedVelocity > 6.0)
+            calculatedVelocity = 6.0;
+        
+        if(calculatedVelocity > 1.5) {
+            startingPitchBendSemitones_ = -1.0 * (calculatedVelocity / 5.0);
+            rampLength_ = milliseconds_to_timestamp((50.0 + calculatedVelocity*25.0));
+            rampBeginTime_ = keyboard_.schedulerCurrentTimestamp();
+        }
+        else
+            rampLength_ = 0;
+        
+        sendOnsetAngleMessage(calculatedVelocity);
+    }
+}
+
+void TouchkeyOnsetAngleMapping::sendOnsetAngleMessage(float onsetAngle, bool force) {
+    if(force || !suspended_) {
+        keyboard_.sendMessage("/touchkeys/onsetangle", "if", noteNumber_, onsetAngle, LO_ARGS_END);
+    }
+}
+
+// Send the pitch bend message of a given number of a semitones. Send by OSC,
+// which can be mapped to MIDI CC externally
+void TouchkeyOnsetAngleMapping::sendPitchBendMessage(float pitchBendSemitones, bool force) {
+    if(force || !suspended_)
+        keyboard_.sendMessage("/touchkeys/scoop", "if", noteNumber_, pitchBendSemitones, LO_ARGS_END);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/Mappings/OnsetAngle/TouchkeyOnsetAngleMapping.h	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,92 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+
+  TouchkeyOnsetAngleMapping.h: per-note mapping for the onset angle mapping,
+  which measures the speed of finger motion along the key surface at the
+  time of MIDI note onset.
+*/
+
+
+#ifndef __TouchKeys__TouchkeyOnsetAngleMapping__
+#define __TouchKeys__TouchkeyOnsetAngleMapping__
+
+#include "../TouchkeyBaseMapping.h"
+
+// This class handles the detection of finger motion specifically at
+// note release, which can be used to trigger specific release effects.
+
+class TouchkeyOnsetAngleMapping : public TouchkeyBaseMapping {
+private:
+    // Default values
+    /*constexpr static const int kDefaultFilterBufferLength = 30;
+     constexpr static const timestamp_diff_type kDefaultMaxLookbackTime = milliseconds_to_timestamp(100);*/
+    
+    static const int kDefaultFilterBufferLength;
+    static const timestamp_diff_type kDefaultMaxLookbackTime;
+    static const int kDefaultMaxLookbackSamples;
+    
+public:
+	// ***** Constructors *****
+	
+	// Default constructor, passing the buffer on which to trigger
+	TouchkeyOnsetAngleMapping(PianoKeyboard &keyboard, MappingFactory *factory, int noteNumber, Node<KeyTouchFrame>* touchBuffer,
+                                Node<key_position>* positionBuffer, KeyPositionTracker* positionTracker);
+	
+    // ***** Modifiers *****
+    
+    // Reset the state back initial values
+    void reset();
+    
+    // Resend the current state of all parameters
+    void resend();
+    
+	// ***** Evaluators *****
+    
+    // This method receives triggers whenever events occur in the touch data or the
+    // continuous key position (state changes only). It alters the behavior and scheduling
+    // of the mapping but does not itself send OSC messages
+	void triggerReceived(TriggerSource* who, timestamp_type timestamp);
+	
+    // This method handles the OSC message transmission. It should be run in the Scheduler
+    // thread provided by PianoKeyboard.
+    timestamp_type performMapping();
+    
+    // ***** Specific Methods *****
+    // Process the release by calculating the angle
+    void processOnset(timestamp_type timestamp);
+    
+private:
+    // ***** Private Methods *****
+    
+    void sendOnsetAngleMessage(float onsetAngle, bool force = false);
+    void sendPitchBendMessage(float pitchBendSemitones, bool force = false);
+    
+	// ***** Member Variables *****
+    
+    Node<KeyTouchFrame> pastSamples_;           // Locations of touch
+    timestamp_diff_type maxLookbackTime_;       // How long to look backwards to find release velocity
+    CriticalSection sampleBufferMutex_;         // Mutex to protect threaded access to sample buffer
+    
+    float startingPitchBendSemitones_;          // The value of pitch bend to start with
+    float lastPitchBendSemitones_;              // The last pitch value we sent out
+    timestamp_type rampBeginTime_;              // When did the pitch bend ramp begin?
+    timestamp_diff_type rampLength_;            // How long should the ramp be?
+};
+
+#endif /* defined(__TouchKeys__TouchkeyOnsetAngleMapping__) */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/Mappings/OnsetAngle/TouchkeyOnsetAngleMappingFactory.cpp	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,48 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+
+  TouchkeyOnsetAngleMappingFactory.cpp: factory for the onset angle mapping,
+  which measures the speed of finger motion along the key surface at the
+  time of MIDI note onset.
+*/
+
+#include "TouchkeyOnsetAngleMappingFactory.h"
+
+// Class constants
+const timestamp_diff_type TouchkeyOnsetAngleMappingFactory::kDefaultMaxLookbackTime = milliseconds_to_timestamp(100);
+
+
+TouchkeyOnsetAngleMappingFactory::TouchkeyOnsetAngleMappingFactory(PianoKeyboard &keyboard, MidiKeyboardSegment& segment)
+: TouchkeyBaseMappingFactory<TouchkeyOnsetAngleMapping>(keyboard, segment) {
+    //setName("/touchkeys/scoop");
+    setMidiParameters(MidiKeyboardSegment::kControlPitchWheel, -2.0, 2.0, 0.0);
+}
+
+// MIDI note ended: see whether the mapping was suspended and if not, execute the angle calculation
+void TouchkeyOnsetAngleMappingFactory::midiNoteOn(int noteNumber, bool touchIsOn, bool keyMotionActive,
+                                                     Node<KeyTouchFrame>* touchBuffer,
+                                                     Node<key_position>* positionBuffer,
+                                                     KeyPositionTracker* positionTracker) {
+    // Call base class method
+    TouchkeyBaseMappingFactory<TouchkeyOnsetAngleMapping>::midiNoteOn(noteNumber, touchIsOn, keyMotionActive, touchBuffer, positionBuffer, positionTracker);
+
+    if(mappings_.count(noteNumber) != 0) {
+        mappings_[noteNumber]->processOnset(keyboard_.schedulerCurrentTimestamp());
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/Mappings/OnsetAngle/TouchkeyOnsetAngleMappingFactory.h	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,59 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+
+  TouchkeyOnsetAngleMappingFactory.h: factory for the onset angle mapping,
+  which measures the speed of finger motion along the key surface at the
+  time of MIDI note onset.
+*/
+
+
+#ifndef __TouchKeys__TouchkeyOnsetAngleMappingFactory__
+#define __TouchKeys__TouchkeyOnsetAngleMappingFactory__
+
+#include "../TouchkeyBaseMappingFactory.h"
+#include "TouchkeyOnsetAngleMapping.h"
+
+class TouchkeyOnsetAngleMappingFactory : public TouchkeyBaseMappingFactory<TouchkeyOnsetAngleMapping> {
+private:
+    static const timestamp_diff_type kDefaultMaxLookbackTime;
+    
+public:
+    // ***** Constructor *****
+    
+	// Default constructor, containing a reference to the PianoKeyboard class.
+    TouchkeyOnsetAngleMappingFactory(PianoKeyboard &keyboard, MidiKeyboardSegment& segment);
+	
+    // ***** Destructor *****
+    
+    ~TouchkeyOnsetAngleMappingFactory() {}
+    
+    // ***** Accessors *****
+    
+    virtual const std::string factoryTypeName() { return "Onset\nAngle"; }
+    
+    // ***** State Updaters *****
+    
+    // Override the MIDI note on method to process the onset angle
+    void midiNoteOn(int noteNumber, bool touchIsOn, bool keyMotionActive,
+                     Node<KeyTouchFrame>* touchBuffer,
+                     Node<key_position>* positionBuffer,
+                     KeyPositionTracker* positionTracker);
+    
+};
+#endif /* defined(__TouchKeys__TouchkeyOnsetAngleMappingFactory__) */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/Mappings/PitchBend/TouchkeyPitchBendMapping.cpp	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,459 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+
+  TouchkeyPitchBendMapping.cpp: per-note mapping for the pitch-bend mapping,
+  which handles changing pitch based on relative finger motion.
+*/
+
+#include "TouchkeyPitchBendMapping.h"
+#include "../../TouchKeys/MidiOutputController.h"
+#include <vector>
+#include <climits>
+#include <cmath>
+#include "../MappingScheduler.h"
+
+#undef DEBUG_PITCHBEND_MAPPING
+
+// Class constants
+const int TouchkeyPitchBendMapping::kDefaultMIDIChannel = 0;
+const int TouchkeyPitchBendMapping::kDefaultFilterBufferLength = 30;
+
+const float TouchkeyPitchBendMapping::kDefaultBendRangeSemitones = 7.0;
+const float TouchkeyPitchBendMapping::kDefaultBendThresholdSemitones = 0.4;
+const float TouchkeyPitchBendMapping::kDefaultBendThresholdKeyLength = 0.1;
+const float TouchkeyPitchBendMapping::kDefaultSnapZoneSemitones = 0.5;
+const int TouchkeyPitchBendMapping::kDefaultPitchBendMode = TouchkeyPitchBendMapping::kPitchBendModeVariableEndpoints;
+const float TouchkeyPitchBendMapping::kDefaultFixedModeEnableDistance = 0.1;
+const float TouchkeyPitchBendMapping::kDefaultFixedModeBufferDistance = 0;
+
+const bool TouchkeyPitchBendMapping::kDefaultIgnoresTwoFingers = false;
+const bool TouchkeyPitchBendMapping::kDefaultIgnoresThreeFingers = false;
+
+// Main constructor takes references/pointers from objects which keep track
+// of touch location, continuous key position and the state detected from that
+// position. The PianoKeyboard object is strictly required as it gives access to
+// Scheduler and OSC methods. The others are optional since any given system may
+// contain only one of continuous key position or touch sensitivity
+TouchkeyPitchBendMapping::TouchkeyPitchBendMapping(PianoKeyboard &keyboard, MappingFactory *factory, int noteNumber, Node<KeyTouchFrame>* touchBuffer,
+                                               Node<key_position>* positionBuffer, KeyPositionTracker* positionTracker)
+: TouchkeyBaseMapping(keyboard, factory, noteNumber, touchBuffer, positionBuffer, positionTracker),
+bendIsEngaged_(false), snapIsEngaged_(false),
+thresholdSemitones_(kDefaultBendThresholdSemitones), thresholdKeyLength_(kDefaultBendThresholdKeyLength),
+snapZoneSemitones_(kDefaultSnapZoneSemitones),
+bendMode_(kDefaultPitchBendMode), fixedModeMinEnableDistance_(kDefaultFixedModeEnableDistance),
+fixedModeBufferDistance_(kDefaultFixedModeBufferDistance),
+ignoresTwoFingers_(kDefaultIgnoresTwoFingers), ignoresThreeFingers_(kDefaultIgnoresThreeFingers),
+onsetLocationX_(missing_value<float>::missing()),
+onsetLocationY_(missing_value<float>::missing()),
+lastX_(missing_value<float>::missing()), lastY_(missing_value<float>::missing()),
+idOfCurrentTouch_(-1), lastTimestamp_(missing_value<timestamp_type>::missing()), 
+lastProcessedIndex_(0), bendScalerPositive_(missing_value<float>::missing()),
+bendScalerNegative_(missing_value<float>::missing()),
+currentSnapDestinationSemitones_(missing_value<float>::missing()),
+bendRangeSemitones_(kDefaultBendRangeSemitones), lastPitchBendSemitones_(0),
+rawDistance_(kDefaultFilterBufferLength)
+{
+    resetDetectionState();
+    updateCombinedThreshold();
+}
+
+TouchkeyPitchBendMapping::~TouchkeyPitchBendMapping() {
+
+}
+
+// Reset state back to defaults
+void TouchkeyPitchBendMapping::reset() {
+    TouchkeyBaseMapping::reset();
+    sendPitchBendMessage(0.0);
+    resetDetectionState();
+}
+
+
+// Resend all current parameters
+void TouchkeyPitchBendMapping::resend() {
+    sendPitchBendMessage(lastPitchBendSemitones_, true);
+}
+
+// Set the range of vibrato
+void TouchkeyPitchBendMapping::setRange(float rangeSemitones) {
+    bendRangeSemitones_ = rangeSemitones;
+}
+
+// Set the vibrato detection thresholds
+void TouchkeyPitchBendMapping::setThresholds(float thresholdSemitones, float thresholdKeyLength) {
+    thresholdSemitones_ = thresholdSemitones;
+    thresholdKeyLength_ = thresholdKeyLength;
+    updateCombinedThreshold();
+}
+
+// Set the mode to bend a fixed amount up and down the key, regardless of where
+// the touch starts. minimumDistanceToEnable sets a floor below which the bend isn't
+// possible (for starting very close to an edge) and bufferAtEnd sets the amount
+// of key length beyond which no further bend takes place.
+void TouchkeyPitchBendMapping::setFixedEndpoints(float minimumDistanceToEnable, float bufferAtEnd) {
+    bendMode_ = kPitchBendModeFixedEndpoints;
+    fixedModeMinEnableDistance_ = minimumDistanceToEnable;
+    fixedModeBufferDistance_ = bufferAtEnd;
+}
+
+// Set the mode to bend an amount proportional to distance, which means
+// that the total range of bend will depend on where the finger started.
+void TouchkeyPitchBendMapping::setVariableEndpoints() {
+    bendMode_ = kPitchBendModeVariableEndpoints;
+}
+
+void TouchkeyPitchBendMapping::setIgnoresMultipleFingers(bool ignoresTwo, bool ignoresThree) {
+    ignoresTwoFingers_ = ignoresTwo;
+    ignoresThreeFingers_ = ignoresThree;
+}
+
+// Trigger method. This receives updates from the TouchKey data or from state changes in
+// the continuous key position (KeyPositionTracker). It will potentially change the scheduled
+// behavior of future mapping calls, but the actual OSC messages should be transmitted in a different
+// thread.
+void TouchkeyPitchBendMapping::triggerReceived(TriggerSource* who, timestamp_type timestamp) {
+    if(who == 0)
+        return;
+    
+    if(who == touchBuffer_) {
+        if(!touchBuffer_->empty()) {
+            // New touch data is available. Find the distance from the onset location.
+            KeyTouchFrame frame = touchBuffer_->latest();
+            lastTimestamp_ = timestamp;
+            
+            if(frame.count == 0) {
+                // No touches. Last values are "missing", and we're not tracking any
+                // particular touch ID
+                lastX_ = lastY_ = missing_value<float>::missing();
+                idOfCurrentTouch_ = -1;
+#ifdef DEBUG_PITCHBEND_MAPPING
+                std::cout << "Touch off\n";
+#endif
+            }
+            else if((frame.count == 2 && ignoresTwoFingers_)
+                    || (frame.count == 3 && ignoresThreeFingers_)) {
+                // Multiple touches that we have chosen to ignore. Do nothing for now...
+            }
+            else {
+                // At least one touch. Check if we are already tracking an ID and, if so,
+                // use its coordinates. Otherwise grab the lowest current ID.
+                
+                bool foundCurrentTouch = false;
+                
+                if(idOfCurrentTouch_ >= 0) {
+                    for(int i = 0; i < frame.count; i++) {
+                        if(frame.ids[i] == idOfCurrentTouch_) {
+                            lastY_ = frame.locs[i];
+                            if(frame.locH < 0)
+                                lastX_ = missing_value<float>::missing();
+                            else
+                                lastX_ = frame.locH;
+                            foundCurrentTouch = true;
+                            break;
+                        }
+                    }
+                }
+                
+                if(!foundCurrentTouch) {
+                    // Assign a new touch to be tracked
+                    int lowestRemainingId = INT_MAX;
+                    int lowestIndex = 0;
+                    
+                    for(int i = 0; i < frame.count; i++) {
+                        if(frame.ids[i] < lowestRemainingId) {
+                            lowestRemainingId = frame.ids[i];
+                            lowestIndex = i;
+                        }
+                    }
+                    
+                    if(!bendIsEngaged_)
+                        onsetLocationX_ = onsetLocationY_ = missing_value<float>::missing();
+                    idOfCurrentTouch_ = lowestRemainingId;
+                    lastY_ = frame.locs[lowestIndex];
+                    if(frame.locH < 0)
+                        lastX_ = missing_value<float>::missing();
+                    else
+                        lastX_ = frame.locH;
+#ifdef DEBUG_PITCHBEND_MAPPING
+                    std::cout << "Previous touch stopped; now ID " << idOfCurrentTouch_ << " at (" << lastX_ << ", " << lastY_ << ")\n";
+#endif
+                }
+                
+                // Now we have an X and (maybe) a Y coordinate for the most recent touch.
+                // Check whether we have an initial location (if the note is active).
+                if(noteIsOn_) {
+                    //ScopedLock sl(distanceAccessMutex_);
+                    
+                    if(missing_value<float>::isMissing(onsetLocationY_) ||
+                       (!foundCurrentTouch && !bendIsEngaged_)) {
+                        // Note is on but touch hasn't yet arrived --> this touch becomes
+                        // our onset location. Alternatively, the current touch is a different
+                        // ID from the previous one.
+                        onsetLocationY_ = lastY_;
+                        onsetLocationX_ = lastX_;
+                        
+                        // Clear buffer and start with 0 distance for this point
+                        clearBuffers();
+#ifdef DEBUG_PITCHBEND_MAPPING
+                        std::cout << "Starting at (" << onsetLocationX_ << ", " << onsetLocationY_ << ")\n";
+#endif
+                    }
+                    else {
+                        float distance = 0.0;
+                        
+                        // Note is on and a start location exists. Calculate distance between
+                        // start location and the current point.
+                        
+                        if(missing_value<float>::isMissing(onsetLocationX_) &&
+                           !missing_value<float>::isMissing(lastX_)) {
+                            // No X location indicated for onset but we have one now.
+                            // Update the onset X location.
+                            onsetLocationX_ = lastX_;
+#ifdef DEBUG_PITCHBEND_MAPPING
+                            std::cout << "Found first X location at " << onsetLocationX_ << std::endl;
+#endif
+                        }
+                        
+                        // Distance is based on Y location. TODO: do we need all the X location stuff??
+                        distance = lastY_ - onsetLocationY_;
+                        
+                        // Insert raw distance into the buffer. The rest of the processing takes place
+                        // in the dedicated thread so as not to slow down commmunication with the hardware.
+                        rawDistance_.insert(distance, timestamp);
+                        
+                        // Move the current scheduled event up to the present time.
+                        // FIXME: this may be more inefficient than just doing everything in the current thread!
+#ifdef NEW_MAPPING_SCHEDULER
+                        keyboard_.mappingScheduler().scheduleNow(this);
+#else
+                        keyboard_.unscheduleEvent(this);
+                        keyboard_.scheduleEvent(this, mappingAction_, keyboard_.schedulerCurrentTimestamp());
+#endif
+                        
+                        //std::cout << "Raw distance " << distance << " filtered " << filteredDistance_.latest() << std::endl;
+                    }
+                }
+            }
+        }
+    }
+}
+
+// Mapping method. This actually does the real work of sending OSC data in response to the
+// latest information from the touch sensors or continuous key angle
+timestamp_type TouchkeyPitchBendMapping::performMapping() {
+    //ScopedLock sl(distanceAccessMutex_);
+    
+    timestamp_type currentTimestamp = keyboard_.schedulerCurrentTimestamp();
+    bool newSamplePresent = false;
+    float lastProcessedDistance = missing_value<float>::missing();
+    
+    // Go through the filtered distance samples that are remaining to process.
+    if(lastProcessedIndex_ < rawDistance_.beginIndex() + 1) {
+        // Fell off the beginning of the position buffer. Skip to the samples we have
+        // (shouldn't happen except in cases of exceptional system load, and not too
+        // consequential if it does happen).
+        lastProcessedIndex_ = rawDistance_.beginIndex() + 1;
+    }
+    
+    while(lastProcessedIndex_ < rawDistance_.endIndex()) {
+        float distance = lastProcessedDistance = rawDistance_[lastProcessedIndex_];
+        //timestamp_type timestamp = rawDistance_.timestampAt(lastProcessedIndex_);
+        newSamplePresent = true;
+        
+        if(bendIsEngaged_) {
+            /*
+            // TODO: look for snapping
+            // Raw distance is the distance from note onset. Adjusted distance takes into account
+            // that the bend actually started on the cross of a threshold.
+            float adjustedDistance = rawDistance_.latest() - bendEngageLocation_;
+            float pitchBendSemitones = 0.0;
+            
+            // Calculate pitch bend based on most recent distance
+            if(adjustedDistance > 0.0)
+                pitchBendSemitones = adjustedDistance * bendScalerPositive_;
+            else
+                pitchBendSemitones = adjustedDistance * bendScalerNegative_;
+            
+            // Find the nearest semitone to the current value by rounding
+            currentSnapDestinationSemitones_ = roundf(pitchBendSemitones);
+            
+            if(snapIsEngaged_) {
+                // TODO: check velocity conditions; if above minimum velocity, disengage
+            }
+            else {
+                if(fabsf(pitchBendSemitones - currentSnapDestinationSemitones_) < snapZoneSemitones_) {
+                    // TODO: check velocity conditions; if below minimum velocity, engage
+                    //engageSnapping();
+                }
+            }
+            */
+        }
+        else {
+            // Check if bend should engage, using two thresholds: one as fraction of
+            // key length, one as distance in semitones
+            if(fabsf(distance) > thresholdCombinedMax_) {
+                bendIsEngaged_ = true;
+#ifdef DEBUG_PITCHBEND_MAPPING
+                std::cout << "engaging bend at distance " << distance << std::endl;
+#endif
+                // Set up dynamic scaling based on fixed distances to edge of key.
+                // TODO: make this more flexible, to always nail the nearest semitone (optionally)
+                
+                // This is how far we would have had from the onset point to the edge of key.
+                float distanceToPositiveEdgeWithoutThreshold = 1.0 - onsetLocationY_;
+                float distanceToNegativeEdgeWithoutThreshold = onsetLocationY_;
+                
+                // This is how far we actually have to go to the edge of the key
+                float actualDistanceToPositiveEdge = 1.0 - (onsetLocationY_ + thresholdCombinedMax_);
+                float actualDistanceToNegativeEdge = onsetLocationY_ - thresholdCombinedMax_;
+                
+                // Make it so moving toward edge of key gets as far as it would have without
+                // the distance lost by the threshold
+
+                if(bendMode_ == kPitchBendModeVariableEndpoints) {
+                    if(actualDistanceToPositiveEdge > 0.0)
+                        bendScalerPositive_ = bendRangeSemitones_ * distanceToPositiveEdgeWithoutThreshold / actualDistanceToPositiveEdge;
+                    else
+                        bendScalerPositive_ = bendRangeSemitones_; // Sanity check
+                    if(actualDistanceToNegativeEdge > 0.0)
+                        bendScalerNegative_ = bendRangeSemitones_ * distanceToNegativeEdgeWithoutThreshold / actualDistanceToNegativeEdge;
+                    else
+                        bendScalerNegative_ = bendRangeSemitones_; // Sanity check
+                }
+                else if(bendMode_ == kPitchBendModeFixedEndpoints) {
+                    // TODO: buffer distance at end
+                    if(actualDistanceToPositiveEdge > fixedModeMinEnableDistance_)
+                        bendScalerPositive_ = bendRangeSemitones_ / actualDistanceToPositiveEdge;
+                    else
+                        bendScalerPositive_ = 0.0;
+                    if(actualDistanceToNegativeEdge > fixedModeMinEnableDistance_)
+                        bendScalerNegative_ = bendRangeSemitones_ / actualDistanceToNegativeEdge;
+                    else
+                        bendScalerNegative_ = 0.0;
+                }
+                else // unknown mode
+                    bendScalerPositive_ = bendScalerNegative_ = 0.0;
+            }
+        }
+            
+        lastProcessedIndex_++;
+    }
+    
+    if(bendIsEngaged_ && !missing_value<float>::isMissing(lastProcessedDistance)) {
+        // Having processed every sample individually for detection, send a pitch bend message based on the most
+        // recent one (no sense in sending multiple pitch bend messages simultaneously).
+        if(newSamplePresent) {
+            // Raw distance is the distance from note onset. Adjusted distance takes into account
+            // that the bend actually started on the cross of a threshold.
+            float pitchBendSemitones;
+            
+            if(lastProcessedDistance > thresholdCombinedMax_)
+                pitchBendSemitones = (lastProcessedDistance - thresholdCombinedMax_) * bendScalerPositive_;
+            else if(lastProcessedDistance < -thresholdCombinedMax_)
+                pitchBendSemitones = (lastProcessedDistance + thresholdCombinedMax_) * bendScalerNegative_;
+            else
+                pitchBendSemitones = 0.0;
+    
+            sendPitchBendMessage(pitchBendSemitones);
+            lastPitchBendSemitones_ = pitchBendSemitones;
+        }
+        else if(snapIsEngaged_) {
+            // We may have arrived here without a new touch, just based on timing. Even so, if pitch snapping
+            // is engaged we need to continue to update the pitch
+            
+            // TODO: calculate the next filtered pitch based on snapping
+        }
+    }
+    
+    // Register for the next update by returning its timestamp
+    nextScheduledTimestamp_ = currentTimestamp + updateInterval_;
+    return nextScheduledTimestamp_;
+}
+
+// MIDI note-on message received
+void TouchkeyPitchBendMapping::midiNoteOnReceived(int channel, int velocity) {
+    // MIDI note has gone on. Set the starting location to be most recent
+    // location. It's possible there has been no touch data before this,
+    // in which case lastX and lastY will hold missing values.
+    onsetLocationX_ = lastX_;
+    onsetLocationY_ = lastY_;
+    bendIsEngaged_ = false;
+    if(!missing_value<float>::isMissing(onsetLocationY_)) {
+        // Already have touch data. Clear the buffer here.
+        // Clear buffer and start with 0 distance for this point
+        clearBuffers();
+#ifdef DEBUG_PITCHBEND_MAPPING
+        std::cout << "MIDI on: starting at (" << onsetLocationX_ << ", " << onsetLocationY_ << ")\n";
+#endif
+    }
+    else {
+#ifdef DEBUG_PITCHBEND_MAPPING
+        std::cout << "MIDI on but no touch\n";
+#endif
+    }
+}
+
+// MIDI note-off message received
+void TouchkeyPitchBendMapping::midiNoteOffReceived(int channel) {
+    if(bendIsEngaged_) {
+        // TODO: should anything happen here? No new samples processed anyway,
+        // but we may want the snapping algorithm to still continue its work.
+    }
+}
+
+// Reset variables involved in detecting a pitch bend gesture
+void TouchkeyPitchBendMapping::resetDetectionState() {
+    bendIsEngaged_ = false;
+    snapIsEngaged_ = false;
+}
+
+// Clear the buffers that hold distance measurements
+void TouchkeyPitchBendMapping::clearBuffers() {
+    rawDistance_.clear();
+    rawDistance_.insert(0.0, lastTimestamp_);
+    lastProcessedIndex_ = 0;
+}
+
+// Engage the snapping algorithm to pull the pitch into the nearest semitone
+void TouchkeyPitchBendMapping::engageSnapping() {
+    snapIsEngaged_ = true;
+}
+
+// Disengage the snapping algorithm
+void TouchkeyPitchBendMapping::disengageSnapping() {
+    snapIsEngaged_ = false;
+}
+
+// Set the combined threshold based on the two independent parameters
+// relating to semitones and key length
+void TouchkeyPitchBendMapping::updateCombinedThreshold() {
+    if(thresholdKeyLength_ > thresholdSemitones_ / bendRangeSemitones_)
+        thresholdCombinedMax_ = thresholdKeyLength_;
+    else
+        thresholdCombinedMax_ = thresholdSemitones_ / bendRangeSemitones_;
+}
+
+
+// Send the pitch bend message of a given number of a semitones. Send by OSC,
+// which can be mapped to MIDI CC externally
+void TouchkeyPitchBendMapping::sendPitchBendMessage(float pitchBendSemitones, bool force) {
+    if(force || !suspended_)
+        keyboard_.sendMessage(controlName_.c_str(), "if", noteNumber_, pitchBendSemitones, LO_ARGS_END);
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/Mappings/PitchBend/TouchkeyPitchBendMapping.h	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,156 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+
+  TouchkeyPitchBendMapping.h: per-note mapping for the pitch-bend mapping,
+  which handles changing pitch based on relative finger motion.
+*/
+
+#ifndef __touchkeys__TouchkeyPitchBendMapping__
+#define __touchkeys__TouchkeyPitchBendMapping__
+
+
+
+#include <map>
+#include <boost/bind.hpp>
+#include "../TouchkeyBaseMapping.h"
+#include "../../Utility/IIRFilter.h"
+
+// This class handles the detection and mapping of vibrato gestures
+// based on Touchkey data. It outputs MIDI or OSC messages that
+// can be used to affect the pitch of the active note.
+
+class TouchkeyPitchBendMapping : public TouchkeyBaseMapping {
+    friend class TouchkeyVibratoMappingFactory;
+    friend class TouchkeyPitchBendMappingFactory;
+    
+
+public:
+    enum {
+        kPitchBendModeVariableEndpoints = 1,
+        kPitchBendModeFixedEndpoints
+    };
+    
+private:
+    // Useful constants for mapping MRP messages
+    
+    static const int kDefaultMIDIChannel;
+    static const int kDefaultFilterBufferLength;
+    
+    static const float kDefaultBendRangeSemitones;
+    static const float kDefaultBendThresholdSemitones;
+    static const float kDefaultBendThresholdKeyLength;
+    static const float kDefaultSnapZoneSemitones;
+    static const int kDefaultPitchBendMode;
+    static const float kDefaultFixedModeEnableDistance;
+    static const float kDefaultFixedModeBufferDistance;
+    
+    static const bool kDefaultIgnoresTwoFingers;
+    static const bool kDefaultIgnoresThreeFingers;
+
+
+public:
+	// ***** Constructors *****
+	
+	// Default constructor, passing the buffer on which to trigger
+	TouchkeyPitchBendMapping(PianoKeyboard &keyboard, MappingFactory *factory, int noteNumber, Node<KeyTouchFrame>* touchBuffer,
+                           Node<key_position>* positionBuffer, KeyPositionTracker* positionTracker);
+	
+	// Copy constructor
+	//TouchkeyPitchBendMapping(TouchkeyPitchBendMapping const& obj);
+    
+    // ***** Destructor *****
+    
+    ~TouchkeyPitchBendMapping();
+	
+    // ***** Modifiers *****
+    
+    // Reset the state back initial values
+	void reset();
+    
+    // Resend the current state of all parameters
+    void resend();
+    
+    // Parameters for pitch bend algorithm
+    void setRange(float rangeSemitones);
+    void setThresholds(float thresholdSemitones, float thresholdKeyLength);
+    void setFixedEndpoints(float minimumDistanceToEnable, float bufferAtEnd);
+    void setVariableEndpoints();
+    
+    // Whether we ignore two or three finger touches for calculating pitch bends
+    void setIgnoresMultipleFingers(bool ignoresTwo, bool ignoresThree);
+    
+	// ***** Evaluators *****
+    
+    // This method receives triggers whenever events occur in the touch data or the
+    // continuous key position (state changes only). It alters the behavior and scheduling
+    // of the mapping but does not itself send OSC messages
+	void triggerReceived(TriggerSource* who, timestamp_type timestamp);
+	
+    // This method handles the OSC message transmission. It should be run in the Scheduler
+    // thread provided by PianoKeyboard.
+    timestamp_type performMapping();
+    
+private:
+    // ***** Private Methods *****
+    void midiNoteOnReceived(int channel, int velocity);
+    void midiNoteOffReceived(int channel);
+    
+    void resetDetectionState();
+    void clearBuffers();
+    
+    void engageSnapping();
+    void disengageSnapping();
+    
+    void updateCombinedThreshold();
+    void sendPitchBendMessage(float pitchBendSemitones, bool force = false);
+    
+	// ***** Member Variables *****
+    
+    bool bendIsEngaged_;                        // Whether the pitch bend is currently engaged
+    bool snapIsEngaged_;                        // Whether the note is currently being snapped to nearest semitone
+    
+    float thresholdSemitones_;                  // Minimum motion in semitones before bend engages
+    float thresholdKeyLength_;                  // Minimum motion as a function of key length
+    float thresholdCombinedMax_;                // Whichever is greater of the above two
+    float snapZoneSemitones_;                   // How wide the snap area is around each semitone
+    int bendMode_;                              // What mode the bend works in (fixed, variable, etc.)
+    float fixedModeMinEnableDistance_;          // Minimum distance to engage in fixed mode
+    float fixedModeBufferDistance_;             // Extra distance at end beyond which no bend happens
+    bool ignoresTwoFingers_;                    // Whether this mapping supresses all messages when two
+    bool ignoresThreeFingers_;                  // or three fingers are present
+    
+    float onsetLocationX_, onsetLocationY_;     // Where the touch began at MIDI note on
+    float lastX_, lastY_;                       // Where the touch was at the last frame we received
+    int idOfCurrentTouch_;                      // Which touch ID we're currently following
+    timestamp_type lastTimestamp_;              // When the last data point arrived
+    Node<float>::size_type lastProcessedIndex_; // Index of the last filtered position sample we've handled
+    
+    float bendScalerPositive_, bendScalerNegative_; // Translation between position and semitones for upward and downward motions
+    
+    float currentSnapDestinationSemitones_;     // Where the pitch would snap to if snapping was engaged
+    
+    float bendRangeSemitones_;                  // Amount of pitch bend in one direction at maximum
+    float lastPitchBendSemitones_;              // The last pitch bend value we sent out
+    
+    Node<float> rawDistance_;                   // Distance from onset location
+    CriticalSection distanceAccessMutex_;       // Mutex protecting access to the distance buffer
+};
+
+
+#endif /* defined(__touchkeys__TouchkeyPitchBendMapping__) */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/Mappings/PitchBend/TouchkeyPitchBendMappingFactory.cpp	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,168 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+
+  TouchkeyPitchBendMappingFactory.cpp: factory for the pitch-bend mapping,
+  which handles changing pitch based on relative finger motion.
+*/
+
+#include "TouchkeyPitchBendMappingFactory.h"
+#include "TouchkeyPitchBendMappingShortEditor.h"
+
+// Class constants
+const float TouchkeyPitchBendMappingFactory::kDefaultPitchWheelRangeSemitones = 12.0;
+
+// Default constructor, containing a reference to the PianoKeyboard class.
+
+TouchkeyPitchBendMappingFactory::TouchkeyPitchBendMappingFactory(PianoKeyboard &keyboard, MidiKeyboardSegment& segment) :
+TouchkeyBaseMappingFactory<TouchkeyPitchBendMapping>(keyboard, segment),
+pitchWheelRangeSemitones_(kDefaultPitchWheelRangeSemitones),
+bendRangeSemitones_(TouchkeyPitchBendMapping::kDefaultBendRangeSemitones),
+bendThresholdSemitones_(TouchkeyPitchBendMapping::kDefaultBendThresholdSemitones),
+bendThresholdKeyLength_(TouchkeyPitchBendMapping::kDefaultBendThresholdKeyLength),
+bendMode_(TouchkeyPitchBendMapping::kDefaultPitchBendMode),
+fixedModeMinEnableDistance_(TouchkeyPitchBendMapping::kDefaultFixedModeEnableDistance),
+fixedModeBufferDistance_(TouchkeyPitchBendMapping::kDefaultFixedModeBufferDistance),
+bendIgnoresTwoFingers_(TouchkeyPitchBendMapping::kDefaultIgnoresTwoFingers),
+bendIgnoresThreeFingers_(TouchkeyPitchBendMapping::kDefaultIgnoresThreeFingers)
+{
+    // Set up the MIDI converter to use pitch wheel
+    // setName("/touchkeys/pitchbend");
+    setBendParameters();
+}
+
+// ***** Destructor *****
+
+TouchkeyPitchBendMappingFactory::~TouchkeyPitchBendMappingFactory() {
+    
+}
+
+// ***** Accessors / Modifiers *****
+
+void TouchkeyPitchBendMappingFactory::setMIDIPitchWheelRange(float maxBendSemitones) {
+    if(maxBendSemitones <= 0)
+        return;
+    pitchWheelRangeSemitones_ = maxBendSemitones;
+    
+    // Update the OSC-MIDI converter
+    setMidiParameters(MidiKeyboardSegment::kControlPitchWheel, -pitchWheelRangeSemitones_, pitchWheelRangeSemitones_, 0.0);
+    
+    if(midiConverter_ != 0) {
+        midiConverter_->setMidiPitchWheelRange(pitchWheelRangeSemitones_);
+        midiConverter_->listenToIncomingControl(MidiKeyboardSegment::kControlPitchWheel);
+    }
+}
+
+void TouchkeyPitchBendMappingFactory::setName(const string& name) {
+    TouchkeyBaseMappingFactory<TouchkeyPitchBendMapping>::setName(name);
+    setBendParameters();
+}
+
+
+// ***** Bend Methods *****
+
+void TouchkeyPitchBendMappingFactory::setBendRange(float rangeSemitones, bool updateCurrent) {
+    /*if(updateCurrent) {
+     // Send new range to all active mappings
+     // TODO: mutex protect
+     std::map<int, mapping_pair>::iterator it = mappings_.begin();
+     while(it != mappings_.end()) {
+     // Tell this mapping to update its range
+     TouchkeyPitchBendMapping *mapping = it->second.second;
+     mapping->setRange(rangeSemitones);
+     it++;
+     }
+     }*/
+    
+    bendRangeSemitones_ = rangeSemitones;
+}
+
+void TouchkeyPitchBendMappingFactory::setBendThresholdSemitones(float thresholdSemitones) {
+    bendThresholdSemitones_ = thresholdSemitones;    
+}
+
+void TouchkeyPitchBendMappingFactory::setBendThresholdKeyLength(float thresholdKeyLength) {
+    bendThresholdKeyLength_ = thresholdKeyLength;    
+}
+
+void TouchkeyPitchBendMappingFactory::setBendThresholds(float thresholdSemitones, float thresholdKeyLength, bool updateCurrent) {
+    /*if(updateCurrent) {
+     // Send new range to all active mappings
+     // TODO: mutex protect
+     std::map<int, mapping_pair>::iterator it = mappings_.begin();
+     while(it != mappings_.end()) {
+     // Tell this mapping to update its range
+     TouchkeyPitchBendMapping *mapping = it->second.second;
+     mapping->setThresholds(thresholdSemitones, thresholdKeyLength);
+     it++;
+     }
+     }*/
+    
+    bendThresholdSemitones_ = thresholdSemitones;
+    bendThresholdKeyLength_ = thresholdKeyLength;
+}
+
+// Set the mode to bend a fixed amount up and down the key, regardless of where
+// the touch starts. minimumDistanceToEnable sets a floor below which the bend isn't
+// possible (for starting very close to an edge) and bufferAtEnd sets the amount
+// of key length beyond which no further bend takes place.
+void TouchkeyPitchBendMappingFactory::setBendFixedEndpoints(float minimumDistanceToEnable, float bufferAtEnd) {
+    bendMode_ = TouchkeyPitchBendMapping::kPitchBendModeFixedEndpoints;
+    fixedModeMinEnableDistance_ = minimumDistanceToEnable;
+    fixedModeBufferDistance_ = bufferAtEnd;
+}
+
+// Set the mode to bend an amount proportional to distance, which means
+// that the total range of bend will depend on where the finger started.
+void TouchkeyPitchBendMappingFactory::setBendVariableEndpoints() {
+    bendMode_ = TouchkeyPitchBendMapping::kPitchBendModeVariableEndpoints;
+}
+
+void TouchkeyPitchBendMappingFactory::setBendIgnoresMultipleFingers(bool ignoresTwo, bool ignoresThree) {
+    // TODO: update current
+    bendIgnoresTwoFingers_ = ignoresTwo;
+    bendIgnoresThreeFingers_ = ignoresThree;
+}
+
+// ***** GUI Support *****
+MappingEditorComponent* TouchkeyPitchBendMappingFactory::createBasicEditor() {
+    return new TouchkeyPitchBendMappingShortEditor(*this);
+}
+
+// ***** Private Methods *****
+
+// Set the initial parameters for a new mapping
+void TouchkeyPitchBendMappingFactory::initializeMappingParameters(int noteNumber, TouchkeyPitchBendMapping *mapping) {
+    mapping->setRange(bendRangeSemitones_);
+    mapping->setThresholds(bendThresholdSemitones_, bendThresholdKeyLength_);
+    if(bendMode_ == TouchkeyPitchBendMapping::kPitchBendModeFixedEndpoints) {
+        mapping->setFixedEndpoints(fixedModeMinEnableDistance_, fixedModeBufferDistance_);
+    }
+    else
+        mapping->setVariableEndpoints();
+    mapping->setIgnoresMultipleFingers(bendIgnoresTwoFingers_, bendIgnoresThreeFingers_);
+}
+
+void TouchkeyPitchBendMappingFactory::setBendParameters() {
+    setMidiParameters(MidiKeyboardSegment::kControlPitchWheel, -pitchWheelRangeSemitones_, pitchWheelRangeSemitones_, 0.0);
+    
+    if(midiConverter_ != 0) {
+        midiConverter_->setMidiPitchWheelRange(pitchWheelRangeSemitones_);
+        midiConverter_->listenToIncomingControl(MidiKeyboardSegment::kControlPitchWheel);
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/Mappings/PitchBend/TouchkeyPitchBendMappingFactory.h	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,99 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+
+  TouchkeyPitchBendMappingFactory.h: factory for the pitch-bend mapping,
+  which handles changing pitch based on relative finger motion.
+*/
+
+
+#ifndef __TouchKeys__TouchkeyPitchBendMappingFactory__
+#define __TouchKeys__TouchkeyPitchBendMappingFactory__
+
+
+#include <iostream>
+
+#include "../TouchkeyBaseMappingFactory.h"
+#include "TouchkeyPitchBendMapping.h"
+
+// Factory class to produce Touchkey pitch bend messages
+// This class keeps track of all the active mappings and responds
+// whenever touches or notes begin or end
+
+class TouchkeyPitchBendMappingFactory : public TouchkeyBaseMappingFactory<TouchkeyPitchBendMapping> {
+private:
+    //typedef std::pair<TouchkeyVibratoMapping*, TouchkeyPitchBendMapping*> mapping_pair;
+    //const float kDefaultPitchWheelRangeSemitones = 12.0;
+    static const float kDefaultPitchWheelRangeSemitones;
+    
+public:
+    // ***** Constructor *****
+    
+	// Default constructor, containing a reference to the PianoKeyboard class.
+    TouchkeyPitchBendMappingFactory(PianoKeyboard &keyboard, MidiKeyboardSegment& segment);
+	
+    // ***** Destructor *****
+    
+    ~TouchkeyPitchBendMappingFactory();
+    
+    // ***** Accessors / Modifiers *****
+    
+    virtual const std::string factoryTypeName() { return "Pitch\nBend"; }
+    
+    void setMIDIPitchWheelRange(float maxBendSemitones); // Set the range for the MIDI pitch wheel
+    void setName(const string& name);
+    
+    // ***** Bend-Specific Methods *****
+    
+    float getBendRange() { return bendRangeSemitones_; }
+    float getBendThresholdSemitones() { return bendThresholdSemitones_; }
+    float getBendThresholdKeyLength() { return bendThresholdKeyLength_; }
+    int getBendMode() { return bendMode_; }
+    
+    void setBendRange(float rangeSemitones, bool updateCurrent = false);
+    void setBendThresholdSemitones(float thresholdSemitones);
+    void setBendThresholdKeyLength(float thresholdKeyLength);
+    void setBendThresholds(float thresholdSemitones, float thresholdKeyLength, bool updateCurrent = false);
+    void setBendFixedEndpoints(float minimumDistanceToEnable, float bufferAtEnd);
+    void setBendVariableEndpoints();
+    void setBendIgnoresMultipleFingers(bool ignoresTwo, bool ignoresThree);
+    
+    // ***** GUI Support *****
+    bool hasBasicEditor() { return true; }
+    MappingEditorComponent* createBasicEditor();
+    bool hasExtendedEditor() { return false; }
+    MappingEditorComponent* createExtendedEditor() { return nullptr; }
+
+private:
+    // ***** Private Methods *****
+    void initializeMappingParameters(int noteNumber, TouchkeyPitchBendMapping *mapping);
+    void setBendParameters();
+    
+    float pitchWheelRangeSemitones_;                    // Range of the MIDI pitch wheel (different than vibrato range)
+    
+    float bendRangeSemitones_;                          // Range of the pitch bend component
+    float bendThresholdSemitones_;                      // Threshold for engaging pitch bend
+    float bendThresholdKeyLength_;                      // Threshold in key length for engaging pitch bend
+    int bendMode_;                                      // What mode the bend works in (fixed, variable, etc.)
+    float fixedModeMinEnableDistance_;                  // Minimum distance to engage in fixed mode
+    float fixedModeBufferDistance_;                     // Extra distance at end beyond which no bend happens
+    bool bendIgnoresTwoFingers_;                        // Whether the pitch bends ignore two
+    bool bendIgnoresThreeFingers_;                      // or three fingers on the key at once
+};
+
+#endif /* defined(__TouchKeys__TouchkeyPitchBendMappingFactory__) */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/Mappings/PitchBend/TouchkeyPitchBendMappingShortEditor.cpp	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,258 @@
+/*
+  ==============================================================================
+
+  This is an automatically generated GUI class created by the Introjucer!
+
+  Be careful when adding custom code to these files, as only the code within
+  the "//[xyz]" and "//[/xyz]" sections will be retained when the file is loaded
+  and re-saved.
+
+  Created with Introjucer version: 3.1.0
+
+  ------------------------------------------------------------------------------
+
+  The Introjucer is part of the JUCE library - "Jules' Utility Class Extensions"
+  Copyright 2004-13 by Raw Material Software Ltd.
+
+  ==============================================================================
+*/
+
+//[Headers] You can add your own extra header files here...
+//[/Headers]
+
+#include "TouchkeyPitchBendMappingShortEditor.h"
+
+
+//[MiscUserDefs] You can add your own user definitions and misc code here...
+//[/MiscUserDefs]
+
+//==============================================================================
+TouchkeyPitchBendMappingShortEditor::TouchkeyPitchBendMappingShortEditor (TouchkeyPitchBendMappingFactory& factory)
+    : factory_(factory)
+{
+    addAndMakeVisible (rangeEditor = new TextEditor ("range text editor"));
+    rangeEditor->setMultiLine (false);
+    rangeEditor->setReturnKeyStartsNewLine (false);
+    rangeEditor->setReadOnly (false);
+    rangeEditor->setScrollbarsShown (true);
+    rangeEditor->setCaretVisible (true);
+    rangeEditor->setPopupMenuEnabled (true);
+    rangeEditor->setText (String::empty);
+
+    addAndMakeVisible (rangeLabel = new Label ("range label",
+                                               "Range:"));
+    rangeLabel->setFont (Font (15.00f, Font::plain));
+    rangeLabel->setJustificationType (Justification::centredLeft);
+    rangeLabel->setEditable (false, false, false);
+    rangeLabel->setColour (TextEditor::textColourId, Colours::black);
+    rangeLabel->setColour (TextEditor::backgroundColourId, Colour (0x00000000));
+
+    addAndMakeVisible (thresholdEditor = new TextEditor ("threshold text editor"));
+    thresholdEditor->setMultiLine (false);
+    thresholdEditor->setReturnKeyStartsNewLine (false);
+    thresholdEditor->setReadOnly (false);
+    thresholdEditor->setScrollbarsShown (true);
+    thresholdEditor->setCaretVisible (true);
+    thresholdEditor->setPopupMenuEnabled (true);
+    thresholdEditor->setText (String::empty);
+
+    addAndMakeVisible (thresholdLabel = new Label ("threshold label",
+                                                   "Threshold:"));
+    thresholdLabel->setFont (Font (15.00f, Font::plain));
+    thresholdLabel->setJustificationType (Justification::centredLeft);
+    thresholdLabel->setEditable (false, false, false);
+    thresholdLabel->setColour (TextEditor::textColourId, Colours::black);
+    thresholdLabel->setColour (TextEditor::backgroundColourId, Colour (0x00000000));
+
+    addAndMakeVisible (controlLabel = new Label ("control label",
+                                                 "Endpoints:"));
+    controlLabel->setFont (Font (15.00f, Font::plain));
+    controlLabel->setJustificationType (Justification::centredLeft);
+    controlLabel->setEditable (false, false, false);
+    controlLabel->setColour (TextEditor::textColourId, Colours::black);
+    controlLabel->setColour (TextEditor::backgroundColourId, Colour (0x00000000));
+
+    addAndMakeVisible (endpointsComboBox = new ComboBox ("control combo box"));
+    endpointsComboBox->setEditableText (false);
+    endpointsComboBox->setJustificationType (Justification::centredLeft);
+    endpointsComboBox->setTextWhenNothingSelected (String::empty);
+    endpointsComboBox->setTextWhenNoChoicesAvailable ("(no choices)");
+    endpointsComboBox->addListener (this);
+
+
+    //[UserPreSize]
+    endpointsComboBox->addItem("Variable", TouchkeyPitchBendMapping::kPitchBendModeVariableEndpoints);
+    endpointsComboBox->addItem("Fixed", TouchkeyPitchBendMapping::kPitchBendModeFixedEndpoints);
+    //[/UserPreSize]
+
+    setSize (328, 71);
+
+
+    //[Constructor] You can add your own custom stuff here..
+    rangeEditor->addListener(this);
+    thresholdEditor->addListener(this);
+    //[/Constructor]
+}
+
+TouchkeyPitchBendMappingShortEditor::~TouchkeyPitchBendMappingShortEditor()
+{
+    //[Destructor_pre]. You can add your own custom destruction code here..
+    //[/Destructor_pre]
+
+    rangeEditor = nullptr;
+    rangeLabel = nullptr;
+    thresholdEditor = nullptr;
+    thresholdLabel = nullptr;
+    controlLabel = nullptr;
+    endpointsComboBox = nullptr;
+
+
+    //[Destructor]. You can add your own custom destruction code here..
+    //[/Destructor]
+}
+
+//==============================================================================
+void TouchkeyPitchBendMappingShortEditor::paint (Graphics& g)
+{
+    //[UserPrePaint] Add your own custom painting code here..
+    //[/UserPrePaint]
+
+    g.fillAll (Colours::white);
+
+    //[UserPaint] Add your own custom painting code here..
+    //[/UserPaint]
+}
+
+void TouchkeyPitchBendMappingShortEditor::resized()
+{
+    rangeEditor->setBounds (80, 8, 80, 24);
+    rangeLabel->setBounds (8, 8, 56, 24);
+    thresholdEditor->setBounds (240, 8, 80, 24);
+    thresholdLabel->setBounds (168, 8, 72, 24);
+    controlLabel->setBounds (8, 40, 72, 24);
+    endpointsComboBox->setBounds (80, 40, 80, 24);
+    //[UserResized] Add your own custom resize handling here..
+    //[/UserResized]
+}
+
+void TouchkeyPitchBendMappingShortEditor::comboBoxChanged (ComboBox* comboBoxThatHasChanged)
+{
+    //[UsercomboBoxChanged_Pre]
+    //[/UsercomboBoxChanged_Pre]
+
+    if (comboBoxThatHasChanged == endpointsComboBox)
+    {
+        //[UserComboBoxCode_endpointsComboBox] -- add your combo box handling code here..
+        int control = endpointsComboBox->getSelectedId();
+        if(control == TouchkeyPitchBendMapping::kPitchBendModeVariableEndpoints)
+            factory_.setBendVariableEndpoints();
+        else if(control == TouchkeyPitchBendMapping::kPitchBendModeFixedEndpoints)
+            factory_.setBendFixedEndpoints(factory_.getBendThresholdKeyLength(), 0);
+        //[/UserComboBoxCode_endpointsComboBox]
+    }
+
+    //[UsercomboBoxChanged_Post]
+    //[/UsercomboBoxChanged_Post]
+}
+
+
+
+//[MiscUserCode] You can add your own definitions of your custom methods or any other code here...
+
+void TouchkeyPitchBendMappingShortEditor::textEditorReturnKeyPressed(TextEditor &editor)
+{
+    if(&editor == rangeEditor) {
+        float range = atof(rangeEditor->getText().toUTF8());
+        factory_.setBendRange(range);
+    }
+    else if(&editor == thresholdEditor) {
+        float threshold = atof(thresholdEditor->getText().toUTF8());
+        factory_.setBendThresholdKeyLength(threshold);
+    }
+}
+
+void TouchkeyPitchBendMappingShortEditor::textEditorEscapeKeyPressed(TextEditor &editor)
+{
+
+}
+
+void TouchkeyPitchBendMappingShortEditor::textEditorFocusLost(TextEditor &editor)
+{
+    textEditorReturnKeyPressed(editor);
+}
+
+void TouchkeyPitchBendMappingShortEditor::synchronize()
+{
+    // Update the editors to reflect the current status
+    if(!rangeEditor->hasKeyboardFocus(true)) {
+        float value = factory_.getBendRange();
+        char st[16];
+        snprintf(st, 16, "%.2f", value);
+
+        rangeEditor->setText(st);
+    }
+
+    if(!thresholdEditor->hasKeyboardFocus(true)) {
+        float value = factory_.getBendThresholdKeyLength();
+        char st[16];
+        snprintf(st, 16, "%.2f", value);
+
+        thresholdEditor->setText(st);
+    }
+
+    endpointsComboBox->setSelectedId(factory_.getBendMode(), dontSendNotification);
+}
+//[/MiscUserCode]
+
+
+//==============================================================================
+#if 0
+/*  -- Introjucer information section --
+
+    This is where the Introjucer stores the metadata that describe this GUI layout, so
+    make changes in here at your peril!
+
+BEGIN_JUCER_METADATA
+
+<JUCER_COMPONENT documentType="Component" className="TouchkeyPitchBendMappingShortEditor"
+                 componentName="" parentClasses="public MappingEditorComponent, public TextEditor::Listener"
+                 constructorParams="TouchkeyPitchBendMappingFactory&amp; factory"
+                 variableInitialisers="factory_(factory)" snapPixels="8" snapActive="1"
+                 snapShown="1" overlayOpacity="0.330000013" fixedSize="1" initialWidth="328"
+                 initialHeight="71">
+  <BACKGROUND backgroundColour="ffffffff"/>
+  <TEXTEDITOR name="range text editor" id="db0f62c03a58af03" memberName="rangeEditor"
+              virtualName="" explicitFocusOrder="0" pos="80 8 80 24" initialText=""
+              multiline="0" retKeyStartsLine="0" readonly="0" scrollbars="1"
+              caret="1" popupmenu="1"/>
+  <LABEL name="range label" id="1ca2d422f4c37b7f" memberName="rangeLabel"
+         virtualName="" explicitFocusOrder="0" pos="8 8 56 24" edTextCol="ff000000"
+         edBkgCol="0" labelText="Range:" editableSingleClick="0" editableDoubleClick="0"
+         focusDiscardsChanges="0" fontname="Default font" fontsize="15"
+         bold="0" italic="0" justification="33"/>
+  <TEXTEDITOR name="threshold text editor" id="854a054d84eaf552" memberName="thresholdEditor"
+              virtualName="" explicitFocusOrder="0" pos="240 8 80 24" initialText=""
+              multiline="0" retKeyStartsLine="0" readonly="0" scrollbars="1"
+              caret="1" popupmenu="1"/>
+  <LABEL name="threshold label" id="864de4f55b5481ee" memberName="thresholdLabel"
+         virtualName="" explicitFocusOrder="0" pos="168 8 72 24" edTextCol="ff000000"
+         edBkgCol="0" labelText="Threshold:" editableSingleClick="0" editableDoubleClick="0"
+         focusDiscardsChanges="0" fontname="Default font" fontsize="15"
+         bold="0" italic="0" justification="33"/>
+  <LABEL name="control label" id="f953b12999632418" memberName="controlLabel"
+         virtualName="" explicitFocusOrder="0" pos="8 40 72 24" edTextCol="ff000000"
+         edBkgCol="0" labelText="Endpoints:" editableSingleClick="0" editableDoubleClick="0"
+         focusDiscardsChanges="0" fontname="Default font" fontsize="15"
+         bold="0" italic="0" justification="33"/>
+  <COMBOBOX name="control combo box" id="f1c84bb5fd2730fb" memberName="endpointsComboBox"
+            virtualName="" explicitFocusOrder="0" pos="80 40 80 24" editable="0"
+            layout="33" items="" textWhenNonSelected="" textWhenNoItems="(no choices)"/>
+</JUCER_COMPONENT>
+
+END_JUCER_METADATA
+*/
+#endif
+
+
+//[EndFile] You can add extra defines here...
+//[/EndFile]
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/Mappings/PitchBend/TouchkeyPitchBendMappingShortEditor.h	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,85 @@
+/*
+  ==============================================================================
+
+  This is an automatically generated GUI class created by the Introjucer!
+
+  Be careful when adding custom code to these files, as only the code within
+  the "//[xyz]" and "//[/xyz]" sections will be retained when the file is loaded
+  and re-saved.
+
+  Created with Introjucer version: 3.1.0
+
+  ------------------------------------------------------------------------------
+
+  The Introjucer is part of the JUCE library - "Jules' Utility Class Extensions"
+  Copyright 2004-13 by Raw Material Software Ltd.
+
+  ==============================================================================
+*/
+
+#ifndef __JUCE_HEADER_FBFB213EC27EA3A0__
+#define __JUCE_HEADER_FBFB213EC27EA3A0__
+
+//[Headers]     -- You can add your own extra header files here --
+#include "JuceHeader.h"
+#include "TouchkeyPitchBendMappingFactory.h"
+//[/Headers]
+
+
+
+//==============================================================================
+/**
+                                                                    //[Comments]
+    An auto-generated component, created by the Introjucer.
+
+    Describe your class and how it works here!
+                                                                    //[/Comments]
+*/
+class TouchkeyPitchBendMappingShortEditor  : public MappingEditorComponent,
+                                             public TextEditor::Listener,
+                                             public ComboBoxListener
+{
+public:
+    //==============================================================================
+    TouchkeyPitchBendMappingShortEditor (TouchkeyPitchBendMappingFactory& factory);
+    ~TouchkeyPitchBendMappingShortEditor();
+
+    //==============================================================================
+    //[UserMethods]     -- You can add your own custom methods in this section.
+    // TextEditor listener methods
+    void textEditorTextChanged(TextEditor &editor) {}
+    void textEditorReturnKeyPressed(TextEditor &editor);
+    void textEditorEscapeKeyPressed(TextEditor &editor);
+    void textEditorFocusLost(TextEditor &editor);
+
+    void synchronize();
+    //[/UserMethods]
+
+    void paint (Graphics& g);
+    void resized();
+    void comboBoxChanged (ComboBox* comboBoxThatHasChanged);
+
+
+
+private:
+    //[UserVariables]   -- You can add your own custom variables in this section.
+    TouchkeyPitchBendMappingFactory& factory_;
+    //[/UserVariables]
+
+    //==============================================================================
+    ScopedPointer<TextEditor> rangeEditor;
+    ScopedPointer<Label> rangeLabel;
+    ScopedPointer<TextEditor> thresholdEditor;
+    ScopedPointer<Label> thresholdLabel;
+    ScopedPointer<Label> controlLabel;
+    ScopedPointer<ComboBox> endpointsComboBox;
+
+
+    //==============================================================================
+    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (TouchkeyPitchBendMappingShortEditor)
+};
+
+//[EndFile] You can add extra defines here...
+//[/EndFile]
+
+#endif   // __JUCE_HEADER_FBFB213EC27EA3A0__
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/Mappings/ReleaseAngle/TouchkeyReleaseAngleMapping.cpp	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,224 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+
+  TouchkeyReleaseAngleMapping.cpp: per-note mapping for the release angle
+  mapping, which measures the speed of finger motion along the key at
+  the time of MIDI note off.
+*/
+
+#include "TouchkeyReleaseAngleMapping.h"
+#include "../MappingFactory.h"
+#include "../../TouchKeys/MidiOutputController.h"
+#include "../MappingScheduler.h"
+
+// Class constants
+const int TouchkeyReleaseAngleMapping::kDefaultFilterBufferLength = 30;
+const timestamp_diff_type TouchkeyReleaseAngleMapping::kDefaultMaxLookbackTime = milliseconds_to_timestamp(100);
+
+// Main constructor takes references/pointers from objects which keep track
+// of touch location, continuous key position and the state detected from that
+// position. The PianoKeyboard object is strictly required as it gives access to
+// Scheduler and OSC methods. The others are optional since any given system may
+// contain only one of continuous key position or touch sensitivity
+TouchkeyReleaseAngleMapping::TouchkeyReleaseAngleMapping(PianoKeyboard &keyboard, MappingFactory *factory, int noteNumber, Node<KeyTouchFrame>* touchBuffer,
+                                                         Node<key_position>* positionBuffer, KeyPositionTracker* positionTracker)
+: TouchkeyBaseMapping(keyboard, factory, noteNumber, touchBuffer, positionBuffer, positionTracker, false),
+  pastSamples_(kDefaultFilterBufferLength), maxLookbackTime_(kDefaultMaxLookbackTime)
+{
+}
+
+// Copy constructor
+/*TouchkeyReleaseAngleMapping::TouchkeyReleaseAngleMapping(TouchkeyReleaseAngleMapping const& obj)
+: TouchkeyBaseMapping(obj), pastSamples_(obj.pastSamples_), maxLookbackTime_(obj.maxLookbackTime_)
+{
+}*/
+
+// Reset state back to defaults
+void TouchkeyReleaseAngleMapping::reset() {
+    ScopedLock sl(sampleBufferMutex_);
+    
+    TouchkeyBaseMapping::reset();
+    pastSamples_.clear();
+}
+
+// Resend all current parameters
+void TouchkeyReleaseAngleMapping::resend() {
+    // Message is only sent at release; resend may not apply here.
+}
+
+// This method receives data from the touch buffer or possibly the continuous key angle (not used here)
+void TouchkeyReleaseAngleMapping::triggerReceived(TriggerSource* who, timestamp_type timestamp) {
+    if(who == touchBuffer_) {
+        ScopedLock sl(sampleBufferMutex_);
+        
+        // Save the latest frame, even if it is an empty touch (we need to know what happened even
+        // after the touch ends since the MIDI off may come later)
+        if(!touchBuffer_->empty())
+            pastSamples_.insert(touchBuffer_->latest(), touchBuffer_->latestTimestamp());
+    }
+}
+
+// Mapping method. This actually does the real work of sending OSC data in response to the
+// latest information from the touch sensors or continuous key angle
+timestamp_type TouchkeyReleaseAngleMapping::performMapping() {
+    // Nothing to do here until note is released.
+    // Register for the next update by returning its timestamp
+    // TODO: do we even need this? Check Mapping::engage() and Mapping::disengage()
+    timestamp_type currentTimestamp = keyboard_.schedulerCurrentTimestamp();
+    nextScheduledTimestamp_ = currentTimestamp + updateInterval_;
+    return nextScheduledTimestamp_;
+}
+
+void TouchkeyReleaseAngleMapping::processRelease(timestamp_type timestamp) {
+    if(!noteIsOn_) {
+        return;
+    }
+    
+    sampleBufferMutex_.enter();
+    
+    // Look backwards from the current timestamp to find the velocity
+    float calculatedVelocity = missing_value<float>::missing();
+    bool touchWasOn = false;
+    
+    
+    //std::cout << "processRelease begin = " << pastSamples_.beginIndex() << " end = " << pastSamples_.endIndex() << "\n";
+    
+    if(!pastSamples_.empty()) {
+        Node<KeyTouchFrame>::size_type index = pastSamples_.endIndex() - 1;
+        Node<KeyTouchFrame>::size_type mostRecentTouchPresentIndex = pastSamples_.endIndex() - 1;
+        while(index >= pastSamples_.beginIndex()) {
+            //std::cout << "examining sample " << index << " with " << pastSamples_[index].count << " touches and time diff " << timestamp - pastSamples_.timestampAt(index) << "\n";
+            if(timestamp - pastSamples_.timestampAt(index) >= maxLookbackTime_)
+                break;
+            if(pastSamples_[index].count == 0) {
+                if(touchWasOn) {
+                    // We found a break in the touch; stop here. But don't stop
+                    // if the first frames we consider have no touches.
+                    if(index < pastSamples_.endIndex() - 1)
+                        index++;
+                    break;
+                }
+            }
+            else if(!touchWasOn) {
+                mostRecentTouchPresentIndex = index;
+                touchWasOn = true;
+            }
+            // Can't decrement past 0 in an unsigned type
+            if(index == 0)
+                break;
+            index--;
+        }
+        
+        // If we fell off the beginning of the buffer, back up.
+        if(index < pastSamples_.beginIndex())
+            index =  pastSamples_.beginIndex();
+        
+        //std::cout << "done\n";
+        
+        // Need at least two points for this calculation to work
+        timestamp_type endingTimestamp = pastSamples_.timestampAt(mostRecentTouchPresentIndex);
+        timestamp_type startingTimestamp = pastSamples_.timestampAt(index);
+        if(endingTimestamp - startingTimestamp > 0) {
+            float endingPosition = pastSamples_[mostRecentTouchPresentIndex].locs[0];
+            float startingPosition = pastSamples_[index].locs[0];
+            calculatedVelocity = (endingPosition - startingPosition) / (endingTimestamp - startingTimestamp);
+        }
+        else { // DEBUG
+            std::cout << "Found 0 timestamp difference on key release (indices " << index << " and " << pastSamples_.endIndex() - 1 << "\n";
+        }
+    }
+    else
+        std::cout << "Found empty touch buffer on key release\n";
+    
+    sampleBufferMutex_.exit();
+    
+    if(!missing_value<float>::isMissing(calculatedVelocity)) {
+        std::cout << "Found release velocity " << calculatedVelocity << " on note " << noteNumber_ << std::endl;
+        sendReleaseAngleMessage(calculatedVelocity);
+    }
+
+    
+    // Check if we're suppose to clean up now
+    finished_ = true;
+    if(finishRequested_)
+        acknowledgeFinish();
+    // KLUDGE
+}
+
+#define TROMBONE
+void TouchkeyReleaseAngleMapping::sendReleaseAngleMessage(float releaseAngle, bool force) {
+    if(force || !suspended_) {
+        keyboard_.sendMessage("/touchkeys/releaseangle", "if", noteNumber_, releaseAngle, LO_ARGS_END);
+        
+#ifdef TROMBONE
+        // KLUDGE: figure out how to do this more elegantly
+        if(keyboard_.midiOutputController() != 0) {
+            if(releaseAngle > 1.0) {
+                keyboard_.midiOutputController()->sendNoteOn(0, 0, 36, 64);
+                keyboard_.midiOutputController()->sendNoteOn(0, 0, 31, 96);
+                keyboard_.midiOutputController()->sendNoteOff(0, 0, 31);
+                keyboard_.midiOutputController()->sendNoteOff(0, 0, 36);
+            }
+            else if(releaseAngle < -1.5) {
+                keyboard_.midiOutputController()->sendNoteOn(0, 0, 36, 64);
+                keyboard_.midiOutputController()->sendNoteOn(0, 0, 33, 80);
+                keyboard_.midiOutputController()->sendNoteOff(0, 0, 33);
+                keyboard_.midiOutputController()->sendNoteOff(0, 0, 36);
+            }
+        }
+#elif defined(TRUMPET)
+        if(keyboard_.midiOutputController() != 0) {
+            if(releaseAngle > 1.0) {
+                keyboard_.midiOutputController()->sendNoteOn(0, 0, 48, 64);
+                keyboard_.midiOutputController()->sendNoteOn(0, 0, 42, 96);
+                //keyboard_.midiOutputController()->sendNoteOff(0, 0, 42);
+                keyboard_.midiOutputController()->sendNoteOff(0, 0, 48);
+                keyboard_.scheduleEvent(this, boost::bind(&TouchkeyReleaseAngleMapping::releaseKeySwitch, this),
+                                        keyboard_.schedulerCurrentTimestamp() + milliseconds_to_timestamp(250));
+            }
+            else if(releaseAngle < -1.5) {
+                keyboard_.midiOutputController()->sendNoteOn(0, 0, 48, 64);
+                keyboard_.midiOutputController()->sendNoteOn(0, 0, 46, 96);
+                //keyboard_.midiOutputController()->sendNoteOff(0, 0, 47);
+                keyboard_.midiOutputController()->sendNoteOff(0, 0, 48);
+                keyboard_.scheduleEvent(this, boost::bind(&TouchkeyReleaseAngleMapping::releaseKeySwitch, this),
+                                        keyboard_.schedulerCurrentTimestamp() + milliseconds_to_timestamp(250));
+            }
+            else {
+                // Check if we're suppose to clean up now
+                finished_ = true;
+                if(finishRequested_)
+                    acknowledgeFinish();
+            }
+        }
+#endif
+    }
+}
+
+timestamp_type TouchkeyReleaseAngleMapping::releaseKeySwitch() {
+    keyboard_.midiOutputController()->sendNoteOff(0, 0, 42);
+    keyboard_.midiOutputController()->sendNoteOff(0, 0, 46);
+    
+    // Check if we're suppose to clean up now
+    /*finished_ = true;
+    if(finishRequested_)
+        acknowledgeFinish();*/
+    
+    return 0;
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/Mappings/ReleaseAngle/TouchkeyReleaseAngleMapping.h	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,89 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+
+  TouchkeyReleaseAngleMapping.h: per-note mapping for the release angle
+  mapping, which measures the speed of finger motion along the key at
+  the time of MIDI note off.
+*/
+
+#ifndef __TouchKeys__TouchkeyReleaseAngleMapping__
+#define __TouchKeys__TouchkeyReleaseAngleMapping__
+
+#include "../TouchkeyBaseMapping.h"
+
+// This class handles the detection of finger motion specifically at
+// note release, which can be used to trigger specific release effects.
+
+class TouchkeyReleaseAngleMapping : public TouchkeyBaseMapping {
+private:
+    // Default values
+    /*constexpr static const int kDefaultFilterBufferLength = 30;
+    constexpr static const timestamp_diff_type kDefaultMaxLookbackTime = milliseconds_to_timestamp(100);*/
+    
+    static const int kDefaultFilterBufferLength;
+    static const timestamp_diff_type kDefaultMaxLookbackTime;
+    
+public:
+	// ***** Constructors *****
+	
+	// Default constructor, passing the buffer on which to trigger
+	TouchkeyReleaseAngleMapping(PianoKeyboard &keyboard, MappingFactory *factory, int noteNumber, Node<KeyTouchFrame>* touchBuffer,
+                                Node<key_position>* positionBuffer, KeyPositionTracker* positionTracker);
+	
+	// Copy constructor
+	//TouchkeyReleaseAngleMapping(TouchkeyReleaseAngleMapping const& obj);
+	
+    // ***** Modifiers *****
+    
+    // Reset the state back initial values
+    void reset();
+    
+    // Resend the current state of all parameters
+    void resend();
+    
+	// ***** Evaluators *****
+    
+    // This method receives triggers whenever events occur in the touch data or the
+    // continuous key position (state changes only). It alters the behavior and scheduling
+    // of the mapping but does not itself send OSC messages
+	void triggerReceived(TriggerSource* who, timestamp_type timestamp);
+	
+    // This method handles the OSC message transmission. It should be run in the Scheduler
+    // thread provided by PianoKeyboard.
+    timestamp_type performMapping();
+
+    // ***** Specific Methods *****
+    // Process the release by calculating the angle
+    void processRelease(timestamp_type timestamp);
+    
+    timestamp_type releaseKeySwitch();
+    
+private:
+    // ***** Private Methods *****
+    
+    void sendReleaseAngleMessage(float releaseAngle, bool force = false);
+    
+	// ***** Member Variables *****
+    
+    Node<KeyTouchFrame> pastSamples_;           // Locations of touch
+    timestamp_diff_type maxLookbackTime_;       // How long to look backwards to find release velocity
+    CriticalSection sampleBufferMutex_;         // Mutex to protect threaded access to sample buffer
+};
+
+#endif /* defined(__TouchKeys__TouchkeyReleaseAngleMapping__) */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/Mappings/ReleaseAngle/TouchkeyReleaseAngleMappingFactory.cpp	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,45 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+
+  TouchkeyReleaseAngleMappingFactory.cpp: factory for the release angle
+  mapping, which measures the speed of finger motion along the key at
+  the time of MIDI note off.
+*/
+
+#include "TouchkeyReleaseAngleMappingFactory.h"
+
+// Class constants
+const timestamp_diff_type TouchkeyReleaseAngleMappingFactory::kDefaultMaxLookbackTime = milliseconds_to_timestamp(100);
+
+
+TouchkeyReleaseAngleMappingFactory::TouchkeyReleaseAngleMappingFactory(PianoKeyboard &keyboard, MidiKeyboardSegment& segment)
+: TouchkeyBaseMappingFactory<TouchkeyReleaseAngleMapping>(keyboard, segment) {}
+
+// MIDI note ended: see whether the mapping was suspended and if not, execute the angle calculation
+void TouchkeyReleaseAngleMappingFactory::midiNoteOff(int noteNumber, bool touchIsOn, bool keyMotionActive,
+                                                     Node<KeyTouchFrame>* touchBuffer,
+                                                     Node<key_position>* positionBuffer,
+                                                     KeyPositionTracker* positionTracker) {
+    if(mappings_.count(noteNumber) != 0) {
+        mappings_[noteNumber]->processRelease(keyboard_.schedulerCurrentTimestamp());
+    }
+    
+    // Call base class method
+    TouchkeyBaseMappingFactory<TouchkeyReleaseAngleMapping>::midiNoteOff(noteNumber, touchIsOn, keyMotionActive, touchBuffer, positionBuffer, positionTracker);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/Mappings/ReleaseAngle/TouchkeyReleaseAngleMappingFactory.h	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,59 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+
+  TouchkeyReleaseAngleMappingFactory.h: factory for the release angle
+  mapping, which measures the speed of finger motion along the key at
+  the time of MIDI note off.
+*/
+
+#ifndef __TouchKeys__TouchkeyReleaseAngleMappingFactory__
+#define __TouchKeys__TouchkeyReleaseAngleMappingFactory__
+
+#include "../TouchkeyBaseMappingFactory.h"
+#include "TouchkeyReleaseAngleMapping.h"
+
+class TouchkeyReleaseAngleMappingFactory : public TouchkeyBaseMappingFactory<TouchkeyReleaseAngleMapping> {
+private:
+    //constexpr static const timestamp_diff_type kDefaultMaxLookbackTime = milliseconds_to_timestamp(100);
+    static const timestamp_diff_type kDefaultMaxLookbackTime;
+    
+public:
+    // ***** Constructor *****
+    
+	// Default constructor, containing a reference to the PianoKeyboard class.
+    TouchkeyReleaseAngleMappingFactory(PianoKeyboard &keyboard, MidiKeyboardSegment& segment);
+	
+    // ***** Destructor *****
+    
+    ~TouchkeyReleaseAngleMappingFactory() {}
+    
+    // ***** Accessors / Modifiers *****
+    virtual const std::string factoryTypeName() { return "Release\nAngle"; }
+    
+    // ***** State Updaters *****
+    
+    // Override the MIDI note off method to process the release angle
+    void midiNoteOff(int noteNumber, bool touchIsOn, bool keyMotionActive,
+                     Node<KeyTouchFrame>* touchBuffer,
+                     Node<key_position>* positionBuffer,
+                     KeyPositionTracker* positionTracker);
+    
+};
+
+#endif /* defined(__TouchKeys__TouchkeyReleaseAngleMappingFactory__) */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/Mappings/TouchkeyBaseMapping.cpp	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,145 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+
+  TouchkeyBaseMapping.cpp: base class from which all TouchKeys-specific
+  mappings derive. Like its Mapping parent class, it handles a mapping
+  for one specific note.
+*/
+
+#include "TouchkeyBaseMapping.h"
+#include "MappingFactory.h"
+#include "../TouchKeys/MidiOutputController.h"
+
+// Main constructor takes references/pointers from objects which keep track
+// of touch location, continuous key position and the state detected from that
+// position. The PianoKeyboard object is strictly required as it gives access to
+// Scheduler and OSC methods. The others are optional since any given system may
+// contain only one of continuous key position or touch sensitivity
+TouchkeyBaseMapping::TouchkeyBaseMapping(PianoKeyboard &keyboard, MappingFactory *factory, int noteNumber, Node<KeyTouchFrame>* touchBuffer,
+                                                         Node<key_position>* positionBuffer, KeyPositionTracker* positionTracker,
+                                                         bool finishesAutomatically)
+: Mapping(keyboard, factory, noteNumber, touchBuffer, positionBuffer, positionTracker),
+  controlName_(""), noteIsOn_(false), finished_(true), finishRequested_(false), finishesAutomatically_(finishesAutomatically)
+{
+    setOscController(&keyboard_);
+}
+
+// Copy constructor
+/*TouchkeyBaseMapping::TouchkeyBaseMapping(TouchkeyBaseMapping const& obj)
+: Mapping(obj), noteIsOn_(obj.noteIsOn_), finished_(obj.finished_), finishRequested_(obj.finishRequested_),
+  finishesAutomatically_(obj.finishesAutomatically_)
+{
+    setOscController(&keyboard_);
+}*/
+
+TouchkeyBaseMapping::~TouchkeyBaseMapping() {
+#ifndef NEW_MAPPING_SCHEDULER
+    try {
+        disengage();
+    }
+    catch(...) {
+        std::cerr << "~TouchkeyBaseMapping(): exception during disengage()\n";
+    }
+#endif
+}
+
+// Turn on mapping of data.
+void TouchkeyBaseMapping::engage() {
+    Mapping::engage();
+    
+    // Register for OSC callbacks on MIDI note on/off
+    addOscListener("/midi/noteon");
+	addOscListener("/midi/noteoff");
+    
+    //cout << "TouchkeyBaseMapping::engage(): after TS " << keyboard_.schedulerCurrentTimestamp() << std::endl;
+}
+
+// Turn off mapping of data. Remove our callback from the scheduler
+void TouchkeyBaseMapping::disengage(bool shouldDelete) {
+    // Remove OSC listeners first
+    removeOscListener("/midi/noteon");
+	removeOscListener("/midi/noteoff");
+    
+    // Don't send any change in bend, let it stay where it is
+    
+    Mapping::disengage(shouldDelete);
+    
+    noteIsOn_ = false;
+}
+
+// Reset state back to defaults
+void TouchkeyBaseMapping::reset() {
+    Mapping::reset();
+    noteIsOn_ = false;
+}
+
+// Set the name of the control
+void TouchkeyBaseMapping::setName(const std::string& name) {
+    controlName_ = name;
+}
+
+// OSC handler method. Called from PianoKeyboard when MIDI data comes in.
+bool TouchkeyBaseMapping::oscHandlerMethod(const char *path, const char *types, int numValues, lo_arg **values, void *data) {
+    if(!strcmp(path, "/midi/noteon") && !noteIsOn_ && numValues >= 1) {
+        if(types[0] == 'i' && values[0]->i == noteNumber_) {
+            // First notify the subclass of this event
+            if(numValues >= 3)
+                midiNoteOnReceived(values[1]->i, values[2]->i);
+            
+            // When the MIDI note goes on, if the mapping doesn't finish automatically,
+            // make sure we indicate we're busy so the factory doesn't delete the mapping when
+            // touch and MIDI both stop. In this case, the subclass is responsible for notifying
+            // the factory when finished.
+            if(!finishesAutomatically_)
+                finished_ = false;
+            noteIsOn_ = true;
+            return false;
+        }
+    }
+    else if(!strcmp(path, "/midi/noteoff") && noteIsOn_ && numValues >= 1) {
+        if(types[0] == 'i' && values[0]->i == noteNumber_) {
+            // MIDI note goes off.
+            
+            // First notify the subclass of this event
+            if(numValues >= 2)
+                midiNoteOffReceived(values[1]->i);
+            
+            noteIsOn_ = false;
+            return false;
+        }
+    }
+    
+    return false;
+}
+
+// Override the requestFinish method to return whether the object
+// can be deleted now or not. If not, delete it when we've finished the
+// release processing
+bool TouchkeyBaseMapping::requestFinish() {
+    finishRequested_ = true;
+    return finished_;
+}
+
+// Acknowledge that the mapping is finished. Only relevant if the mapping doesn't
+// finish automatically.
+void TouchkeyBaseMapping::acknowledgeFinish() {
+    if(!finishRequested_ || finishesAutomatically_)
+        return;
+    factory_->mappingFinished(noteNumber_);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/Mappings/TouchkeyBaseMapping.h	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,118 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+
+  TouchkeyBaseMapping.h: base class from which all TouchKeys-specific
+  mappings derive. Like its Mapping parent class, it handles a mapping
+  for one specific note.
+*/
+
+#ifndef TouchKeys_TouchkeyBaseMapping_h
+#define TouchKeys_TouchkeyBaseMapping_h
+
+
+#include <map>
+#include <boost/bind.hpp>
+#include "../TouchKeys/KeyTouchFrame.h"
+#include "../TouchKeys/KeyPositionTracker.h"
+#include "../TouchKeys/PianoKeyboard.h"
+#include "Mapping.h"
+
+// This class is a virtual base class for mappings which work specifically with TouchKeys
+// and MIDI data (not continuous key angle), designed to work with TouchkeyBaseMappingFactory.
+// Specific behaviors will be implemented in subclasses.
+
+class TouchkeyBaseMapping : public Mapping, public OscHandler {
+    friend class MappingFactory;
+    
+public:
+	// ***** Constructors *****
+	
+	// Default constructor, passing the buffer on which to trigger
+	TouchkeyBaseMapping(PianoKeyboard &keyboard, MappingFactory *factory, int noteNumber, Node<KeyTouchFrame>* touchBuffer,
+                        Node<key_position>* positionBuffer, KeyPositionTracker* positionTracker, bool finishesAutomatically = true);
+    
+    // ***** Destructor *****
+    
+    virtual ~TouchkeyBaseMapping();
+	
+    // ***** Modifiers *****
+    
+    // Enable mappings to be sent
+    virtual void engage();
+    
+    // Disable mappings from being sent
+    virtual void disengage(bool shouldDelete = false);
+	
+    // Reset the state back initial values
+	virtual void reset();
+    
+    // Resend the current state of all parameters
+    virtual void resend() = 0;
+    
+    // ***** Parameters *****
+    
+    // Name for this control, used in the OSC path
+    virtual void setName(const std::string& name);
+    
+	// ***** Evaluators *****
+    
+    // OSC Handler Method: called by PianoKeyboard (or other OSC source)
+	virtual bool oscHandlerMethod(const char *path, const char *types, int numValues, lo_arg **values, void *data);
+	
+    // This method receives triggers whenever events occur in the touch data or the
+    // continuous key position (state changes only). It alters the behavior and scheduling
+    // of the mapping but does not itself send OSC messages
+	virtual void triggerReceived(TriggerSource* who, timestamp_type timestamp) = 0;
+	
+    // This method handles the OSC message transmission. It should be run in the Scheduler
+    // thread provided by PianoKeyboard.
+    virtual timestamp_type performMapping() = 0;
+    
+    // Override the finished() method to give the note time for a post-release action.
+    virtual bool requestFinish();
+    
+    // Acknowledge the finish when the mapping is done, removing the mapping
+    virtual void acknowledgeFinish();
+    
+protected:
+    // ***** Internal Methods for MIDI *****
+    
+    // These methods are called when an OSC message is received indicating
+    // a MIDI note on/off message took place. They let the subclass insert
+    // its own code right before the main MIDI processing.
+    
+    virtual void midiNoteOnReceived(int channel, int velocity) {}
+    virtual void midiNoteOffReceived(int channel) {}
+    
+	// ***** Member Variables *****
+    
+    std::string controlName_;                   // Name of this control, used in the OSC message
+    bool noteIsOn_;                             // Whether the MIDI note is active or not
+    bool finished_;                             // Whether the note is finished
+    bool finishRequested_;                      // Whether the factory has requested a finish
+    bool finishesAutomatically_;                // Whether the mapping finishes automatically when requested
+    
+private:
+    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (TouchkeyBaseMapping)
+};
+
+
+
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/Mappings/TouchkeyBaseMappingFactory.h	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,389 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+
+  TouchkeyBaseMappingFactory.h: base factory class specifically for
+  TouchKeys mappings. It provides a collection of useful methods for
+  creating and destroying individual mappings on touch/MIDI onset and
+  release, as well as parameter adjustment code and OSC to MIDI conversion.
+  This is a template class that must be created with a specific Mapping
+  subclass.
+*/
+
+#ifndef __TouchKeys__TouchkeyBaseMappingFactory__
+#define __TouchKeys__TouchkeyBaseMappingFactory__
+
+#include <iostream>
+#include <map>
+#include <sstream>
+#include "MappingFactory.h"
+#include "../TouchKeys/OscMidiConverter.h"
+#include "../TouchKeys/MidiOutputController.h"
+#include "../TouchKeys/MidiKeyboardSegment.h"
+#include "MappingScheduler.h"
+
+#undef DEBUG_TOUCHKEY_BASE_MAPPING_FACTORY
+
+// Base class for mapping factories that meet the following criteria:
+// * MIDI and TouchKeys data (no continuous angle)
+// * Mappings begin when either or touch or MIDI starts and end when both finish
+// * Each mapping object affects a single note
+
+template <class MappingType>
+class TouchkeyBaseMappingFactory : public MappingFactory {
+    
+public:
+    // ***** Constructor *****
+    
+	// Default constructor, containing a reference to the PianoKeyboard class.
+    TouchkeyBaseMappingFactory(PianoKeyboard &keyboard, MidiKeyboardSegment& segment) :
+      MappingFactory(keyboard), keyboardSegment_(segment), midiConverter_(0),
+      controlName_(""),
+      inputRangeMin_(0.0), inputRangeMax_(1.0), inputRangeCenter_(0.0),
+      outOfRangeBehavior_(OscMidiConverter::kOutOfRangeClip),
+      midiControllerNumber_(-1), bypassed_(false), activeNotes_(0x0FFF) {}
+    
+    // ***** Destructor *****
+    
+    virtual ~TouchkeyBaseMappingFactory()  {
+        removeAllMappings();
+        if(midiConverter_ != 0 && controlName_ != "")
+            midiConverter_->removeControl(controlName_.c_str());
+        if(midiControllerNumber_ >= 0) {
+            keyboardSegment_.releaseOscMidiConverter(midiControllerNumber_);
+        }
+    }
+    
+    // ***** Accessors / Modifiers *****
+   
+    // Return the keyboard segment associated with this factory
+    MidiKeyboardSegment& segment() { return keyboardSegment_; }
+    
+    // Look up a mapping with the given note number
+    virtual MappingType* mapping(int noteNumber) {
+        ScopedLock sl(mappingsMutex_);
+        if(mappings_.count(noteNumber) == 0)
+            return 0;
+        return mappings_[noteNumber];
+    }
+    
+    // Return a list of all active notes
+    virtual std::vector<int> activeMappings()  {
+        ScopedLock sl(mappingsMutex_);
+        std::vector<int> keys;
+        typename std::map<int, MappingType*>::iterator it = mappings_.begin();
+        while(it != mappings_.end()) {
+            int nextKey = (it++)->first;
+            keys.push_back(nextKey);
+        }
+        return keys;
+    }
+
+    // Remove all active mappings
+    virtual void removeAllMappings() {
+        ScopedLock sl(mappingsMutex_);
+        typename std::map<int, MappingType*>::iterator it = mappings_.begin();
+        
+        while(it != mappings_.end()) {
+            // Delete everybody in the container
+            MappingType *mapping = it->second;
+#ifdef NEW_MAPPING_SCHEDULER
+            mapping->disengage(true);
+            //keyboard_.mappingScheduler().unscheduleAndDelete(mapping);
+#else
+            mapping->disengage();
+            delete mapping;
+#endif
+            it++;
+        }
+        
+        // Now clear the container
+        mappings_.clear();
+    }
+    
+    // Callback from mapping to say it's finished
+    virtual void mappingFinished(int noteNumber) {
+        ScopedLock sl(mappingsMutex_);
+        removeMapping(noteNumber);
+    }
+    
+    // Suspend messages from a particular note
+    virtual void suspendMapping(int noteNumber) {
+        ScopedLock sl(mappingsMutex_);
+        if(mappings_.count(noteNumber) == 0)
+            return;
+        mappings_[noteNumber]->suspend();
+    }
+    
+    // Suspend messages from all notes
+    virtual void suspendAllMappings() {
+        ScopedLock sl(mappingsMutex_);
+        typename std::map<int, MappingType*>::iterator it = mappings_.begin();
+        
+        while(it != mappings_.end()) {
+            //std::cout << "suspending mapping on note " << it->first << std::endl;
+            it->second->suspend();
+            it++;
+        }
+    }
+    
+    // Resume messages from a particular note
+    virtual void resumeMapping(int noteNumber, bool resend) {
+        ScopedLock sl(mappingsMutex_);
+        if(mappings_.count(noteNumber) == 0)
+            return;
+        //std::cout << "resuming mapping on note " << noteNumber << std::endl;
+        mappings_[noteNumber]->resume(resend);
+    }
+    
+    // Resume messages on all notes
+    virtual void resumeAllMappings(bool resend) {
+        ScopedLock sl(mappingsMutex_);
+        typename std::map<int, MappingType*>::iterator it = mappings_.begin();
+        
+        while(it != mappings_.end()) {
+            it->second->resume(resend);
+            it++;
+        }
+    }
+    
+    // Whether this mapping is bypassed
+    virtual int bypassed() {
+        return bypassed_ ? kBypassOn : kBypassOff;
+    }
+    
+    // Set whether the mapping is bypassed or not
+    virtual void setBypassed(bool bypass) {
+        bypassed_ = bypass;
+    }
+    
+    // ***** Class-Specific Methods *****
+    
+    virtual void setMidiParameters(int controller, float inputMinValue, float inputMaxValue, float inputCenterValue,
+                           int outputDefaultValue = -1, int outputMinValue = -1, int outputMaxValue = -1,
+                           int outputCenterValue = -1, bool use14BitControl = false,
+                           int outOfRangeBehavior = OscMidiConverter::kOutOfRangeClip) {
+        if(controller < 0)
+            return;
+        
+        inputRangeMin_ = inputMinValue;
+        inputRangeMax_ = inputMaxValue;
+        inputRangeCenter_ = inputCenterValue;
+        outOfRangeBehavior_ = outOfRangeBehavior;
+        
+        // Remove listener on previous name (if any)
+        //midiConverter_.removeAllControls();
+        if(midiControllerNumber_ >= 0 && controller != midiControllerNumber_) {
+            keyboardSegment_.releaseOscMidiConverter(midiControllerNumber_);
+            midiConverter_ = keyboardSegment_.acquireOscMidiConverter(controller);
+        }
+        else if(midiControllerNumber_ < 0 || midiConverter_ == 0) {
+            midiConverter_ = keyboardSegment_.acquireOscMidiConverter(controller);
+        }        
+        midiControllerNumber_ = controller;
+
+        midiConverter_->setMidiMessageType(outputDefaultValue, outputMinValue, outputMaxValue, outputCenterValue, use14BitControl);
+
+        // Add listener for new name
+        if(controlName_ != "")
+            midiConverter_->addControl(controlName_.c_str(), 1, inputRangeMin_, inputRangeMax_, inputRangeCenter_, outOfRangeBehavior_);
+    }
+    
+    virtual string const getName() { return controlName_; }
+    
+    virtual void setName(const string& name) {
+        if(name == "")
+            return;
+        std::stringstream ss;
+        
+        // Remove listener on previous name (if any)
+        if(midiConverter_ != 0 && controlName_ != "")
+            midiConverter_->removeControl(controlName_.c_str());
+        
+        ss << "/touchkeys/mapping/segment" << (int)keyboardSegment_.outputPort() << "/" << name;
+        controlName_ = ss.str();
+
+        // Add listener for new name
+        if(midiConverter_ != 0)
+            midiConverter_->addControl(controlName_.c_str(), 1, inputRangeMin_, inputRangeMax_, inputRangeCenter_, outOfRangeBehavior_);
+    }
+    
+    // Set which keys should have this mapping enable
+    virtual void setActiveNotes(unsigned int notes) {
+        activeNotes_ = notes;
+    }
+    
+    // ***** State Updaters *****
+    
+    // These are called by PianoKey whenever certain events occur that might
+    // merit the start and stop of a mapping. What is done with them depends on
+    // the particular factory subclass.
+    
+    // Touch becomes active on a key where it wasn't previously
+    virtual void touchBegan(int noteNumber, bool midiNoteIsOn, bool keyMotionActive,
+                    Node<KeyTouchFrame>* touchBuffer,
+                    Node<key_position>* positionBuffer,
+                    KeyPositionTracker* positionTracker)  {
+        ScopedLock sl(mappingsMutex_);
+        // Add a new mapping if one doesn't exist already
+        if(mappings_.count(noteNumber) == 0) {
+#ifdef DEBUG_TOUCHKEY_BASE_MAPPING_FACTORY
+            std::cout << "Note " << noteNumber << ": adding mapping (touch)\n";
+#endif
+            int moduloNoteNumber = noteNumber % 12;
+            if((activeNotes_ & (1 << moduloNoteNumber)) && !bypassed_)
+                addMapping(noteNumber, touchBuffer, positionBuffer, positionTracker);
+        }
+    }
+    
+    // Touch ends on a key where it wasn't previously
+    virtual void touchEnded(int noteNumber, bool midiNoteIsOn, bool keyMotionActive,
+                    Node<KeyTouchFrame>* touchBuffer,
+                    Node<key_position>* positionBuffer,
+                    KeyPositionTracker* positionTracker) {
+        ScopedLock sl(mappingsMutex_);
+        // If a mapping exists but the MIDI note is off, remove the mapping
+        if(mappings_.count(noteNumber) != 0 && !midiNoteIsOn) {
+#ifdef DEBUG_TOUCHKEY_BASE_MAPPING_FACTORY
+            std::cout << "Note " << noteNumber << ": removing mapping (touch)\n";
+#endif
+            if(mappings_[noteNumber]->requestFinish())
+                removeMapping(noteNumber);
+        }
+    }
+    
+    // MIDI note on for a key
+    virtual void midiNoteOn(int noteNumber, bool touchIsOn, bool keyMotionActive,
+                    Node<KeyTouchFrame>* touchBuffer,
+                    Node<key_position>* positionBuffer,
+                    KeyPositionTracker* positionTracker)  {
+        ScopedLock sl(mappingsMutex_);
+        // Add a new mapping if one doesn't exist already
+        if(mappings_.count(noteNumber) == 0) {
+#ifdef DEBUG_TOUCHKEY_BASE_MAPPING_FACTORY
+            std::cout << "Note " << noteNumber << ": adding mapping (MIDI)\n";
+#endif
+            int moduloNoteNumber = noteNumber % 12;
+            if((activeNotes_ & (1 << moduloNoteNumber)) && !bypassed_)
+                addMapping(noteNumber, touchBuffer, positionBuffer, positionTracker);
+        }
+    }
+
+    // MIDI note off for a key
+    virtual void midiNoteOff(int noteNumber, bool touchIsOn, bool keyMotionActive,
+                     Node<KeyTouchFrame>* touchBuffer,
+                     Node<key_position>* positionBuffer,
+                     KeyPositionTracker* positionTracker)  {
+        ScopedLock sl(mappingsMutex_);
+        // If a mapping exists but the touch is off, remove the mapping
+        if(mappings_.count(noteNumber) != 0 && !touchIsOn) {
+#ifdef DEBUG_TOUCHKEY_BASE_MAPPING_FACTORY
+            std::cout << "Note " << noteNumber << ": removing mapping (MIDI)\n";
+#endif
+            if(mappings_[noteNumber]->requestFinish())
+                removeMapping(noteNumber);
+        }
+    }
+    
+    // Subclasses of this one won't care about these two methods:
+    
+    // Key goes active from continuous key position
+    virtual void keyMotionActive(int noteNumber, bool midiNoteIsOn, bool touchIsOn,
+                         Node<KeyTouchFrame>* touchBuffer,
+                         Node<key_position>* positionBuffer,
+                         KeyPositionTracker* positionTracker) {}
+    // Key goes idle from continuous key position
+    virtual void keyMotionIdle(int noteNumber, bool midiNoteIsOn, bool touchIsOn,
+                       Node<KeyTouchFrame>* touchBuffer,
+                       Node<key_position>* positionBuffer,
+                       KeyPositionTracker* positionTracker) {}
+    
+    // But we do use this one to send out default values:
+    virtual void noteWillBegin(int noteNumber, int midiChannel, int midiVelocity) {
+        if(midiConverter_ == 0)
+            return;
+        midiConverter_->clearLastValues(midiChannel, true);
+        //midiConverter_->sendDefaultValue(midiChannel);
+    }
+
+    
+protected:
+    // ***** Protected Methods *****
+    
+    // This method should be set by the subclass to initialize the parameters of
+    // a new mapping.
+    virtual void initializeMappingParameters(int noteNumber, MappingType *mapping) {}
+    
+private:
+    // ***** Private Methods *****
+    
+    // Add a new mapping
+    void addMapping(int noteNumber,
+                    Node<KeyTouchFrame>* touchBuffer,
+                    Node<key_position>* positionBuffer,
+                    KeyPositionTracker* positionTracker)  {
+        // TODO: mutex
+        removeMapping(noteNumber);  // Free any mapping that's already present on this note
+        
+        MappingType *mapping = new MappingType(keyboard_, this, noteNumber, touchBuffer,
+                                               positionBuffer, positionTracker);
+
+        // Set parameters
+        mapping->setName(controlName_);
+        initializeMappingParameters(noteNumber, mapping);
+        
+        // Save the mapping
+        mappings_[noteNumber] = mapping;
+
+        // Finally, engage the new mapping
+        mapping->engage();
+    }
+    
+    void removeMapping(int noteNumber)  {
+        // TODO: mutex
+        if(mappings_.count(noteNumber) == 0)
+            return;
+        MappingType* mapping = mappings_[noteNumber];
+#ifdef NEW_MAPPING_SCHEDULER
+        mapping->disengage(true);
+        //keyboard_.mappingScheduler().unscheduleAndDelete(mapping);
+#else
+        mapping->disengage();
+        delete mapping;
+#endif
+        mappings_.erase(noteNumber);
+    }
+
+protected:
+    // State variables
+    MidiKeyboardSegment& keyboardSegment_;         // Segment of the keyboard that this mapping addresses
+    OscMidiConverter *midiConverter_;              // Object to convert OSC messages to MIDI
+    std::map<int, MappingType*> mappings_;         // Collection of active mappings
+    CriticalSection mappingsMutex_;                // Mutex protecting mappings from changes
+    
+    std::string controlName_;                           // Name of the mapping
+    float inputRangeMin_, inputRangeMax_;               // Input ranges
+    float inputRangeCenter_;      
+    int outOfRangeBehavior_;                            // What happens to out of range inputs
+
+    int midiControllerNumber_;                          // Which controller to use
+    bool bypassed_;                                     // Whether the mapping has been bypassed by UI
+    unsigned int activeNotes_;                          // Indication of which notes out of the 12 to use
+};
+
+#endif /* defined(__TouchKeys__TouchkeyBaseMappingFactory__) */
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/Mappings/Vibrato/TouchkeyVibratoMapping.cpp	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,602 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+
+  TouchkeyVibratoMapping.cpp: per-note mapping for the vibrato mapping class,
+  which creates vibrato through side-to-side motion of the finger on the
+  key surface.
+*/
+
+#include "TouchkeyVibratoMapping.h"
+#include "../MappingScheduler.h"
+#include <vector>
+#include <climits>
+#include <cmath>
+
+#undef DEBUG_TOUCHKEY_VIBRATO_MAPPING
+
+// Class constants
+const int TouchkeyVibratoMapping::kDefaultMIDIChannel = 0;
+const int TouchkeyVibratoMapping::kDefaultFilterBufferLength = 30;
+
+const float TouchkeyVibratoMapping::kDefaultVibratoThresholdX = 0.05;
+const float TouchkeyVibratoMapping::kDefaultVibratoRatioX = 0.3;
+const float TouchkeyVibratoMapping::kDefaultVibratoThresholdY = 0.02;
+const float TouchkeyVibratoMapping::kDefaultVibratoRatioY = 0.8;
+const timestamp_diff_type TouchkeyVibratoMapping::kDefaultVibratoTimeout = microseconds_to_timestamp(400000); // 0.4s
+const float TouchkeyVibratoMapping::kDefaultVibratoPrescaler = 2.0;
+const float TouchkeyVibratoMapping::kDefaultVibratoRangeSemitones = 1.25;
+
+const timestamp_diff_type TouchkeyVibratoMapping::kZeroCrossingMinimumTime = microseconds_to_timestamp(50000); // 50ms
+const timestamp_diff_type TouchkeyVibratoMapping::kMinimumOnsetTime = microseconds_to_timestamp(30000); // 30ms
+const timestamp_diff_type TouchkeyVibratoMapping::kMaximumOnsetTime = microseconds_to_timestamp(300000); // 300ms
+const timestamp_diff_type TouchkeyVibratoMapping::kMinimumReleaseTime = microseconds_to_timestamp(30000); // 30ms
+const timestamp_diff_type TouchkeyVibratoMapping::kMaximumReleaseTime = microseconds_to_timestamp(300000); // 300ms
+
+const float TouchkeyVibratoMapping::kWhiteKeySingleAxisThreshold = (7.0 / 19.0);
+
+// Main constructor takes references/pointers from objects which keep track
+// of touch location, continuous key position and the state detected from that
+// position. The PianoKeyboard object is strictly required as it gives access to
+// Scheduler and OSC methods. The others are optional since any given system may
+// contain only one of continuous key position or touch sensitivity
+
+TouchkeyVibratoMapping::TouchkeyVibratoMapping(PianoKeyboard &keyboard, MappingFactory *factory, int noteNumber, Node<KeyTouchFrame>* touchBuffer,
+                                               Node<key_position>* positionBuffer, KeyPositionTracker* positionTracker)
+: TouchkeyBaseMapping(keyboard, factory, noteNumber, touchBuffer, positionBuffer, positionTracker),
+vibratoState_(kStateInactive),
+rampBeginTime_(missing_value<timestamp_type>::missing()),
+rampScaleValue_(0),
+rampLength_(0),
+lastCalculatedRampValue_(0),
+onsetThresholdX_(kDefaultVibratoThresholdX), onsetThresholdY_(kDefaultVibratoThresholdY),
+onsetRatioX_(kDefaultVibratoRatioX), onsetRatioY_(kDefaultVibratoRatioY),
+onsetTimeout_(kDefaultVibratoTimeout),
+onsetLocationX_(missing_value<float>::missing()),
+onsetLocationY_(missing_value<float>::missing()),
+lastX_(missing_value<float>::missing()), lastY_(missing_value<float>::missing()),
+idOfCurrentTouch_(-1),
+lastTimestamp_(missing_value<timestamp_type>::missing()),
+lastProcessedIndex_(0),
+lastZeroCrossingTimestamp_(missing_value<timestamp_type>::missing()),
+lastZeroCrossingInterval_(0),
+lastSampleWasPositive_(false),
+foundFirstExtremum_(false),
+firstExtremumX_(0), firstExtremumY_(0),
+firstExtremumTimestamp_(missing_value<timestamp_diff_type>::missing()),
+lastExtremumTimestamp_(missing_value<timestamp_diff_type>::missing()),
+//vibratoType_(kDefaultVibratoType),
+vibratoPrescaler_(kDefaultVibratoPrescaler),
+vibratoRangeSemitones_(kDefaultVibratoRangeSemitones),
+lastPitchBendSemitones_(0),
+rawDistance_(kDefaultFilterBufferLength),
+filteredDistance_(kDefaultFilterBufferLength, rawDistance_)
+{
+    // Initialize the filter coefficients for filtered key velocity (used for vibrato detection)
+    std::vector<double> bCoeffs, aCoeffs;
+    designSecondOrderBandpass(bCoeffs, aCoeffs, 9.0, 0.707, 200.0);
+    std::vector<float> bCf(bCoeffs.begin(), bCoeffs.end()), aCf(aCoeffs.begin(), aCoeffs.end());
+    filteredDistance_.setCoefficients(bCf, aCf);
+    filteredDistance_.setAutoCalculate(true);
+    
+    //setOscController(&keyboard_);
+    resetDetectionState();
+}
+
+TouchkeyVibratoMapping::~TouchkeyVibratoMapping() {
+}
+
+// Turn off mapping of data. Remove our callback from the scheduler
+void TouchkeyVibratoMapping::disengage(bool shouldDelete) {
+    sendVibratoMessage(0.0);
+    TouchkeyBaseMapping::disengage(shouldDelete);
+}
+
+// Reset state back to defaults
+void TouchkeyVibratoMapping::reset() {
+    TouchkeyBaseMapping::reset();
+    sendVibratoMessage(0.0);
+    resetDetectionState();
+}
+
+// Resend all current parameters
+void TouchkeyVibratoMapping::resend() {
+    sendVibratoMessage(lastPitchBendSemitones_, true);
+}
+
+// Set the range of vibrato
+void TouchkeyVibratoMapping::setRange(float rangeSemitones) {
+    vibratoRangeSemitones_ = rangeSemitones;
+}
+
+// Set the vibrato prescaler
+void TouchkeyVibratoMapping::setPrescaler(float prescaler) {
+    vibratoPrescaler_ = prescaler;
+}
+
+// Set the vibrato detection thresholds
+void TouchkeyVibratoMapping::setThresholds(float thresholdX, float thresholdY, float ratioX, float ratioY) {
+    onsetThresholdX_ = thresholdX;
+    onsetThresholdY_ = thresholdY;
+    onsetRatioX_ = ratioX;
+    onsetRatioY_ = ratioY;
+}
+
+// Set the timeout for vibrato detection
+void TouchkeyVibratoMapping::setTimeout(timestamp_diff_type timeout) {
+    onsetTimeout_ = timeout;
+}
+
+// Trigger method. This receives updates from the TouchKey data or from state changes in
+// the continuous key position (KeyPositionTracker). It will potentially change the scheduled
+// behavior of future mapping calls, but the actual OSC messages should be transmitted in a different
+// thread.
+void TouchkeyVibratoMapping::triggerReceived(TriggerSource* who, timestamp_type timestamp) {
+    if(who == 0)
+        return;
+    
+    if(who == touchBuffer_) {
+        if(!touchBuffer_->empty()) {
+            // New touch data is available. Find the distance from the onset location.
+            KeyTouchFrame frame = touchBuffer_->latest();
+            lastTimestamp_ = timestamp;
+            
+            if(frame.count == 0) {
+                // No touches. Last values are "missing", and we're not tracking any
+                // particular touch ID
+                lastX_ = lastY_ = missing_value<float>::missing();
+                idOfCurrentTouch_ = -1;
+#ifdef DEBUG_TOUCHKEY_VIBRATO_MAPPING
+                std::cout << "Touch off\n";
+#endif
+            }
+            else {
+                // At least one touch. Check if we are already tracking an ID and, if so,
+                // use its coordinates. Otherwise grab the lowest current ID.
+                
+                bool foundCurrentTouch = false;
+                
+                if(idOfCurrentTouch_ >= 0) {
+                    for(int i = 0; i < frame.count; i++) {
+                        if(frame.ids[i] == idOfCurrentTouch_) {
+                            lastY_ = frame.locs[i];
+                            if(frame.locH < 0 || (keyIsWhite() && lastY_ > kWhiteKeySingleAxisThreshold))
+                                lastX_ = missing_value<float>::missing();
+                            else
+                                lastX_ = frame.locH;
+                            foundCurrentTouch = true;
+                            break;
+                        }
+                    }
+                }
+                
+                if(!foundCurrentTouch) {
+                    // Assign a new touch to be tracked
+                    int lowestRemainingId = INT_MAX;
+                    int lowestIndex = 0;
+                    
+                    for(int i = 0; i < frame.count; i++) {
+                        if(frame.ids[i] < lowestRemainingId) {
+                            lowestRemainingId = frame.ids[i];
+                            lowestIndex = i;
+                        }
+                    }
+                    
+                    idOfCurrentTouch_ = lowestRemainingId;
+                    lastY_ = frame.locs[lowestIndex];
+                    if(frame.locH < 0 || (keyIsWhite() && lastY_ > kWhiteKeySingleAxisThreshold))
+                        lastX_ = missing_value<float>::missing();
+                    else
+                        lastX_ = frame.locH;
+#ifdef DEBUG_TOUCHKEY_VIBRATO_MAPPING
+                    std::cout << "Previous touch stopped; now ID " << idOfCurrentTouch_ << " at (" << lastX_ << ", " << lastY_ << ")\n";
+#endif
+                }
+                
+                // Now we have an X and (maybe) a Y coordinate for the most recent touch.
+                // Check whether we have an initial location (if the note is active).
+                if(noteIsOn_) {
+                    //ScopedLock sl(distanceAccessMutex_);
+                    
+                    if(missing_value<float>::isMissing(onsetLocationY_) ||
+                       !foundCurrentTouch) {
+                        // Note is on but touch hasn't yet arrived --> this touch becomes
+                        // our onset location. Alternatively, the current touch is a different
+                        // ID from the previous one.
+                        onsetLocationY_ = lastY_;
+                        onsetLocationX_ = lastX_;
+                        
+                        // Clear buffer and start with 0 distance for this point
+                        clearBuffers();
+                        
+#ifdef DEBUG_TOUCHKEY_VIBRATO_MAPPING
+                        std::cout << "Starting at (" << onsetLocationX_ << ", " << onsetLocationY_ << ")\n";
+#endif
+                    }
+                    else {
+                        float distance = 0.0;
+                        
+                        // Note is on and a start location exists. Calculate distance between
+                        // start location and the current point.
+                        
+                        if(missing_value<float>::isMissing(onsetLocationX_) &&
+                           !missing_value<float>::isMissing(lastX_)) {
+                            // No X location indicated for onset but we have one now.
+                            // Update the onset X location.
+                            onsetLocationX_ = lastX_;
+#ifdef DEBUG_TOUCHKEY_VIBRATO_MAPPING
+                            std::cout << "Found first X location at " << onsetLocationX_ << std::endl;
+#endif
+                        }
+                        
+                        
+                        if(missing_value<float>::isMissing(lastX_) ||
+                           missing_value<float>::isMissing(onsetLocationX_)) {
+                            // If no X value is available on the current touch, calculate the distance
+                            // based on Y only. TODO: check whether we should do this by keeping the
+                            // last X value we recorded.
+                            
+                            //distance = fabsf(lastY_ - onsetLocationY_);
+                            distance = lastY_ - onsetLocationY_;
+                        }
+                        else {
+                            // Euclidean distance between points
+                            //distance = sqrtf((lastY_ - onsetLocationY_) * (lastY_ - onsetLocationY_) +
+                            //                 (lastX_ - onsetLocationX_) * (lastX_ - onsetLocationX_));
+                            distance = lastX_ - onsetLocationX_;
+                        }
+                        
+                        // Insert raw distance into the buffer. Bandpass filter calculates the next
+                        // sample automatically. The rest of the processing takes place in the dedicated
+                        // thread so as not to slow down commmunication with the hardware.
+                        rawDistance_.insert(distance, timestamp);
+                           
+                        // Move the current scheduled event up to the present time.
+                        // FIXME: this may be more inefficient than just doing everything in the current thread!
+#ifdef NEW_MAPPING_SCHEDULER
+                        keyboard_.mappingScheduler().scheduleNow(this);
+#else
+                        keyboard_.unscheduleEvent(this);
+                        keyboard_.scheduleEvent(this, mappingAction_, keyboard_.schedulerCurrentTimestamp());
+#endif
+                        
+                        //std::cout << "Raw distance " << distance << " filtered " << filteredDistance_.latest() << std::endl;
+                    }
+                }
+            }
+        }
+    }
+}
+
+// Mapping method. This actually does the real work of sending OSC data in response to the
+// latest information from the touch sensors or continuous key angle
+timestamp_type TouchkeyVibratoMapping::performMapping() {
+    //ScopedLock sl(distanceAccessMutex_);
+    
+    timestamp_type currentTimestamp = keyboard_.schedulerCurrentTimestamp();
+    bool newSamplePresent = false;
+
+    // Go through the filtered distance samples that are remaining to process.
+    if(lastProcessedIndex_ < filteredDistance_.beginIndex() + 1) {
+        // Fell off the beginning of the position buffer. Skip to the samples we have
+        // (shouldn't happen except in cases of exceptional system load, and not too
+        // consequential if it does happen).
+        lastProcessedIndex_ = filteredDistance_.beginIndex() + 1;
+    }
+    
+    while(lastProcessedIndex_ < filteredDistance_.endIndex()) {
+        float distance = filteredDistance_[lastProcessedIndex_];
+        timestamp_type timestamp = filteredDistance_.timestampAt(lastProcessedIndex_);
+        newSamplePresent = true;
+        
+        if((distance > 0 && !lastSampleWasPositive_) ||
+           (distance < 0 && lastSampleWasPositive_)) {
+              // Found a zero crossing: save it if we're active or have at least found the
+              // first extremum
+               if(!missing_value<timestamp_type>::isMissing(lastZeroCrossingTimestamp_) &&
+                  (timestamp - lastZeroCrossingTimestamp_ > kZeroCrossingMinimumTime)) {
+                   if(vibratoState_ == kStateActive || vibratoState_ == kStateSwitchingOn ||
+                      foundFirstExtremum_) {
+                       lastZeroCrossingInterval_ = timestamp - lastZeroCrossingTimestamp_;
+#ifdef DEBUG_TOUCHKEY_VIBRATO_MAPPING
+                       std::cout << "Zero crossing interval " << lastZeroCrossingInterval_ << std::endl;
+#endif
+                   }
+               }
+               lastZeroCrossingTimestamp_ = timestamp;
+        }
+        lastSampleWasPositive_ = (distance > 0);
+        
+        // If not currently engaged, check for the pattern of side-to-side motion that
+        // begins a vibrato gesture.
+        if(vibratoState_ == kStateInactive || vibratoState_ == kStateSwitchingOff) {
+            if(foundFirstExtremum_) {
+                // Already found first extremum. Look for second extremum in the opposite
+                // direction of the given ratio from the original.
+                if((firstExtremumX_ > 0 && distance < 0) ||
+                   (firstExtremumX_ < 0 && distance > 0)) {
+                    if(fabsf(distance) >= fabsf(firstExtremumX_) * onsetRatioX_) {
+#ifdef DEBUG_TOUCHKEY_VIBRATO_MAPPING
+                        std::cout << "Found second extremum at " << distance << ", TS " << timestamp << std::endl;
+#endif
+                        changeStateSwitchingOn(timestamp);
+                    }
+                }
+                else if(timestamp - lastExtremumTimestamp_ > onsetTimeout_) {
+#ifdef DEBUG_TOUCHKEY_VIBRATO_MAPPING
+                    std::cout << "Onset timeout at " << timestamp << endl;
+#endif
+                    resetDetectionState();
+                }
+            }
+            else {
+                if(fabsf(distance) >= onsetThresholdX_) {
+                    // TODO: differentiate X/Y here
+                    if(missing_value<float>::isMissing(firstExtremumX_) ||
+                       fabsf(distance) > fabsf(firstExtremumX_)) {
+                        firstExtremumX_ = distance;
+                        lastExtremumTimestamp_ = timestamp;
+#ifdef DEBUG_TOUCHKEY_VIBRATO_MAPPING
+                        std::cout << "First extremum candidate at " << firstExtremumX_ << ", TS " << lastExtremumTimestamp_ << std::endl;
+#endif
+                    }
+                }
+                else if(!missing_value<float>::isMissing(firstExtremumX_) &&
+                        fabsf(firstExtremumX_) > onsetThresholdX_) {
+                    // We must have found the first extremum since its maximum value is
+                    // above the threshold, and we must have moved away from it since we are
+                    // now below the threshold. Next step will be to look for extremum in
+                    // opposite direction. Save the timestamp of this location in case
+                    // another extremum is found later.
+                    firstExtremumTimestamp_ = lastExtremumTimestamp_;
+                    foundFirstExtremum_ = true;
+#ifdef DEBUG_TOUCHKEY_VIBRATO_MAPPING
+                    std::cout << "Found first extremum at " << firstExtremumX_ << ", TS " << lastExtremumTimestamp_ << std::endl;
+#endif
+                }
+            }
+        }
+        else {
+            // Currently engaged. Look for timeout, defined as the finger staying below the lower (ratio-adjusted) threshold.
+            if(fabsf(distance) >= onsetThresholdX_ * onsetRatioX_)
+                lastExtremumTimestamp_ = timestamp;
+            if(timestamp - lastExtremumTimestamp_ > onsetTimeout_) {
+#ifdef DEBUG_TOUCHKEY_VIBRATO_MAPPING
+                std::cout << "Vibrato timeout at " << timestamp << " (last was " << lastExtremumTimestamp_ << ")" << endl;
+#endif
+                changeStateSwitchingOff(timestamp);
+            }
+        }
+        
+        lastProcessedIndex_++;
+    }
+    
+    // Having processed every sample individually for detection, send a pitch bend message based on the most
+    // recent one (no sense in sending multiple pitch bend messages simultaneously).
+    if(newSamplePresent && vibratoState_ != kStateInactive) {
+        float distance = filteredDistance_.latest();
+        float scale = 1.0;
+        
+        if(vibratoState_ == kStateSwitchingOn) {
+            // Switching on state gradually scales vibrato depth from 0 to
+            // its final value over a specified switch-on time.
+            if(rampLength_ <= 0 || (currentTimestamp - rampBeginTime_ >= rampLength_)) {
+                scale = 1.0;
+                changeStateActive(currentTimestamp);
+#ifdef DEBUG_TOUCHKEY_VIBRATO_MAPPING
+                std::cout << "Vibrato switch on finished, going to Active\n";
+#endif
+            }
+            else {
+                lastCalculatedRampValue_ = rampScaleValue_ * (float)(currentTimestamp - rampBeginTime_)/(float)rampLength_;
+                scale = lastCalculatedRampValue_;
+                //std::cout << "Vibrato scale " << scale << ", TS " << currentTimestamp - rampBeginTime_ << std::endl;
+            }
+        }
+        else if(vibratoState_ == kStateSwitchingOff) {
+            // Switching off state gradually scales vibrato depth from full
+            // value to 0 over a specified switch-off time.
+            if(rampLength_ <= 0 || (currentTimestamp - rampBeginTime_ >= rampLength_)) {
+                scale = 0.0;
+                changeStateInactive(currentTimestamp);
+#ifdef DEBUG_TOUCHKEY_VIBRATO_MAPPING
+                std::cout << "Vibrato switch off finished, going to Inactive\n";
+#endif
+            }
+            else {
+                lastCalculatedRampValue_ = rampScaleValue_ * (1.0 - (float)(currentTimestamp - rampBeginTime_)/(float)rampLength_);
+                scale = lastCalculatedRampValue_;
+                //std::cout << "Vibrato scale " << scale << ", TS " << currentTimestamp - rampBeginTime_ << std::endl;
+            }
+        }
+        
+        // Calculate pitch bend based on current distance, with a non-linear scaling to accentuate
+        // smaller motions.
+        float pitchBendSemitones = vibratoRangeSemitones_ * tanhf(vibratoPrescaler_ * scale * distance);
+        
+        sendVibratoMessage(pitchBendSemitones);
+        lastPitchBendSemitones_ = pitchBendSemitones;
+    }
+    
+    // We may have arrived here without a new touch, just based on timing. Check for timeouts and process
+    // any release in progress.
+    if(!newSamplePresent) {
+        if(vibratoState_ == kStateSwitchingOff) {
+            // No new information in the distance buffer, but we do need to gradually reduce the pitch bend to zero
+            if(rampLength_ <= 0 || (currentTimestamp - rampBeginTime_ >= rampLength_)) {
+                sendVibratoMessage(0.0);
+                lastPitchBendSemitones_ = 0;
+                changeStateInactive(currentTimestamp);
+#ifdef DEBUG_TOUCHKEY_VIBRATO_MAPPING
+                std::cout << "Vibrato switch off finished, going to Inactive\n";
+#endif
+            }
+            else {
+                // Still in the middle of the ramp. Calculate its current value based on the last one
+                // that actually had a touch data point (lastPitchBendSemitones_).
+                
+                lastCalculatedRampValue_ = rampScaleValue_ * (1.0 - (float)(currentTimestamp - rampBeginTime_)/(float)rampLength_);
+                float pitchBendSemitones = lastPitchBendSemitones_ * lastCalculatedRampValue_;
+                
+                sendVibratoMessage(pitchBendSemitones);
+            }
+        }
+        else if(vibratoState_ != kStateInactive) {
+            // Might still be active but with no data coming in. We need to look for a timeout here too.
+            if(currentTimestamp - lastExtremumTimestamp_ > onsetTimeout_) {
+#ifdef DEBUG_TOUCHKEY_VIBRATO_MAPPING
+                std::cout << "Vibrato timeout at " << currentTimestamp << " (2; last was " << lastExtremumTimestamp_ << ")" << endl;
+#endif
+                changeStateSwitchingOff(currentTimestamp);
+            }
+        }
+    }
+    
+    // Register for the next update by returning its timestamp
+    nextScheduledTimestamp_ = currentTimestamp + updateInterval_;
+    return nextScheduledTimestamp_;
+}
+
+// MIDI note-on message received
+void TouchkeyVibratoMapping::midiNoteOnReceived(int channel, int velocity) {
+    // MIDI note has gone on. Set the starting location to be most recent
+    // location. It's possible there has been no touch data before this,
+    // in which case lastX and lastY will hold missing values.
+    onsetLocationX_ = lastX_;
+    onsetLocationY_ = lastY_;
+    if(!missing_value<float>::isMissing(onsetLocationY_)) {
+        // Already have touch data. Clear the buffer here.
+        // Clear buffer and start with 0 distance for this point
+        clearBuffers();
+    
+#ifdef DEBUG_TOUCHKEY_VIBRATO_MAPPING
+        std::cout << "MIDI on: starting at (" << onsetLocationX_ << ", " << onsetLocationY_ << ")\n";
+#endif
+    }
+    else {
+#ifdef DEBUG_TOUCHKEY_VIBRATO_MAPPING
+        std::cout << "MIDI on but no touch\n";
+#endif
+    }
+}
+
+// MIDI note-off message received
+void TouchkeyVibratoMapping::midiNoteOffReceived(int channel) {
+    if(vibratoState_ == kStateActive || vibratoState_ == kStateSwitchingOn) {
+        changeStateSwitchingOff(keyboard_.schedulerCurrentTimestamp());
+    }
+}
+
+// Internal state-change methods, which keep the state variables in sync
+void TouchkeyVibratoMapping::changeStateSwitchingOn(timestamp_type timestamp) {
+    // Go to SwitchingOn state, which brings the vibrato value gradually up to full amplitude
+    
+    // TODO: need to start from a non-zero value if SwitchingOff
+    rampScaleValue_ = 1.0;
+    rampBeginTime_ = timestamp;
+    rampLength_ = 0.0;
+    // Interval between peak and zero crossing will be a quarter of a cycle.
+    // From this, figure out how much longer we have to go to get to the next
+    // peak if the rate remains the same.
+    if(!missing_value<timestamp_type>::isMissing(lastZeroCrossingTimestamp_) &&
+       !missing_value<timestamp_type>::isMissing(firstExtremumTimestamp_)) {
+        timestamp_type estimatedPeakTimestamp = lastZeroCrossingTimestamp_ + (lastZeroCrossingTimestamp_ - firstExtremumTimestamp_);
+        rampLength_ = estimatedPeakTimestamp - timestamp;
+        if(rampLength_ < kMinimumOnsetTime)
+            rampLength_ = kMinimumOnsetTime;
+        if(rampLength_ > kMaximumOnsetTime)
+            rampLength_ = kMaximumOnsetTime;
+#ifdef DEBUG_TOUCHKEY_VIBRATO_MAPPING
+        std::cout << "Switching on with ramp length " << rampLength_ << " (peak " << firstExtremumTimestamp_ << ", zero " << lastZeroCrossingTimestamp_ << ")" << std::endl;
+#endif
+    }
+    
+    vibratoState_ = kStateSwitchingOn;    
+}
+
+void TouchkeyVibratoMapping::changeStateSwitchingOff(timestamp_type timestamp) {
+    // Go to SwitchingOff state, which brings the vibrato value gradually down to 0
+    
+    if(vibratoState_ == kStateSwitchingOn) {
+        // Might already be in the midst of a ramp up. Start from its current value
+        rampScaleValue_ = lastCalculatedRampValue_;
+    }
+    else
+        rampScaleValue_ = 1.0;
+    
+    rampBeginTime_ = timestamp;
+    rampLength_ = lastZeroCrossingInterval_;
+    if(rampLength_ < kMinimumReleaseTime)
+        rampLength_ = kMinimumReleaseTime;
+    if(rampLength_ > kMaximumReleaseTime)
+        rampLength_ = kMaximumReleaseTime;
+    
+#ifdef DEBUG_TOUCHKEY_VIBRATO_MAPPING
+    std::cout << "Switching off with ramp length " << rampLength_ << std::endl;
+#endif
+    
+    resetDetectionState();
+    vibratoState_ = kStateSwitchingOff;
+}
+
+void TouchkeyVibratoMapping::changeStateActive(timestamp_type timestamp) {
+    vibratoState_ = kStateActive;
+}
+
+void TouchkeyVibratoMapping::changeStateInactive(timestamp_type timestamp) {
+    vibratoState_ = kStateInactive;
+}
+
+// Reset variables involved in detecting a vibrato gesture
+void TouchkeyVibratoMapping::resetDetectionState() {
+    foundFirstExtremum_ = false;
+    firstExtremumX_ = firstExtremumY_ = 0.0;
+    lastExtremumTimestamp_ = firstExtremumTimestamp_ = lastZeroCrossingTimestamp_ = missing_value<timestamp_type>::missing();
+}
+
+// Clear the buffers that hold distance measurements
+void TouchkeyVibratoMapping::clearBuffers() {
+    rawDistance_.clear();
+    filteredDistance_.clear();
+    rawDistance_.insert(0.0, lastTimestamp_);
+    lastProcessedIndex_ = 0;
+}
+
+bool TouchkeyVibratoMapping::keyIsWhite() {
+    int modNoteNumber = noteNumber_ % 12;
+    if(modNoteNumber == 1 ||
+       modNoteNumber == 3 ||
+       modNoteNumber == 6 ||
+       modNoteNumber == 8 ||
+       modNoteNumber == 10)
+        return false;
+    return true;
+}
+
+// Send the vibrato message of a given number of a semitones. Send by OSC,
+// which can be mapped to MIDI CC externally
+void TouchkeyVibratoMapping::sendVibratoMessage(float pitchBendSemitones, bool force) {
+    if(force || !suspended_) {
+        //if(vibratoType_ == kVibratoTypePitchBend)
+        //    keyboard_.sendMessage("/touchkeys/vibrato", "if", noteNumber_, pitchBendSemitones, LO_ARGS_END);
+        //else if(vibratoType_ == kVibratoTypeAmplitude)
+            keyboard_.sendMessage(controlName_.c_str(), "if", noteNumber_, pitchBendSemitones, LO_ARGS_END);
+        // Otherwise, if unknown type, ignore.
+    }
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/Mappings/Vibrato/TouchkeyVibratoMapping.h	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,184 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+
+  TouchkeyVibratoMapping.h: per-note mapping for the vibrato mapping class,
+  which creates vibrato through side-to-side motion of the finger on the
+  key surface.
+*/
+
+#ifndef __touchkeys__TouchkeyVibratoMapping__
+#define __touchkeys__TouchkeyVibratoMapping__
+
+
+#include <map>
+#include <boost/bind.hpp>
+#include "../../TouchKeys/KeyTouchFrame.h"
+#include "../../TouchKeys/KeyPositionTracker.h"
+#include "../../TouchKeys/PianoKeyboard.h"
+#include "../TouchkeyBaseMapping.h"
+#include "../../Utility/IIRFilter.h"
+
+// This class handles the detection and mapping of vibrato gestures
+// based on Touchkey data. It outputs MIDI or OSC messages that
+// can be used to affect the pitch of the active note.
+
+class TouchkeyVibratoMapping : public TouchkeyBaseMapping {
+    friend class TouchkeyVibratoMappingFactory;
+
+private:
+    // Useful constants for mapping MRP messages
+    /*constexpr static const int kDefaultMIDIChannel = 0;
+    constexpr static const int kDefaultFilterBufferLength = 30;
+    
+    constexpr static const float kDefaultVibratoThresholdX = 0.05;
+    constexpr static const float kDefaultVibratoRatioX = 0.3;
+    constexpr static const float kDefaultVibratoThresholdY = 0.02;
+    constexpr static const float kDefaultVibratoRatioY = 0.8;
+    constexpr static const timestamp_diff_type kDefaultVibratoTimeout = microseconds_to_timestamp(400000); // 0.4s
+    constexpr static const float kDefaultVibratoPrescaler = 2.0;
+    constexpr static const float kDefaultVibratoRangeSemitones = 1.25;
+    
+    constexpr static const timestamp_diff_type kZeroCrossingMinimumTime = microseconds_to_timestamp(50000); // 50ms
+    constexpr static const timestamp_diff_type kMinimumOnsetTime = microseconds_to_timestamp(30000); // 30ms
+    constexpr static const timestamp_diff_type kMaximumOnsetTime = microseconds_to_timestamp(300000); // 300ms
+    constexpr static const timestamp_diff_type kMinimumReleaseTime = microseconds_to_timestamp(30000); // 30ms
+    constexpr static const timestamp_diff_type kMaximumReleaseTime = microseconds_to_timestamp(300000); // 300ms*/
+    
+    static const int kDefaultMIDIChannel;
+    static const int kDefaultFilterBufferLength;
+    
+    static const float kDefaultVibratoThresholdX;
+    static const float kDefaultVibratoRatioX;
+    static const float kDefaultVibratoThresholdY;
+    static const float kDefaultVibratoRatioY;
+    static const timestamp_diff_type kDefaultVibratoTimeout;
+    static const float kDefaultVibratoPrescaler;
+    static const float kDefaultVibratoRangeSemitones;
+    
+    static const timestamp_diff_type kZeroCrossingMinimumTime;
+    static const timestamp_diff_type kMinimumOnsetTime;
+    static const timestamp_diff_type kMaximumOnsetTime;
+    static const timestamp_diff_type kMinimumReleaseTime;
+    static const timestamp_diff_type kMaximumReleaseTime;
+    
+    static const float kWhiteKeySingleAxisThreshold;
+    
+    enum {
+        kStateInactive = 0,
+        kStateSwitchingOn,
+        kStateActive,
+        kStateSwitchingOff
+    };
+    
+public:
+	// ***** Constructors *****
+	
+	// Default constructor, passing the buffer on which to trigger
+	TouchkeyVibratoMapping(PianoKeyboard &keyboard, MappingFactory *factory, int noteNumber, Node<KeyTouchFrame>* touchBuffer,
+               Node<key_position>* positionBuffer, KeyPositionTracker* positionTracker);
+
+    // ***** Destructor *****
+    
+    ~TouchkeyVibratoMapping();
+	
+    // ***** Modifiers *****
+    
+    // Disable mappings from being sent
+    void disengage(bool shouldDelete = false);
+	
+    // Reset the state back initial values
+	void reset();
+    
+    // Resend the current state of all parameters
+    void resend();
+
+    // Parameters for vibrato algorithm
+    //void setType(int vibratoType);
+    void setRange(float rangeSemitones);
+    void setPrescaler(float prescaler);
+    void setThresholds(float thresholdX, float thresholdY, float ratioX, float ratioY);
+    void setTimeout(timestamp_diff_type timeout);
+    
+	// ***** Evaluators *****
+    
+    // This method receives triggers whenever events occur in the touch data or the
+    // continuous key position (state changes only). It alters the behavior and scheduling
+    // of the mapping but does not itself send OSC messages
+	void triggerReceived(TriggerSource* who, timestamp_type timestamp);
+	
+    // This method handles the OSC message transmission. It should be run in the Scheduler
+    // thread provided by PianoKeyboard.
+    timestamp_type performMapping();
+    
+private:
+    // ***** Private Methods *****
+    void midiNoteOnReceived(int channel, int velocity);
+    void midiNoteOffReceived(int channel);
+    
+    void changeStateSwitchingOn(timestamp_type timestamp);
+    void changeStateActive(timestamp_type timestamp);
+    void changeStateSwitchingOff(timestamp_type timestamp);
+    void changeStateInactive(timestamp_type timestamp);
+
+    void resetDetectionState();
+    void clearBuffers();
+    
+    bool keyIsWhite();
+    
+    void sendVibratoMessage(float pitchBendSemitones, bool force = false);
+    
+	// ***** Member Variables *****
+    
+    int vibratoState_;                          // Whether a vibrato gesture is currently detected
+    
+    timestamp_type rampBeginTime_;              // If in a switching state, when does the transition begin?
+    float rampScaleValue_;                      // If in a switching state, what is the end point of the ramp?
+    timestamp_diff_type rampLength_;            // If in a switching state, how long is the transition?
+    float lastCalculatedRampValue_;             // Value of the ramp that was last calculated
+    
+    float onsetThresholdX_, onsetThresholdY_;   // Thresholds for detecting vibrato (first extremum)
+    float onsetRatioX_, onsetRatioY_;           // Thresholds for detection vibrato (second extremum)
+    timestamp_diff_type onsetTimeout_;          // Timeout between first and second extrema
+    
+    float onsetLocationX_, onsetLocationY_;     // Where the touch began at MIDI note on
+    float lastX_, lastY_;                       // Where the touch was at the last frame we received
+    int idOfCurrentTouch_;                      // Which touch ID we're currently following
+    timestamp_type lastTimestamp_;              // When the last data point arrived
+    Node<float>::size_type lastProcessedIndex_; // Index of the last filtered position sample we've handled
+    
+    timestamp_type lastZeroCrossingTimestamp_;  // Timestamp of the last zero crossing
+    timestamp_diff_type lastZeroCrossingInterval_;   // Interval between the last two zero-crossings of filtered distance
+    bool lastSampleWasPositive_;                // Whether the last sample was > 0
+    
+    bool foundFirstExtremum_;                   // Whether the first extremum has occurred
+    float firstExtremumX_, firstExtremumY_;     // Where the first extremum occurred
+    timestamp_type firstExtremumTimestamp_;     // Where the first extremum occurred
+    timestamp_type lastExtremumTimestamp_;      // When the most recent extremum occurred
+    
+    float vibratoPrescaler_;                    // Parameter controlling prescaler before nonlinear scaling
+    float vibratoRangeSemitones_;               // Amount of pitch bend in one direction at maximum
+    
+    float lastPitchBendSemitones_;              // The last pitch bend value we sent out
+    
+    Node<float> rawDistance_;                   // Distance from onset location
+    IIRFilterNode<float> filteredDistance_;     // Bandpass filtered finger motion
+    CriticalSection distanceAccessMutex_;       // Mutex that protects the access buffer from changes
+};
+
+#endif /* defined(__touchkeys__TouchkeyVibratoMapping__) */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/Mappings/Vibrato/TouchkeyVibratoMappingFactory.cpp	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,204 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+
+  TouchkeyVibratoMappingFactory.cpp: factory for the vibrato mapping class,
+  which creates vibrato through side-to-side motion of the finger on the
+  key surface.
+*/
+
+#include "TouchkeyVibratoMappingFactory.h"
+#include "TouchkeyVibratoMappingShortEditor.h"
+
+// Class constants
+const float TouchkeyVibratoMappingFactory::kDefaultPitchWheelRangeSemitones = 12.0;
+const int TouchkeyVibratoMappingFactory::kDefaultVibratoControl = MidiKeyboardSegment::kControlPitchWheel;
+
+// Default constructor, containing a reference to the PianoKeyboard class.
+
+TouchkeyVibratoMappingFactory::TouchkeyVibratoMappingFactory(PianoKeyboard &keyboard, MidiKeyboardSegment& segment) :
+TouchkeyBaseMappingFactory<TouchkeyVibratoMapping>(keyboard, segment), pitchWheelRangeSemitones_(kDefaultPitchWheelRangeSemitones),
+vibratoControl_(kDefaultVibratoControl),
+vibratoRange_(TouchkeyVibratoMapping::kDefaultVibratoRangeSemitones),
+vibratoPrescaler_(TouchkeyVibratoMapping::kDefaultVibratoPrescaler),
+vibratoTimeout_(TouchkeyVibratoMapping::kDefaultVibratoTimeout),
+vibratoOnsetThresholdX_(TouchkeyVibratoMapping::kDefaultVibratoThresholdX),
+vibratoOnsetThresholdY_(TouchkeyVibratoMapping::kDefaultVibratoThresholdY),
+vibratoOnsetRatioX_(TouchkeyVibratoMapping::kDefaultVibratoRatioX),
+vibratoOnsetRatioY_(TouchkeyVibratoMapping::kDefaultVibratoRatioY)
+{    
+    // Set up the MIDI converter to use pitch wheel
+    configurePitchWheelVibrato();
+}
+
+// ***** Destructor *****
+
+TouchkeyVibratoMappingFactory::~TouchkeyVibratoMappingFactory() {
+
+}
+
+// ***** Accessors / Modifiers *****
+
+void TouchkeyVibratoMappingFactory::setMIDIPitchWheelRange(float maxBendSemitones) {
+    if(maxBendSemitones <= 0)
+        return;
+    pitchWheelRangeSemitones_ = maxBendSemitones;
+    
+    if(vibratoControl_ != MidiKeyboardSegment::kControlPitchWheel)
+        return;
+    configurePitchWheelVibrato();
+}
+
+void TouchkeyVibratoMappingFactory::setName(const string& name) {
+    TouchkeyBaseMappingFactory<TouchkeyVibratoMapping>::setName(name);
+    setVibratoControl(vibratoControl_);
+}
+
+// ***** Vibrato Methods *****
+
+void TouchkeyVibratoMappingFactory::setVibratoControl(int vibratoControl) {
+    if(vibratoControl < 0 || vibratoControl >= MidiKeyboardSegment::kControlMax)
+        return;
+    
+    // Update the variable which affects future mappings
+    vibratoControl_ = vibratoControl;
+    
+    if(vibratoControl_ == MidiKeyboardSegment::kControlPitchWheel)
+        configurePitchWheelVibrato();
+    else
+        configureControlChangeVibrato();
+}
+
+void TouchkeyVibratoMappingFactory::setVibratoRange(float range, bool updateCurrent) {
+    /*if(updateCurrent) {
+        // Send new range to all active mappings
+        // TODO: mutex protect
+        std::map<int, mapping_pair>::iterator it = mappings_.begin();
+        while(it != mappings_.end()) {
+            // Tell this mapping to update its range
+            TouchkeyVibratoMapping *mapping = it->second.first;
+            mapping->setRange(rangeSemitones);
+            it++;
+        }
+    }*/
+    
+    // Update the variable which affects future mappings
+    vibratoRange_ = range;
+    if(vibratoRange_ < 0.01)
+        vibratoRange_ = 0.01;
+    if(vibratoRange_ > 127.0)
+        vibratoRange_ = 127.0;
+}
+
+void TouchkeyVibratoMappingFactory::setVibratoPrescaler(float prescaler, bool updateCurrent) {
+    /*if(updateCurrent) {
+        // Send new range to all active mappings
+        // TODO: mutex protect
+        std::map<int, mapping_pair>::iterator it = mappings_.begin();
+        while(it != mappings_.end()) {
+            // Tell this mapping to update its range
+            TouchkeyVibratoMapping *mapping = it->second.first;
+            mapping->setPrescaler(prescaler);
+            it++;
+        }
+    }*/
+    
+    // Update the variable which affects future mappings
+    vibratoPrescaler_ = prescaler;
+}
+
+void TouchkeyVibratoMappingFactory::setVibratoThreshold(float threshold, bool updateCurrent) {
+    vibratoOnsetThresholdX_ = threshold;
+    if(vibratoOnsetThresholdX_ < 0)
+        vibratoOnsetThresholdX_ = 0;
+    if(vibratoOnsetThresholdX_ > 1.0)
+        vibratoOnsetThresholdX_ = 1.0;
+}
+
+void TouchkeyVibratoMappingFactory::setVibratoThresholds(float thresholdX, float thresholdY, float ratioX, float ratioY, bool updateCurrent) {
+    /*if(updateCurrent) {
+        // Send new range to all active mappings
+        // TODO: mutex protect
+        std::map<int, mapping_pair>::iterator it = mappings_.begin();
+        while(it != mappings_.end()) {
+            // Tell this mapping to update its range
+            TouchkeyVibratoMapping *mapping = it->second.first;
+            mapping->setThresholds(thresholdX, thresholdY, ratioX, ratioY);
+            it++;
+        }
+    }*/
+    
+    // Update the variables which affect future mappings
+    vibratoOnsetThresholdX_ = thresholdX;
+    vibratoOnsetThresholdY_ = thresholdY;
+    vibratoOnsetRatioX_ = ratioX;
+    vibratoOnsetRatioY_ = ratioY;
+}
+
+void TouchkeyVibratoMappingFactory::setVibratoTimeout(timestamp_diff_type timeout, bool updateCurrent) {
+    /*if(updateCurrent) {
+        // Send new range to all active mappings
+        // TODO: mutex protect
+        std::map<int, mapping_pair>::iterator it = mappings_.begin();
+        while(it != mappings_.end()) {
+            // Tell this mapping to update its range
+            TouchkeyVibratoMapping *mapping = it->second.first;
+            mapping->setTimeout(timeout);
+            it++;
+        }
+    }*/
+    
+    // Update the variable which affects future mappings
+    vibratoTimeout_ = timeout;
+}
+
+// ***** GUI Support *****
+MappingEditorComponent* TouchkeyVibratoMappingFactory::createBasicEditor() {
+    return new TouchkeyVibratoMappingShortEditor(*this);
+}
+
+// ***** Private Methods *****
+
+// Set the initial parameters for a new mapping
+void TouchkeyVibratoMappingFactory::initializeMappingParameters(int noteNumber, TouchkeyVibratoMapping *mapping) {
+    mapping->setRange(vibratoRange_);
+    mapping->setPrescaler(vibratoPrescaler_);
+    mapping->setThresholds(vibratoOnsetThresholdX_, vibratoOnsetThresholdY_, vibratoOnsetRatioX_, vibratoOnsetRatioY_);
+    mapping->setTimeout(vibratoTimeout_);
+}
+
+// Configure the OSC-MIDI converter to handle pitchwheel vibrato
+void TouchkeyVibratoMappingFactory::configurePitchWheelVibrato() {
+    setMidiParameters(MidiKeyboardSegment::kControlPitchWheel, -pitchWheelRangeSemitones_, pitchWheelRangeSemitones_, 0.0);
+    
+    if(midiConverter_ != 0) {
+        midiConverter_->setMidiPitchWheelRange(pitchWheelRangeSemitones_);
+        midiConverter_->listenToIncomingControl(MidiKeyboardSegment::kControlPitchWheel);
+    }
+}
+
+// Configure the OSC-MIDI converter to handle vibrato based on a CC
+void TouchkeyVibratoMappingFactory::configureControlChangeVibrato() {
+    setMidiParameters(vibratoControl_, 0.0, 127.0, 0.0, 0, 0, 127, 0, false, OscMidiConverter::kOutOfRangeExtrapolate);
+    
+    if(midiConverter_ != 0) {
+        midiConverter_->listenToIncomingControl(vibratoControl_);
+    }
+}
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/Mappings/Vibrato/TouchkeyVibratoMappingFactory.h	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,101 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+
+  TouchkeyVibratoMappingFactory.h: factory for the vibrato mapping class,
+  which creates vibrato through side-to-side motion of the finger on the
+  key surface.
+*/
+
+#ifndef __touchkeys__TouchkeyVibratoMappingFactory__
+#define __touchkeys__TouchkeyVibratoMappingFactory__
+
+#include <iostream>
+
+#include "../TouchkeyBaseMappingFactory.h"
+#include "TouchkeyVibratoMapping.h"
+
+// Factory class to produce Touchkey vibrato (pitch-bend) mappings
+// This class keeps track of all the active mappings and responds
+// whenever touches or notes begin or end
+
+class TouchkeyVibratoMappingFactory : public TouchkeyBaseMappingFactory<TouchkeyVibratoMapping> {
+private:
+    static const float kDefaultPitchWheelRangeSemitones;
+    static const int kDefaultVibratoControl;
+
+public:
+    
+    // ***** Constructor *****
+    
+	// Default constructor, containing a reference to the PianoKeyboard class.
+    TouchkeyVibratoMappingFactory(PianoKeyboard &keyboard, MidiKeyboardSegment& segment);
+	
+    // ***** Destructor *****
+    
+    ~TouchkeyVibratoMappingFactory();
+    
+    // ***** Accessors / Modifiers *****
+    
+    virtual const std::string factoryTypeName() { return "Vibrato"; }
+    
+    void setMIDIPitchWheelRange(float maxBendSemitones); // Set the range for the MIDI pitch wheel
+    
+    void setName(const string& name);
+    
+    // ***** Vibrato-Specific Methods *****
+    
+    int getVibratoControl() { return vibratoControl_; }
+    float getVibratoRange() { return vibratoRange_; }
+    float getVibratoPrescaler() { return vibratoPrescaler_; }
+    float getVibratoThreshold() { return vibratoOnsetThresholdX_; }
+    float getVibratoRatio() { return vibratoOnsetRatioX_; }
+    timestamp_diff_type getVibratoTimeout() { return vibratoTimeout_; }
+    
+    void setVibratoControl(int vibratoControl);
+    void setVibratoRange(float range, bool updateCurrent = false);
+    void setVibratoPrescaler(float prescaler, bool updateCurrent = false);
+    void setVibratoThreshold(float threshold, bool updateCurrent = false);
+    void setVibratoThresholds(float thresholdX, float thresholdY, float ratioX, float ratioY, bool updateCurrent = false);
+    void setVibratoTimeout(timestamp_diff_type timeout, bool updateCurrent = false);
+    
+    // ***** GUI Support *****
+    bool hasBasicEditor() { return true; }
+    MappingEditorComponent* createBasicEditor();
+    bool hasExtendedEditor() { return false; }
+    MappingEditorComponent* createExtendedEditor() { return nullptr; }
+    
+private:
+    // ***** Private Methods *****
+    void initializeMappingParameters(int noteNumber, TouchkeyVibratoMapping *mapping);
+    
+    void configurePitchWheelVibrato();
+    void configureControlChangeVibrato();
+    
+    float pitchWheelRangeSemitones_;                    // Range of the MIDI pitch wheel (different than vibrato range)
+    int vibratoControl_;                                // Controller to use with vibrato
+    float vibratoRange_;                                // Range that the vibrato should use, in semitones or CC values
+    float vibratoPrescaler_;                            // Prescaler value to use before nonlinear vibrato mapping
+    float vibratoTimeout_;                              // Timeout for vibrato detection
+    float vibratoOnsetThresholdX_;                      // Thresholds for detection
+    float vibratoOnsetThresholdY_;
+    float vibratoOnsetRatioX_;
+    float vibratoOnsetRatioY_;
+};
+
+#endif /* defined(__touchkeys__TouchkeyVibratoMappingFactory__) */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/Mappings/Vibrato/TouchkeyVibratoMappingShortEditor.cpp	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,258 @@
+/*
+  ==============================================================================
+
+  This is an automatically generated GUI class created by the Introjucer!
+
+  Be careful when adding custom code to these files, as only the code within
+  the "//[xyz]" and "//[/xyz]" sections will be retained when the file is loaded
+  and re-saved.
+
+  Created with Introjucer version: 3.1.0
+
+  ------------------------------------------------------------------------------
+
+  The Introjucer is part of the JUCE library - "Jules' Utility Class Extensions"
+  Copyright 2004-13 by Raw Material Software Ltd.
+
+  ==============================================================================
+*/
+
+//[Headers] You can add your own extra header files here...
+//[/Headers]
+
+#include "TouchkeyVibratoMappingShortEditor.h"
+
+
+//[MiscUserDefs] You can add your own user definitions and misc code here...
+//[/MiscUserDefs]
+
+//==============================================================================
+TouchkeyVibratoMappingShortEditor::TouchkeyVibratoMappingShortEditor (TouchkeyVibratoMappingFactory& factory)
+    : factory_(factory)
+{
+    addAndMakeVisible (rangeEditor = new TextEditor ("range text editor"));
+    rangeEditor->setMultiLine (false);
+    rangeEditor->setReturnKeyStartsNewLine (false);
+    rangeEditor->setReadOnly (false);
+    rangeEditor->setScrollbarsShown (true);
+    rangeEditor->setCaretVisible (true);
+    rangeEditor->setPopupMenuEnabled (true);
+    rangeEditor->setText (String::empty);
+
+    addAndMakeVisible (rangeLabel = new Label ("range label",
+                                               "Range:"));
+    rangeLabel->setFont (Font (15.00f, Font::plain));
+    rangeLabel->setJustificationType (Justification::centredLeft);
+    rangeLabel->setEditable (false, false, false);
+    rangeLabel->setColour (TextEditor::textColourId, Colours::black);
+    rangeLabel->setColour (TextEditor::backgroundColourId, Colour (0x00000000));
+
+    addAndMakeVisible (thresholdEditor = new TextEditor ("threshold text editor"));
+    thresholdEditor->setMultiLine (false);
+    thresholdEditor->setReturnKeyStartsNewLine (false);
+    thresholdEditor->setReadOnly (false);
+    thresholdEditor->setScrollbarsShown (true);
+    thresholdEditor->setCaretVisible (true);
+    thresholdEditor->setPopupMenuEnabled (true);
+    thresholdEditor->setText (String::empty);
+
+    addAndMakeVisible (thresholdLabel = new Label ("threshold label",
+                                                   "Threshold:"));
+    thresholdLabel->setFont (Font (15.00f, Font::plain));
+    thresholdLabel->setJustificationType (Justification::centredLeft);
+    thresholdLabel->setEditable (false, false, false);
+    thresholdLabel->setColour (TextEditor::textColourId, Colours::black);
+    thresholdLabel->setColour (TextEditor::backgroundColourId, Colour (0x00000000));
+
+    addAndMakeVisible (controlLabel = new Label ("control label",
+                                                 "Control:"));
+    controlLabel->setFont (Font (15.00f, Font::plain));
+    controlLabel->setJustificationType (Justification::centredLeft);
+    controlLabel->setEditable (false, false, false);
+    controlLabel->setColour (TextEditor::textColourId, Colours::black);
+    controlLabel->setColour (TextEditor::backgroundColourId, Colour (0x00000000));
+
+    addAndMakeVisible (controlComboBox = new ComboBox ("control combo box"));
+    controlComboBox->setEditableText (false);
+    controlComboBox->setJustificationType (Justification::centredLeft);
+    controlComboBox->setTextWhenNothingSelected (String::empty);
+    controlComboBox->setTextWhenNoChoicesAvailable ("(no choices)");
+    controlComboBox->addListener (this);
+
+
+    //[UserPreSize]
+    // Populate controllers field
+    controlComboBox->addItem("Pitch Wheel", MidiKeyboardSegment::kControlPitchWheel);
+    for(int i = 1; i <= 120; i++) {
+        controlComboBox->addItem(String(i), i);
+    }
+    //[/UserPreSize]
+
+    setSize (328, 71);
+
+
+    //[Constructor] You can add your own custom stuff here..
+    rangeEditor->addListener(this);
+    thresholdEditor->addListener(this);
+    //[/Constructor]
+}
+
+TouchkeyVibratoMappingShortEditor::~TouchkeyVibratoMappingShortEditor()
+{
+    //[Destructor_pre]. You can add your own custom destruction code here..
+    //[/Destructor_pre]
+
+    rangeEditor = nullptr;
+    rangeLabel = nullptr;
+    thresholdEditor = nullptr;
+    thresholdLabel = nullptr;
+    controlLabel = nullptr;
+    controlComboBox = nullptr;
+
+
+    //[Destructor]. You can add your own custom destruction code here..
+    //[/Destructor]
+}
+
+//==============================================================================
+void TouchkeyVibratoMappingShortEditor::paint (Graphics& g)
+{
+    //[UserPrePaint] Add your own custom painting code here..
+    //[/UserPrePaint]
+
+    g.fillAll (Colours::white);
+
+    //[UserPaint] Add your own custom painting code here..
+    //[/UserPaint]
+}
+
+void TouchkeyVibratoMappingShortEditor::resized()
+{
+    rangeEditor->setBounds (64, 8, 88, 24);
+    rangeLabel->setBounds (8, 8, 56, 24);
+    thresholdEditor->setBounds (232, 8, 88, 24);
+    thresholdLabel->setBounds (160, 8, 72, 24);
+    controlLabel->setBounds (8, 40, 56, 24);
+    controlComboBox->setBounds (64, 40, 88, 24);
+    //[UserResized] Add your own custom resize handling here..
+    //[/UserResized]
+}
+
+void TouchkeyVibratoMappingShortEditor::comboBoxChanged (ComboBox* comboBoxThatHasChanged)
+{
+    //[UsercomboBoxChanged_Pre]
+    //[/UsercomboBoxChanged_Pre]
+
+    if (comboBoxThatHasChanged == controlComboBox)
+    {
+        //[UserComboBoxCode_controlComboBox] -- add your combo box handling code here..
+        int control = controlComboBox->getSelectedId();
+        factory_.setVibratoControl(control);
+        //[/UserComboBoxCode_controlComboBox]
+    }
+
+    //[UsercomboBoxChanged_Post]
+    //[/UsercomboBoxChanged_Post]
+}
+
+
+
+//[MiscUserCode] You can add your own definitions of your custom methods or any other code here...
+
+void TouchkeyVibratoMappingShortEditor::textEditorReturnKeyPressed(TextEditor &editor)
+{
+    if(&editor == rangeEditor) {
+        float range = atof(rangeEditor->getText().toUTF8());
+        factory_.setVibratoRange(range);
+    }
+    else if(&editor == thresholdEditor) {
+        float threshold = atof(thresholdEditor->getText().toUTF8());
+        factory_.setVibratoThreshold(threshold);
+    }
+}
+
+void TouchkeyVibratoMappingShortEditor::textEditorEscapeKeyPressed(TextEditor &editor)
+{
+
+}
+
+void TouchkeyVibratoMappingShortEditor::textEditorFocusLost(TextEditor &editor)
+{
+    textEditorReturnKeyPressed(editor);
+}
+
+void TouchkeyVibratoMappingShortEditor::synchronize()
+{
+    // Update the editors to reflect the current status
+    if(!rangeEditor->hasKeyboardFocus(true)) {
+        float value = factory_.getVibratoRange();
+        char st[16];
+        snprintf(st, 16, "%.2f", value);
+
+        rangeEditor->setText(st);
+    }
+
+    if(!thresholdEditor->hasKeyboardFocus(true)) {
+        float value = factory_.getVibratoThreshold();
+        char st[16];
+        snprintf(st, 16, "%.2f", value);
+
+        thresholdEditor->setText(st);
+    }
+
+    controlComboBox->setSelectedId(factory_.getVibratoControl(), dontSendNotification);
+}
+//[/MiscUserCode]
+
+
+//==============================================================================
+#if 0
+/*  -- Introjucer information section --
+
+    This is where the Introjucer stores the metadata that describe this GUI layout, so
+    make changes in here at your peril!
+
+BEGIN_JUCER_METADATA
+
+<JUCER_COMPONENT documentType="Component" className="TouchkeyVibratoMappingShortEditor"
+                 componentName="" parentClasses="public MappingEditorComponent, public TextEditor::Listener"
+                 constructorParams="TouchkeyVibratoMappingFactory&amp; factory"
+                 variableInitialisers="factory_(factory)" snapPixels="8" snapActive="1"
+                 snapShown="1" overlayOpacity="0.330000013" fixedSize="1" initialWidth="328"
+                 initialHeight="71">
+  <BACKGROUND backgroundColour="ffffffff"/>
+  <TEXTEDITOR name="range text editor" id="db0f62c03a58af03" memberName="rangeEditor"
+              virtualName="" explicitFocusOrder="0" pos="64 8 88 24" initialText=""
+              multiline="0" retKeyStartsLine="0" readonly="0" scrollbars="1"
+              caret="1" popupmenu="1"/>
+  <LABEL name="range label" id="1ca2d422f4c37b7f" memberName="rangeLabel"
+         virtualName="" explicitFocusOrder="0" pos="8 8 56 24" edTextCol="ff000000"
+         edBkgCol="0" labelText="Range:" editableSingleClick="0" editableDoubleClick="0"
+         focusDiscardsChanges="0" fontname="Default font" fontsize="15"
+         bold="0" italic="0" justification="33"/>
+  <TEXTEDITOR name="threshold text editor" id="854a054d84eaf552" memberName="thresholdEditor"
+              virtualName="" explicitFocusOrder="0" pos="232 8 88 24" initialText=""
+              multiline="0" retKeyStartsLine="0" readonly="0" scrollbars="1"
+              caret="1" popupmenu="1"/>
+  <LABEL name="threshold label" id="864de4f55b5481ee" memberName="thresholdLabel"
+         virtualName="" explicitFocusOrder="0" pos="160 8 72 24" edTextCol="ff000000"
+         edBkgCol="0" labelText="Threshold:" editableSingleClick="0" editableDoubleClick="0"
+         focusDiscardsChanges="0" fontname="Default font" fontsize="15"
+         bold="0" italic="0" justification="33"/>
+  <LABEL name="control label" id="f953b12999632418" memberName="controlLabel"
+         virtualName="" explicitFocusOrder="0" pos="8 40 56 24" edTextCol="ff000000"
+         edBkgCol="0" labelText="Control:" editableSingleClick="0" editableDoubleClick="0"
+         focusDiscardsChanges="0" fontname="Default font" fontsize="15"
+         bold="0" italic="0" justification="33"/>
+  <COMBOBOX name="control combo box" id="f1c84bb5fd2730fb" memberName="controlComboBox"
+            virtualName="" explicitFocusOrder="0" pos="64 40 88 24" editable="0"
+            layout="33" items="" textWhenNonSelected="" textWhenNoItems="(no choices)"/>
+</JUCER_COMPONENT>
+
+END_JUCER_METADATA
+*/
+#endif
+
+
+//[EndFile] You can add extra defines here...
+//[/EndFile]
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/Mappings/Vibrato/TouchkeyVibratoMappingShortEditor.h	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,85 @@
+/*
+  ==============================================================================
+
+  This is an automatically generated GUI class created by the Introjucer!
+
+  Be careful when adding custom code to these files, as only the code within
+  the "//[xyz]" and "//[/xyz]" sections will be retained when the file is loaded
+  and re-saved.
+
+  Created with Introjucer version: 3.1.0
+
+  ------------------------------------------------------------------------------
+
+  The Introjucer is part of the JUCE library - "Jules' Utility Class Extensions"
+  Copyright 2004-13 by Raw Material Software Ltd.
+
+  ==============================================================================
+*/
+
+#ifndef __JUCE_HEADER_15DE9AB413FD564C__
+#define __JUCE_HEADER_15DE9AB413FD564C__
+
+//[Headers]     -- You can add your own extra header files here --
+#include "JuceHeader.h"
+#include "TouchkeyVibratoMappingFactory.h"
+//[/Headers]
+
+
+
+//==============================================================================
+/**
+                                                                    //[Comments]
+    An auto-generated component, created by the Introjucer.
+
+    Describe your class and how it works here!
+                                                                    //[/Comments]
+*/
+class TouchkeyVibratoMappingShortEditor  : public MappingEditorComponent,
+                                           public TextEditor::Listener,
+                                           public ComboBoxListener
+{
+public:
+    //==============================================================================
+    TouchkeyVibratoMappingShortEditor (TouchkeyVibratoMappingFactory& factory);
+    ~TouchkeyVibratoMappingShortEditor();
+
+    //==============================================================================
+    //[UserMethods]     -- You can add your own custom methods in this section.
+    // TextEditor listener methods
+    void textEditorTextChanged(TextEditor &editor) {}
+    void textEditorReturnKeyPressed(TextEditor &editor);
+    void textEditorEscapeKeyPressed(TextEditor &editor);
+    void textEditorFocusLost(TextEditor &editor);
+
+    void synchronize();
+    //[/UserMethods]
+
+    void paint (Graphics& g);
+    void resized();
+    void comboBoxChanged (ComboBox* comboBoxThatHasChanged);
+
+
+
+private:
+    //[UserVariables]   -- You can add your own custom variables in this section.
+    TouchkeyVibratoMappingFactory& factory_;
+    //[/UserVariables]
+
+    //==============================================================================
+    ScopedPointer<TextEditor> rangeEditor;
+    ScopedPointer<Label> rangeLabel;
+    ScopedPointer<TextEditor> thresholdEditor;
+    ScopedPointer<Label> thresholdLabel;
+    ScopedPointer<Label> controlLabel;
+    ScopedPointer<ComboBox> controlComboBox;
+
+
+    //==============================================================================
+    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (TouchkeyVibratoMappingShortEditor)
+};
+
+//[EndFile] You can add extra defines here...
+//[/EndFile]
+
+#endif   // __JUCE_HEADER_15DE9AB413FD564C__
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/TouchKeys/KeyIdleDetector.cpp	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,180 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+ 
+  KeyIdleDetector.cpp: uses continuous key position to detect whether a key
+  is idle or active; active keys will have more detailed tracking applied
+  to their position, so detecting idle keys saves processing.
+*/
+
+#include "KeyIdleDetector.h"
+
+// Default constructor
+KeyIdleDetector::KeyIdleDetector(capacity_type capacity, Node<key_position>& keyBuffer, key_position positionThreshold, 
+								 key_position activityThreshold, int counterThreshold)
+: Node<int>(capacity), keyBuffer_(keyBuffer), accumulator_(kKeyIdleNumSamples+1, keyBuffer),
+  keyIdleThreshold_(kDefaultKeyIdleThreshold), activityThreshold_(activityThreshold), positionThreshold_(positionThreshold),
+  numberOfFramesWithoutActivity_(0), noActivityCounterThreshold_(counterThreshold),
+  idleState_(kIdleDetectorUnknown)
+{
+	// Register to receive messages from the accumulator each time it gets a new sample
+	  //std::cout << "Registering IdleDetector\n";
+	  
+	  registerForTrigger(&accumulator_);
+	  
+	//  std::cout << "IdleDetector: this_source = " << (TriggerSource*)this << " this_dest = " << (TriggerDestination*)this << " accumulator = " << &accumulator_ << std::endl;
+}
+
+// Copy constructor
+/*KeyIdleDetector::KeyIdleDetector(KeyIdleDetector const& obj)
+  : Node<int>(obj), keyBuffer_(obj.keyBuffer_), accumulator_(obj.accumulator_), idleState_(obj.idleState_), 
+    activityThreshold_(obj.activityThreshold_), positionThreshold_(obj.positionThreshold_),
+    numberOfFramesWithoutActivity_(obj.numberOfFramesWithoutActivity_),
+    keyIdleThreshold_(obj.keyIdleThreshold_), noActivityCounterThreshold_(obj.noActivityCounterThreshold_) {
+	registerForTrigger(&accumulator_);
+}*/
+
+// Clear current state and reset to unknown idle state.
+void KeyIdleDetector::clear() {
+	Node<int>::clear();
+	idleState_ = kIdleDetectorUnknown;
+	numberOfFramesWithoutActivity_ = 0;
+}
+
+// Evaluator function.  Find the maximum deviation from average of the key motion.
+
+void KeyIdleDetector::triggerReceived(TriggerSource* who, timestamp_type timestamp) {
+	//std::cout << "KeyIdleDetector::triggerReceived\n";
+
+	if(who != &accumulator_)
+		return;
+
+    key_position currentKeyPosition = keyBuffer_.latest();
+    std::pair<int, key_position> currentAccumulator = accumulator_.latest();
+    
+    // Check that we have enough samples
+    if(currentAccumulator.first < kKeyIdleNumSamples)
+        return;
+    
+    // Behavior depends on whether we were idle or not before (or in unknown state)
+    if(idleState_ == kIdleDetectorIdle) {
+        // If idle right now, don't do anything if the key position is below a threshold
+        if(currentKeyPosition < keyIdleThreshold_)
+            return;
+
+        // If average is below a second, slightly higher threshold, stay idle
+        key_position averageValue = currentAccumulator.second / (key_position)currentAccumulator.first;
+        if(averageValue < keyIdleThreshold_ * 2)
+            return;
+        
+        // Go active, notifying any listeners
+        idleState_ = kIdleDetectorActive;
+        insert(kIdleDetectorActive, timestamp);
+    }
+    else { // Active or unknown
+        // Rule out any cases that would immediately take the key active
+        key_position averageValue = currentAccumulator.second / (key_position)currentAccumulator.first;
+        if(averageValue >= keyIdleThreshold_ * 2) {
+            numberOfFramesWithoutActivity_ = 0;
+            return;
+        }
+        
+#if 0
+        key_position maxDeviation = 0;
+        size_type endIndex = keyBuffer_.endIndex();
+        // Find and return the maximum deviation from the average
+        for(int i = endIndex - kKeyIdleNumSamples; i < endIndex; i++) {
+            key_position diff = key_abs(keyBuffer_[i] - averageValue);
+            if(diff > maxDeviation)
+                maxDeviation = diff;
+        }
+#endif
+        key_position averageDeviation = 0;
+        size_type endIndex = keyBuffer_.endIndex();
+        // Find and return the average deviation from mean
+        for(int i = endIndex - kKeyIdleNumSamples; i < endIndex; i++) {
+            averageDeviation += key_abs(keyBuffer_[i] - averageValue);
+        }
+        averageDeviation /= kKeyIdleNumSamples;
+        
+        //std::cout << "averageDeviation = " << averageDeviation << " counter = " << numberOfFramesWithoutActivity_ << std::endl;
+        
+        if(averageDeviation < activityThreshold_) {
+            // Key registers as "flat".  Check if it has stayed that way for long enough, and with a position close enough
+            // to resting position, to change the state back to Idle.
+            
+            numberOfFramesWithoutActivity_++;
+            if(numberOfFramesWithoutActivity_ >= noActivityCounterThreshold_) {
+                idleState_ = kIdleDetectorIdle;
+                insert(kIdleDetectorIdle, timestamp);
+            }
+        }
+        else
+            numberOfFramesWithoutActivity_ = 0;
+    }
+
+#if 0 /* Old idle detection */
+	
+	//std::cout << "KeyIdleDetector::triggerReceived2\n";
+	
+	std::pair<int, key_position> current = accumulator_.latest();
+	
+	if(current.first < kKeyIdleNumSamples)
+		return;
+
+	// Find the average value
+	key_position averageValue = current.second / (key_position)current.first;
+	key_position maxDeviation = 0;
+	
+	size_type endIndex = keyBuffer_.endIndex();
+	// Find and return the maximum deviation from the average
+	for(int i = endIndex - kKeyIdleNumSamples; i < endIndex; i++) {
+		key_position diff = key_abs(keyBuffer_[i] - averageValue);
+		if(diff > maxDeviation)
+			maxDeviation = diff;
+	}
+	
+	// TODO: If we get here, good enough to go to initial activity.  But need to search back to determine start point.
+	// Also need to update current start values (see kblisten code).
+	
+	// Insert a new sample (and hence send a trigger) whenever the maximum deviation crosses the threshold.
+	
+	if((maxDeviation >= activityThreshold_ || keyBuffer_.latest() >= positionThreshold_) && idleState_ != kIdleDetectorActive) {
+		idleState_ = kIdleDetectorActive;
+		numberOfFramesWithoutActivity_ = 0;
+		insert(kIdleDetectorActive, timestamp);
+		//std::cout << "deviation = " << maxDeviation << " average = " << averageValue << std::endl;
+	}
+	else if(maxDeviation < activityThreshold_ && idleState_ != kIdleDetectorIdle) {
+		// Key registers as "flat".  Check if it has stayed that way for long enough, and with a position close enough
+		// to resting position, to change the state back to Idle.
+		
+		numberOfFramesWithoutActivity_++;
+		if(numberOfFramesWithoutActivity_ >= noActivityCounterThreshold_ && keyBuffer_.latest() < positionThreshold_) {
+			idleState_ = kIdleDetectorIdle;
+			insert(kIdleDetectorIdle, timestamp);
+			//std::cout << "deviation = " << maxDeviation << " average = " << averageValue << std::endl;
+			/*Accumulator<key_position,kKeyIdleNumSamples>::iterator it;
+			
+			for(it = accumulator_.begin(); it != accumulator_.end(); it++) {
+				std::cout << it->first << " " << it->second << std::endl;
+			}*/
+		}
+	}
+#endif
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/TouchKeys/KeyIdleDetector.h	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,107 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+ 
+  KeyIdleDetector.h: uses continuous key position to detect whether a key
+  is idle or active; active keys will have more detailed tracking applied
+  to their position, so detecting idle keys saves processing.
+*/
+
+
+#ifndef KEYCONTROL_KEYIDLEDETECTOR_H
+#define KEYCONTROL_KEYIDLEDETECTOR_H
+
+#include "../Utility/Node.h"
+#include "../Utility/Accumulator.h"
+//#include "Trigger.h"
+//#include "PianoKeyboard.h"
+#include "PianoTypes.h"
+
+#define kKeyIdleNumSamples 10
+#define kDefaultKeyIdleThreshold (scale_key_position(0.05))
+
+// Three states of idle detector
+enum {
+	kIdleDetectorIdle = 0,
+	kIdleDetectorActive = 1,
+	kIdleDetectorUnknown = 2
+};
+
+/*
+ * KeyIdleDetector
+ *
+ * A Filter that looks for whether the key position has been flat over time, or is changing.
+ * Uses this information to detect when a key has begun to move.
+ *
+ * This class contains a second Filter object, operating on the same data source, which is used
+ * to maintain a running sum of the last N values.  The running sum is converted to an average
+ * value, and the maximum deviation from the average is calculated.
+ *
+ */
+
+class KeyIdleDetector : public Node<int> {
+public:
+	// ***** Constructors *****
+	
+	// Default constructor, taking an input and thresholds (position and timing) at which to detect "not idle"
+	KeyIdleDetector(capacity_type capacity, Node<key_position>& keyBuffer, key_position positionThreshold, 
+					key_position activityThreshold, int counterThreshold);
+	
+	// Copy constructor
+	// KeyIdleDetector(KeyIdleDetector const& obj);
+	
+	// ***** State Access *****
+	
+	// Determine whether the key is currently idle or not.
+	int idleState() { return idleState_; }
+	
+	// Set the threshold at which a key is determined to be idle or not.
+	key_position activityThreshold() { return activityThreshold_; }
+	key_position positionThreshold() { return positionThreshold_; }
+	void setActivityThreshold(key_position thresh) { activityThreshold_ = thresh; }
+	void setPositionThreshold(key_position thresh) { positionThreshold_ = thresh; }
+	
+	// ***** Modifiers *****
+	
+	void clear();
+	
+	// ***** Evaluator *****
+	
+	// This method actually handles the quantification of key activity.  When it
+	// exceeds a preset threshold, it sends a trigger
+	
+	void triggerReceived(TriggerSource* who, timestamp_type timestamp);
+	
+	// ***** Member Variables *****
+	
+	Node<key_position>& keyBuffer_;								// Raw key position data	
+	Accumulator<key_position, kKeyIdleNumSamples> accumulator_;	// This class accumulates the last N key samples (to find an average)
+	
+    key_position keyIdleThreshold_;                             // Position below which we assume key is staying idle
+    
+	key_position activityThreshold_;							// How much key motion should take place to make key active
+	key_position positionThreshold_;							// Position below which key can return to idle
+	int numberOfFramesWithoutActivity_;                         // For how many samples have we been below the idle threshold?
+    int noActivityCounterThreshold_;
+	int idleState_;												// Currently idle?
+
+    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(KeyIdleDetector)
+};
+ 
+
+#endif /* KEYCONTROL_KEYIDLEDETECTOR_H */
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/TouchKeys/KeyPositionTracker.cpp	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,697 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+ 
+  KeyPositionTracker.cpp: parses continuous key position and detects the
+  state of the key.
+*/
+
+#include "KeyPositionTracker.h"
+
+// Default constructor
+KeyPositionTracker::KeyPositionTracker(capacity_type capacity, Node<key_position>& keyBuffer)
+: Node<KeyPositionTrackerNotification>(capacity), keyBuffer_(keyBuffer), engaged_(false) {
+    reset();
+}
+
+// Copy constructor
+/*KeyPositionTracker::KeyPositionTracker(KeyPositionTracker const& obj)
+: Node<int>(obj), keyBuffer_(obj.keyBuffer_), engaged_(obj.engaged_) {
+    if(engaged_)
+        registerForTrigger(&keyBuffer_);
+}*/
+
+// Calculate (MIDI-style) key press velocity from continuous key position
+std::pair<timestamp_type, key_velocity> KeyPositionTracker::pressVelocity() {
+    return pressVelocity(pressVelocityEscapementPosition_);
+}
+
+std::pair<timestamp_type, key_velocity> KeyPositionTracker::pressVelocity(key_position escapementPosition) {
+    // Check that we have a valid start point from which to calculate
+    if(missing_value<timestamp_type>::isMissing(startTimestamp_)) {
+        return std::pair<timestamp_type, key_velocity>(missing_value<timestamp_type>::missing(),
+                                                       missing_value<key_velocity>::missing());
+    }
+    
+    // Find where the key position crosses the indicated level
+    key_buffer_index index = startIndex_;
+    if(index < keyBuffer_.beginIndex() + 2)
+        index = keyBuffer_.beginIndex() + 2;
+
+    while(index < keyBuffer_.endIndex() - kPositionTrackerSamplesNeededForPressVelocityAfterEscapement) {
+        // If the key press has a defined end, make sure we don't go past it
+        if(pressIndex_ != 0 && index >= pressIndex_)
+            break;
+        
+        if(keyBuffer_[index] > escapementPosition) {
+            // Found the place the position crosses the indicated threshold
+            // Now find the exact (interpolated) timestamp and velocity
+            timestamp_type exactPressTimestamp = keyBuffer_.timestampAt(index); // TODO
+            
+            // Velocity is calculated by an average of 2 samples before and 1 after
+            key_position diffPosition = keyBuffer_[index + kPositionTrackerSamplesNeededForPressVelocityAfterEscapement] - keyBuffer_[index - 2];
+            timestamp_diff_type diffTimestamp = keyBuffer_.timestampAt(index + kPositionTrackerSamplesNeededForPressVelocityAfterEscapement) - keyBuffer_.timestampAt(index - 2);
+            key_velocity velocity = calculate_key_velocity(diffPosition, diffTimestamp);
+            
+            return std::pair<timestamp_type, key_velocity>(exactPressTimestamp, velocity);
+        }
+        index++;
+    }
+    
+    // Didn't find anything matching that threshold
+    return std::pair<timestamp_type, key_velocity>(missing_value<timestamp_type>::missing(),
+                                                   missing_value<key_velocity>::missing());
+}
+
+// Calculate (MIDI-style) key release velocity from continuous key position
+std::pair<timestamp_type, key_velocity> KeyPositionTracker::releaseVelocity() {
+    return releaseVelocity(releaseVelocityEscapementPosition_);
+}
+
+std::pair<timestamp_type, key_velocity> KeyPositionTracker::releaseVelocity(key_position returnPosition) {
+    // Check that we have a valid start point from which to calculate
+    if(missing_value<timestamp_type>::isMissing(releaseBeginTimestamp_)) {
+        return std::pair<timestamp_type, key_velocity>(missing_value<timestamp_type>::missing(),
+                                                       missing_value<key_velocity>::missing());
+    }
+    
+    // Find where the key position crosses the indicated level
+    key_buffer_index index = releaseBeginIndex_;
+    if(index < keyBuffer_.beginIndex() + 2)
+        index = keyBuffer_.beginIndex() + 2;
+
+    while(index < keyBuffer_.endIndex() - kPositionTrackerSamplesNeededForReleaseVelocityAfterEscapement) {
+        // Check for whether we've hit the end of the release interval, assuming
+        // the interval exists yet
+        if(releaseEndIndex_ != 0 && index >= releaseEndIndex_)
+            break;
+        
+        if(keyBuffer_[index] < returnPosition) {
+            // Found the place the position crosses the indicated threshold
+            // Now find the exact (interpolated) timestamp and velocity
+            timestamp_type exactPressTimestamp = keyBuffer_.timestampAt(index); // TODO
+            
+            // Velocity is calculated by an average of 2 samples before and 1 after
+            key_position diffPosition = keyBuffer_[index + kPositionTrackerSamplesNeededForReleaseVelocityAfterEscapement] - keyBuffer_[index - 2];
+            timestamp_diff_type diffTimestamp = keyBuffer_.timestampAt(index + kPositionTrackerSamplesNeededForReleaseVelocityAfterEscapement) - keyBuffer_.timestampAt(index - 2);
+            key_velocity velocity = calculate_key_velocity(diffPosition, diffTimestamp);
+            
+            std::cout << "found release velocity " << velocity << "(diffp " << diffPosition << ", diffT " << diffTimestamp << ")" << std::endl;
+            
+            return std::pair<timestamp_type, key_velocity>(exactPressTimestamp, velocity);
+        }
+        index++;
+    }
+    
+    // Didn't find anything matching that threshold
+    return std::pair<timestamp_type, key_velocity>(missing_value<timestamp_type>::missing(),
+                                                   missing_value<key_velocity>::missing());
+}
+
+// Calculate and return features about the percussiveness of the key press
+KeyPositionTracker::PercussivenessFeatures KeyPositionTracker::pressPercussiveness() {
+    PercussivenessFeatures features;
+    key_buffer_index index;
+    key_velocity maximumVelocity, largestVelocityDifference;
+    key_buffer_index maximumVelocityIndex, largestVelocityDifferenceIndex;
+    
+    // Check that we have a valid start point from which to calculate
+    if(missing_value<timestamp_type>::isMissing(startTimestamp_) || keyBuffer_.beginIndex() > startIndex_ - 1) {
+        std::cout << "*** no start time\n";
+        features.percussiveness = missing_value<float>::missing();
+        return features;
+    }
+    
+    // From the start of the key press, look for an initial maximum in velocity
+    index = startIndex_;
+    
+    maximumVelocity = scale_key_velocity(0);
+    maximumVelocityIndex = startIndex_;
+    largestVelocityDifference = scale_key_velocity(0);
+    largestVelocityDifferenceIndex = startIndex_;
+    
+    std::cout << "*** start index " << index << std::endl;
+    
+    while(index < keyBuffer_.endIndex()) {
+        if(pressIndex_ != 0 && index >= pressIndex_)
+            break;
+        
+        key_position diffPosition = keyBuffer_[index] - keyBuffer_[index - 1];
+        timestamp_diff_type diffTimestamp = keyBuffer_.timestampAt(index) - keyBuffer_.timestampAt(index - 1);
+        key_velocity velocity = calculate_key_velocity(diffPosition, diffTimestamp);
+        
+        // Look for maximum of velocity
+        if(velocity > maximumVelocity) {
+            maximumVelocity = velocity;
+            maximumVelocityIndex = index;
+            std::cout << "*** found new max velocity " << maximumVelocity << " at index " << index << std::endl;
+        }
+        
+        // And given the difference between the max and the current sample,
+        // look for the largest rebound (velocity hitting a peak and falling)
+        if(maximumVelocity - velocity > largestVelocityDifference) {
+            largestVelocityDifference = maximumVelocity - velocity;
+            largestVelocityDifferenceIndex = index;
+            std::cout << "*** found new diff velocity " << largestVelocityDifference << " at index " << index << std::endl;
+        }
+        
+        // Only look at the early part of the key press: if the key position
+        // makes it more than a certain amount down, assume the initial spike
+        // has passed and finish up. But always allow at least 5 points for the
+        // fastest key presses to be considered.
+        if(index - startIndex_ >= 4 && keyBuffer_[index] > kPositionTrackerPositionThresholdForPercussivenessCalculation) {
+            break;
+        }
+        
+        index++;
+    }
+    
+    // Now transfer what we've found to the data structure
+    features.velocitySpikeMaximum = Event(maximumVelocityIndex, maximumVelocity, keyBuffer_.timestampAt(maximumVelocityIndex));
+    features.velocitySpikeMinimum = Event(largestVelocityDifferenceIndex, maximumVelocity - largestVelocityDifference,
+                                          keyBuffer_.timestampAt(largestVelocityDifferenceIndex));
+    features.timeFromStartToSpike = keyBuffer_.timestampAt(maximumVelocityIndex) - keyBuffer_.timestampAt(startIndex_);
+    
+    // Check if we found a meaningful difference. If not, percussiveness is set to 0
+    if(largestVelocityDifference == scale_key_velocity(0)) {
+        features.percussiveness = 0.0;
+        features.areaPrecedingSpike = scale_key_velocity(0);
+        features.areaFollowingSpike = scale_key_velocity(0);
+        return features;
+    }
+    
+    // Calculate the area under the velocity curve before and after the maximum
+    features.areaPrecedingSpike = scale_key_velocity(0);
+    for(index = startIndex_; index < maximumVelocityIndex; index++) {
+        key_position diffPosition = keyBuffer_[index] - keyBuffer_[index - 1];
+        timestamp_diff_type diffTimestamp = keyBuffer_.timestampAt(index) - keyBuffer_.timestampAt(index - 1);
+        features.areaPrecedingSpike += calculate_key_velocity(diffPosition, diffTimestamp);
+    }
+    features.areaFollowingSpike = scale_key_velocity(0);
+    for(index = maximumVelocityIndex; index < largestVelocityDifferenceIndex; index++) {
+        key_position diffPosition = keyBuffer_[index] - keyBuffer_[index - 1];
+        timestamp_diff_type diffTimestamp = keyBuffer_.timestampAt(index) - keyBuffer_.timestampAt(index - 1);
+        features.areaFollowingSpike += calculate_key_velocity(diffPosition, diffTimestamp);
+    }
+    
+    std::cout << "area before = " << features.areaPrecedingSpike << " after = " << features.areaFollowingSpike << std::endl;
+    
+    features.percussiveness = features.velocitySpikeMaximum.position;
+    
+    return features;
+}
+
+// Register to receive messages from the key buffer on each new sample
+void KeyPositionTracker::engage() {
+    if(engaged_)
+        return;
+
+    registerForTrigger(&keyBuffer_);
+    engaged_ = true;
+}
+
+// Unregister from receiving message on new samples
+void KeyPositionTracker::disengage() {
+    if(!engaged_)
+        return;
+    
+    unregisterForTrigger(&keyBuffer_);
+    engaged_ = false;
+}
+
+// Clear current state and reset to unknown state
+void KeyPositionTracker::reset() {
+	Node<KeyPositionTrackerNotification>::clear();
+    
+    currentState_ = kPositionTrackerStateUnknown;
+    currentlyAvailableFeatures_ = KeyPositionTrackerNotification::kFeaturesNone;
+    currentMinIndex_ = currentMaxIndex_ = startIndex_ = pressIndex_ = 0;
+    releaseBeginIndex_ = releaseEndIndex_ = 0;
+    lastMinMaxPosition_ = startPosition_ = pressPosition_ = missing_value<key_position>::missing();
+    releaseBeginPosition_ = releaseEndPosition_ = missing_value<key_position>::missing();
+    currentMinPosition_ = currentMaxPosition_ = missing_value<key_position>::missing();
+    startTimestamp_ = pressTimestamp_ = missing_value<timestamp_type>::missing();
+    currentMinTimestamp_ = currentMaxTimestamp_ = missing_value<timestamp_type>::missing();
+    releaseBeginTimestamp_ = releaseEndTimestamp_ = missing_value<timestamp_type>::missing();
+    pressVelocityEscapementPosition_ = kPositionTrackerDefaultPositionForPressVelocityCalculation;
+    releaseVelocityEscapementPosition_ = kPositionTrackerDefaultPositionForReleaseVelocityCalculation;
+    pressVelocityAvailableIndex_ = releaseVelocityAvailableIndex_ = percussivenessAvailableIndex_ = 0;
+    releaseVelocityWaitingForThresholdCross_ = false;
+}
+
+// Evaluator function. Update the current state
+void KeyPositionTracker::triggerReceived(TriggerSource* who, timestamp_type timestamp) {
+
+	if(who != &keyBuffer_)
+		return;
+    
+    // Always start in the partial press state after a reset, retroactively locating
+    // the start position for this key press
+    if(empty()) {
+        findKeyPressStart(timestamp);
+        changeState(kPositionTrackerStatePartialPressAwaitingMax, timestamp);
+    }
+    
+    key_position currentKeyPosition = keyBuffer_.latest();
+    key_buffer_index currentBufferIndex = keyBuffer_.endIndex() - 1;
+    
+    // First, check queued actions to see if we can calculate a new feature
+    // ** Press Velocity **
+    if(pressVelocityAvailableIndex_ != 0) {
+        if(currentBufferIndex >= pressVelocityAvailableIndex_) {
+            // Can now calculate press velocity
+            currentlyAvailableFeatures_ |= KeyPositionTrackerNotification::kFeaturePressVelocity;
+            notifyFeature(KeyPositionTrackerNotification::kNotificationTypeFeatureAvailableVelocity, timestamp);
+            pressVelocityAvailableIndex_ = 0;
+        }
+    }
+    // ** Release Velocity **
+    if(releaseVelocityWaitingForThresholdCross_) {
+        if(currentKeyPosition < releaseVelocityEscapementPosition_)
+            prepareReleaseVelocityFeature(currentBufferIndex, timestamp);
+    }
+    else if(releaseVelocityAvailableIndex_ != 0) {
+        if(currentBufferIndex >= releaseVelocityAvailableIndex_) {
+            // Can now calculate release velocity
+            currentlyAvailableFeatures_ |= KeyPositionTrackerNotification::kFeatureReleaseVelocity;
+            notifyFeature(KeyPositionTrackerNotification::kNotificationTypeFeatureAvailableReleaseVelocity, timestamp);
+            releaseVelocityAvailableIndex_ = 0;
+        }
+    }
+    // ** Percussiveness **
+    if(percussivenessAvailableIndex_ != 0) {
+        if(currentBufferIndex >= percussivenessAvailableIndex_) {
+            // Can now calculate percussiveness
+            currentlyAvailableFeatures_ |= KeyPositionTrackerNotification::kFeaturePercussiveness;
+            notifyFeature(KeyPositionTrackerNotification::kNotificationTypeFeatureAvailablePercussiveness, timestamp);
+            percussivenessAvailableIndex_ = 0;
+        }
+    }
+    
+    // Major state transitions next, centered on whether the key is pressed
+    // fully or partially
+    if(currentState_ == kPositionTrackerStatePartialPressAwaitingMax ||
+       currentState_ == kPositionTrackerStatePartialPressFoundMax) {
+        // These are collectively the pre-press states
+        if(currentKeyPosition >= kPositionTrackerPressPosition + kPositionTrackerPressHysteresis) {
+            // Key has gone far enough down to be considered pressed, but hasn't necessarily
+            // made it down yet.
+            pressIndex_ = 0;
+            pressPosition_ = missing_value<key_position>::missing();
+            pressTimestamp_ = missing_value<timestamp_type>::missing();
+            
+            changeState(kPositionTrackerStatePressInProgress, timestamp);
+        }
+    }
+    else if(currentState_ == kPositionTrackerStateReleaseInProgress ||
+            currentState_ == kPositionTrackerStateReleaseFinished) {
+        if(currentKeyPosition >= kPositionTrackerPressPosition + kPositionTrackerPressHysteresis) {
+            // Key was releasing but is now back down. Need to reprime the start
+            // position information, which will be taken as the last minimum.
+            startIndex_ = currentMinIndex_;
+            startPosition_ = currentMinPosition_;
+            startTimestamp_ = currentMinTimestamp_;
+            pressIndex_ = 0;
+            pressPosition_ = missing_value<key_position>::missing();
+            pressTimestamp_ = missing_value<timestamp_type>::missing();
+            
+            changeState(kPositionTrackerStatePressInProgress, timestamp);
+        }
+    }
+    else if(currentState_ == kPositionTrackerStatePressInProgress) {
+        // Press has started, wait to find its max position before labeling the key as "down"
+        if(currentKeyPosition < kPositionTrackerPressPosition - kPositionTrackerPressHysteresis) {
+            // Key is on its way back up: find where release began
+            findKeyReleaseStart(timestamp);
+
+            changeState(kPositionTrackerStateReleaseInProgress, timestamp);
+        }
+    }
+    else if(currentState_ == kPositionTrackerStateDown) {
+        if(currentKeyPosition < kPositionTrackerPressPosition - kPositionTrackerPressHysteresis) {
+            // Key is on its way back up: find where release began
+            findKeyReleaseStart(timestamp);
+            
+            changeState(kPositionTrackerStateReleaseInProgress, timestamp);
+        }
+    }
+
+    // Find the maxima and minima of the key motion
+    if(missing_value<key_position>::isMissing(currentMaxPosition_) ||
+       currentKeyPosition > currentMaxPosition_) {
+        // Found a new local maximum
+        currentMaxIndex_ = currentBufferIndex;
+        currentMaxPosition_ = currentKeyPosition;
+        currentMaxTimestamp_ = timestamp;
+        
+        // If we previously found a maximum, go back to the original
+        // state so we can process the new max that is in progress
+        if(currentState_ == kPositionTrackerStatePartialPressFoundMax)
+            changeState(kPositionTrackerStatePartialPressAwaitingMax, timestamp);
+    }
+    else if(missing_value<key_position>::isMissing(currentMinPosition_) ||
+            currentKeyPosition < currentMinPosition_) {
+        // Found a new local minimum
+        currentMinIndex_ = currentBufferIndex;
+        currentMinPosition_ = currentKeyPosition;
+        currentMinTimestamp_ = timestamp;
+    }
+    
+    // Check if the deviation between min and max exceeds the threshold of significance,
+    // and if so, figure out when a peak occurs
+    if(!missing_value<key_position>::isMissing(currentMaxPosition_) &&
+       !missing_value<key_position>::isMissing(lastMinMaxPosition_)) {
+        if(currentMaxPosition_ - lastMinMaxPosition_ >= kPositionTrackerMinMaxSpacingThreshold && currentBufferIndex != currentMaxIndex_) {
+            // We need to come down off the current maximum before we can be sure that we've found the right location.
+            // Implement a sliding threshold that gets lower the farther away from the maximum we get
+            key_position triggerThreshold = kPositionTrackerMinMaxSpacingThreshold / (key_position)(currentBufferIndex - currentMaxIndex_);
+            
+            if(currentKeyPosition < currentMaxPosition_ - triggerThreshold) {
+                // Found the local maximum and the position has already retreated from it
+                lastMinMaxPosition_ = currentMaxPosition_;
+                
+                if(currentState_ == kPositionTrackerStatePressInProgress) {
+                    // If we were waiting for a press to complete, this is it.
+                    pressIndex_ = currentMaxIndex_;
+                    pressPosition_ = currentMaxPosition_;
+                    pressTimestamp_ = currentMaxTimestamp_;
+                    
+                    // Insert the state change into the buffer timestamped according to
+                    // when the maximum arrived, unless that would put it earlier than what's already there
+                    timestamp_type stateChangeTimestamp = latestTimestamp() > currentMaxTimestamp_ ? latestTimestamp() : currentMaxTimestamp_;
+                    changeState(kPositionTrackerStateDown, stateChangeTimestamp);
+                }
+                else if(currentState_ == kPositionTrackerStatePartialPressAwaitingMax) {
+                    // Otherwise if we were waiting for a maximum to occur that was
+                    // short of a full press, this might be it if it is of sufficient size
+                    if(currentMaxPosition_ >= kPositionTrackerFirstMaxThreshold) {
+                        timestamp_type stateChangeTimestamp = latestTimestamp() > currentMaxTimestamp_ ? latestTimestamp() : currentMaxTimestamp_;
+                        changeState(kPositionTrackerStatePartialPressFoundMax, stateChangeTimestamp);
+                    }
+                }
+                
+                // Reinitialize the minimum value for the next search
+                currentMinIndex_ = currentBufferIndex;
+                currentMinPosition_ = currentKeyPosition;
+                currentMinTimestamp_ = timestamp;
+            }
+        }
+    }
+    if(!missing_value<key_position>::isMissing(currentMinPosition_) &&
+       !missing_value<key_position>::isMissing(lastMinMaxPosition_)) {
+        if(lastMinMaxPosition_ - currentMinPosition_ >= kPositionTrackerMinMaxSpacingThreshold && currentBufferIndex != currentMinIndex_) {
+            // We need to come up from the current minimum before we can be sure that we've found the right location.
+            // Implement a sliding threshold that gets lower the farther away from the minimum we get
+            key_position triggerThreshold = kPositionTrackerMinMaxSpacingThreshold / (key_position)(currentBufferIndex - currentMinIndex_);
+
+            if(currentKeyPosition > currentMinPosition_ + triggerThreshold) {
+                // Found the local minimum and the position has already retreated from it
+                lastMinMaxPosition_ = currentMinPosition_;
+                
+                // If in the middle of releasing, see whether this minimum appears to have completed the release
+                if(currentState_ == kPositionTrackerStateReleaseInProgress) {
+                    if(currentMinPosition_ < kPositionTrackerReleaseFinishPosition) {
+                        releaseEndIndex_ = currentMinIndex_;
+                        releaseEndPosition_ = currentMinPosition_;
+                        releaseEndTimestamp_ = currentMinTimestamp_;
+                        
+                        timestamp_type stateChangeTimestamp = latestTimestamp() > currentMinTimestamp_ ? latestTimestamp() : currentMinTimestamp_;
+                        changeState(kPositionTrackerStateReleaseFinished, stateChangeTimestamp);
+                    }
+                }
+                
+                // Reinitialize the maximum value for the next search
+                currentMaxIndex_ = currentBufferIndex;
+                currentMaxPosition_ = currentKeyPosition;
+                currentMaxTimestamp_ = timestamp;
+            }
+        }
+    }
+}
+
+// Change the current state of the tracker and generate a notification
+void KeyPositionTracker::changeState(int newState, timestamp_type timestamp) {
+    KeyPositionTracker::key_buffer_index index;
+    KeyPositionTracker::key_buffer_index mostRecentIndex = 0;
+    
+    if(keyBuffer_.empty())
+        mostRecentIndex = keyBuffer_.endIndex() - 1;
+    
+    // Manage features based on state
+    switch(newState) {
+        case kPositionTrackerStatePressInProgress:
+            // Clear features for a retrigger
+            if(currentState_ == kPositionTrackerStateReleaseInProgress ||
+               currentState_ == kPositionTrackerStateReleaseFinished)
+                currentlyAvailableFeatures_ = KeyPositionTrackerNotification::kFeaturesNone;
+            
+            // Look for percussiveness first since it will always be available by the time of
+            // key press. That means we can count on it arriving before velocity every time.
+            if((currentlyAvailableFeatures_ & KeyPositionTrackerNotification::kFeaturePercussiveness) == 0
+               && percussivenessAvailableIndex_ == 0) {
+                currentlyAvailableFeatures_ |= KeyPositionTrackerNotification::kFeaturePercussiveness;
+                notifyFeature(KeyPositionTrackerNotification::kNotificationTypeFeatureAvailablePercussiveness, timestamp);
+                percussivenessAvailableIndex_ = 0;
+            }
+            
+            // Start looking for the data needed for MIDI onset velocity.
+            // Where did the key cross the escapement position? How many more samples do
+            // we need to calculate velocity?
+            index = findMostRecentKeyPositionCrossing(pressVelocityEscapementPosition_, false, 1000);
+            if(index + kPositionTrackerSamplesNeededForPressVelocityAfterEscapement <= mostRecentIndex) {
+                // Here, we already have the velocity information
+                currentlyAvailableFeatures_ |= KeyPositionTrackerNotification::kFeaturePressVelocity;
+                notifyFeature(KeyPositionTrackerNotification::kNotificationTypeFeatureAvailableVelocity, timestamp);
+            }
+            else {
+                // Otherwise, we need to send a notification when the information becomes available
+                pressVelocityAvailableIndex_ = index + kPositionTrackerSamplesNeededForPressVelocityAfterEscapement;
+            }
+            break;
+        case kPositionTrackerStateReleaseInProgress:
+            // Start looking for the data needed for MIDI release velocity.
+            // Where did the key cross the release escaoentb position? How many more samples do
+            // we need to calculate velocity?
+            prepareReleaseVelocityFeature(mostRecentIndex, timestamp);
+            break;
+        case kPositionTrackerStatePartialPressFoundMax:
+            // Also look for the percussiveness features, if not already present
+            if((currentlyAvailableFeatures_ & KeyPositionTrackerNotification::kFeaturePercussiveness) == 0
+               && percussivenessAvailableIndex_ == 0) {
+                currentlyAvailableFeatures_ |= KeyPositionTrackerNotification::kFeaturePercussiveness;
+                notifyFeature(KeyPositionTrackerNotification::kNotificationTypeFeatureAvailablePercussiveness, timestamp);
+                percussivenessAvailableIndex_ = 0;
+            }
+            break;
+        case kPositionTrackerStatePartialPressAwaitingMax:
+        case kPositionTrackerStateUnknown:
+            // Reset all features
+            currentlyAvailableFeatures_ = KeyPositionTrackerNotification::kFeaturesNone;
+            break;
+        case kPositionTrackerStateDown:
+        case kPositionTrackerStateReleaseFinished:
+        default:
+            // Don't change features
+            break;
+    }
+    
+    currentState_ = newState;
+    
+    KeyPositionTrackerNotification notification;
+    notification.type = KeyPositionTrackerNotification::kNotificationTypeStateChange;
+    notification.state = newState;
+    notification.features = currentlyAvailableFeatures_;
+    
+    insert(notification, timestamp);
+}
+
+// Notify listeners that a given feature has become available
+void KeyPositionTracker::notifyFeature(int notificationType, timestamp_type timestamp) {
+    // Can now calculate press velocity
+    KeyPositionTrackerNotification notification;
+    
+    notification.state = currentState_;
+    notification.type = notificationType;
+    notification.features = currentlyAvailableFeatures_;
+    
+    insert(notification, timestamp);
+}
+
+// When starting from a blank state, retroactively locate
+// the start of the key press so it can be used to calculate
+// features of key motion
+void KeyPositionTracker::findKeyPressStart(timestamp_type timestamp) {
+    if(keyBuffer_.size() < kPositionTrackerSamplesToAverageForStartVelocity + 1)
+        return;
+    
+    key_buffer_index index = keyBuffer_.endIndex() - 1;
+    int searchBackCounter = 0;
+    
+    while(index >= keyBuffer_.beginIndex() + kPositionTrackerSamplesToAverageForStartVelocity && searchBackCounter <= kPositionTrackerSamplesToSearchForStartLocation) {
+        // Take the N-sample velocity average and compare to a minimum threshold
+        key_position diffPosition = keyBuffer_[index] - keyBuffer_[index - kPositionTrackerSamplesToAverageForStartVelocity];
+        timestamp_diff_type diffTimestamp = keyBuffer_.timestampAt(index) - keyBuffer_.timestampAt(index - kPositionTrackerSamplesToAverageForStartVelocity);
+        key_velocity velocity = calculate_key_velocity(diffPosition, diffTimestamp);
+        
+        if(velocity < kPositionTrackerStartVelocityThreshold) {
+            break;
+        }
+        
+        searchBackCounter++;
+        index--;
+    }
+    
+    // Having either found the minimum velocity or reached the beginning of the search period,
+    // store the key start information. Since the velocity is calculated over a window, choose
+    // a start position in the middle of the window.
+    startIndex_ = index - kPositionTrackerSamplesToAverageForStartVelocity/2;
+    startPosition_ = keyBuffer_[index - kPositionTrackerSamplesToAverageForStartVelocity/2];
+    startTimestamp_ = keyBuffer_.timestampAt(index - kPositionTrackerSamplesToAverageForStartVelocity/2);
+    lastMinMaxPosition_ = startPosition_;
+    
+    // After saving that information, look further back for a specified number of samples to see if there
+    // is another mini-spike at the beginning of the key press. This can happen with highly percussive presses.
+    // If so, the start is actually the earlier time.
+    
+    // Leave index where it was...
+    searchBackCounter = 0;
+    bool haveFoundVelocitySpike = false, haveFoundNewMinimum = false;
+    
+    while(index >= keyBuffer_.beginIndex() + kPositionTrackerSamplesToAverageForStartVelocity && searchBackCounter <= kPositionTrackerSamplesToSearchBeyondStartLocation) {
+        // Take the N-sample velocity average and compare to a minimum threshold
+        key_position diffPosition = keyBuffer_[index] - keyBuffer_[index - kPositionTrackerSamplesToAverageForStartVelocity];
+        timestamp_diff_type diffTimestamp = keyBuffer_.timestampAt(index) - keyBuffer_.timestampAt(index - kPositionTrackerSamplesToAverageForStartVelocity);
+        key_velocity velocity = calculate_key_velocity(diffPosition, diffTimestamp);
+        
+        if(velocity > kPositionTrackerStartVelocitySpikeThreshold) {
+            std::cout << "At index " << index << ", velocity is " << velocity << std::endl;
+            haveFoundVelocitySpike = true;
+        }
+        
+        if(velocity < kPositionTrackerStartVelocityThreshold && haveFoundVelocitySpike) {
+            std::cout << "At index " << index << ", velocity is " << velocity << std::endl;
+            haveFoundNewMinimum = true;
+            break;
+        }
+        
+        searchBackCounter++;
+        index--;
+    }
+    
+    if(haveFoundNewMinimum) {
+        // Here we looked back beyond a small spike and found an earlier start time
+        startIndex_ = index - kPositionTrackerSamplesToAverageForStartVelocity/2;
+        startPosition_ = keyBuffer_[index - kPositionTrackerSamplesToAverageForStartVelocity/2];
+        startTimestamp_ = keyBuffer_.timestampAt(index - kPositionTrackerSamplesToAverageForStartVelocity/2);
+        lastMinMaxPosition_ = startPosition_;
+        
+        std::cout << "Found previous location\n";
+    }
+}
+
+// When a key is released, retroactively locate where the release started
+void KeyPositionTracker::findKeyReleaseStart(timestamp_type timestamp) {
+    if(keyBuffer_.size() < kPositionTrackerSamplesToAverageForStartVelocity + 1)
+        return;
+    
+    key_buffer_index index = keyBuffer_.endIndex() - 1;
+    int searchBackCounter = 0;
+    
+    while(index >= keyBuffer_.beginIndex() + kPositionTrackerSamplesToAverageForStartVelocity && searchBackCounter <= kPositionTrackerSamplesToSearchForReleaseLocation) {
+        // Take the N-sample velocity average and compare to a minimum threshold
+        key_position diffPosition = keyBuffer_[index] - keyBuffer_[index - kPositionTrackerSamplesToAverageForStartVelocity];
+        timestamp_diff_type diffTimestamp = keyBuffer_.timestampAt(index) - keyBuffer_.timestampAt(index - kPositionTrackerSamplesToAverageForStartVelocity);
+        key_velocity velocity = calculate_key_velocity(diffPosition, diffTimestamp);
+        
+        if(velocity > kPositionTrackerReleaseVelocityThreshold) {
+            std::cout << "Found release at index " << index << " (vel = " << velocity << ")\n";
+            break;
+        }
+        
+        searchBackCounter++;
+        index--;
+    }
+    
+    // Having either found the minimum velocity or reached the beginning of the search period,
+    // store the key release information.
+    releaseBeginIndex_ = index - kPositionTrackerSamplesToAverageForStartVelocity/2;
+    releaseBeginPosition_ = keyBuffer_[index - kPositionTrackerSamplesToAverageForStartVelocity/2];
+    releaseBeginTimestamp_ = keyBuffer_.timestampAt(index - kPositionTrackerSamplesToAverageForStartVelocity/2);
+    lastMinMaxPosition_ = releaseBeginPosition_;
+    
+    // Clear the release end position so there's no possibility of an inconsistent state
+    releaseEndIndex_ = 0;
+    releaseEndPosition_ = missing_value<key_position>::missing();
+    releaseEndTimestamp_ = missing_value<timestamp_type>::missing();
+}
+
+// Find the index at which the key position crosses the given threshold. Returns 0 if not found.
+KeyPositionTracker::key_buffer_index KeyPositionTracker::findMostRecentKeyPositionCrossing(key_position threshold, bool greaterThan, int maxDistance) {
+    if(keyBuffer_.empty())
+        return 0;
+    
+    key_buffer_index index = keyBuffer_.endIndex() - 1;
+    int searchBackCounter = 0;
+    
+    // Check if the most recent sample already meets the criterion. If so,
+    // there's no crossing yet.
+    if(keyBuffer_[index] >= threshold && greaterThan)
+        return 0;
+    if(keyBuffer_[index] <= threshold && !greaterThan)
+        return 0;
+    
+    while(index >= keyBuffer_.beginIndex() && searchBackCounter <= maxDistance) {
+        if(keyBuffer_[index] >= threshold && greaterThan)
+            return index;
+        else if(keyBuffer_[index] <= threshold && !greaterThan)
+            return index;
+        
+        searchBackCounter++;
+        index--;
+    }
+    
+    return 0;
+}
+
+void KeyPositionTracker::prepareReleaseVelocityFeature(KeyPositionTracker::key_buffer_index mostRecentIndex, timestamp_type timestamp) {
+    KeyPositionTracker::key_buffer_index index;
+
+    // Find the sample where the key position crosses the release threshold. What is returned
+    // will be the last sample which is above the threshold. What we need is the first sample
+    // below the threshold plus at least one more (SamplesNeededForReleaseVelocity...) to
+    // perform a local velocity calculation.
+    index = findMostRecentKeyPositionCrossing(releaseVelocityEscapementPosition_, true, 1000);
+
+    if(index == 0) {
+        // Haven't crossed the threshold yet
+        releaseVelocityWaitingForThresholdCross_ = true;
+    }
+    else if(index + kPositionTrackerSamplesNeededForReleaseVelocityAfterEscapement + 1 <= mostRecentIndex) {
+        // Here, we already have the velocity information
+        std::cout << "release available, at index = " << keyBuffer_[index] << ", most recent position = " << keyBuffer_[mostRecentIndex] << std::endl;
+        currentlyAvailableFeatures_ |= KeyPositionTrackerNotification::kFeatureReleaseVelocity;
+        notifyFeature(KeyPositionTrackerNotification::kNotificationTypeFeatureAvailableReleaseVelocity, timestamp);
+        releaseVelocityWaitingForThresholdCross_ = false;
+    }
+    else {
+        // Otherwise, we need to send a notification when the information becomes available
+        std::cout << "release available at index " << index + kPositionTrackerSamplesNeededForReleaseVelocityAfterEscapement + 1 << std::endl;
+        releaseVelocityAvailableIndex_ = index + kPositionTrackerSamplesNeededForReleaseVelocityAfterEscapement + 1;
+        releaseVelocityWaitingForThresholdCross_ = false;
+    }
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/TouchKeys/KeyPositionTracker.h	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,316 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+ 
+  KeyPositionTracker.h: parses continuous key position and detects the
+  state of the key.
+*/
+
+
+#ifndef __touchkeys__KeyPositionTracker__
+#define __touchkeys__KeyPositionTracker__
+
+#include <set>
+#include "../Utility/Node.h"
+#include "../Utility/Accumulator.h"
+#include "PianoTypes.h"
+
+// Three states of idle detector
+enum {
+	kPositionTrackerStateUnknown = 0,
+    kPositionTrackerStatePartialPressAwaitingMax,
+    kPositionTrackerStatePartialPressFoundMax,
+    kPositionTrackerStatePressInProgress,
+    kPositionTrackerStateDown,
+    kPositionTrackerStateReleaseInProgress,
+    kPositionTrackerStateReleaseFinished
+};
+
+// Constants for key state detection
+const key_position kPositionTrackerPressPosition = scale_key_position(0.75);
+const key_position kPositionTrackerPressHysteresis = scale_key_position(0.05);
+const key_position kPositionTrackerMinMaxSpacingThreshold = scale_key_position(0.02);
+const key_position kPositionTrackerFirstMaxThreshold = scale_key_position(0.075);
+const key_position kPositionTrackerReleaseFinishPosition = scale_key_position(0.2);
+
+// How far back to search at the beginning to find the real start or release of a key press
+const int kPositionTrackerSamplesToSearchForStartLocation = 50;
+const int kPositionTrackerSamplesToSearchBeyondStartLocation = 20;
+const int kPositionTrackerSamplesToSearchForReleaseLocation = 100;
+const int kPositionTrackerSamplesToAverageForStartVelocity = 3;
+const key_velocity kPositionTrackerStartVelocityThreshold = scale_key_velocity(0.5);
+const key_velocity kPositionTrackerStartVelocitySpikeThreshold = scale_key_velocity(2.5);
+const key_velocity kPositionTrackerReleaseVelocityThreshold = scale_key_velocity(-0.2);
+
+// Constants for feature calculations. The first one is the approximate location of the escapement
+// (empirically measured on one piano, so only approximate), used for velocity calculations
+const key_position kPositionTrackerDefaultPositionForPressVelocityCalculation = scale_key_position(0.65);
+const key_position kPositionTrackerDefaultPositionForReleaseVelocityCalculation = scale_key_position(0.5);
+const key_position kPositionTrackerPositionThresholdForPercussivenessCalculation = scale_key_position(0.4);
+const int kPositionTrackerSamplesNeededForPressVelocityAfterEscapement = 1;
+const int kPositionTrackerSamplesNeededForReleaseVelocityAfterEscapement = 1;
+
+// KeyPositionTrackerNotification
+//
+// This class contains information on the notifications sent and stored by
+// KeyPositionTracker. Includes state changes and feature available.
+
+class KeyPositionTrackerNotification {
+public:
+    enum {
+        kNotificationTypeStateChange = 1,
+        kNotificationTypeFeatureAvailableVelocity,
+        kNotificationTypeFeatureAvailableReleaseVelocity,
+        kNotificationTypeFeatureAvailablePercussiveness,
+        kNotificationTypeNewMinimum,
+        kNotificationTypeNewMaximum
+    };
+    
+    enum {
+        kFeaturesNone = 0,
+        kFeaturePressVelocity = 0x0001,
+        kFeatureReleaseVelocity = 0x0002,
+        kFeaturePercussiveness = 0x0004
+    };
+    
+    int type;
+    int state;
+    int features;
+};
+
+// KeyPositionTracker
+//
+// This class implements a state machine for a key that is currently active (not idle),
+// using continuous key position data to determine the location and parameters of key
+// presses and other events. It includes management of partial press patterns and detection
+// of percussiveness as well as velocity features.
+//
+// This class is triggered by new data points in the key position buffer. Its output is
+// a series of state changes which indicate what the key is doing.
+
+class KeyPositionTracker : public Node<KeyPositionTrackerNotification> {
+public:
+    typedef Node<key_position>::size_type key_buffer_index;
+    //typedef void (*KeyActionFunction)(KeyPositionTracker *object, void *userData);
+    
+    // Simple class to hold index/position/timestamp triads
+    class Event {
+    public:
+        Event() : index(0), position(missing_value<key_position>::missing()),
+        timestamp(missing_value<timestamp_type>::missing()) {}
+        
+        Event(key_buffer_index i, key_position p, timestamp_type t)
+        : index(i), position(p), timestamp(t) {}
+        
+        Event(const Event& obj)
+        : index(obj.index), position(obj.position), timestamp(obj.timestamp) {}
+        
+        Event& operator=(const Event& obj) {
+            index = obj.index;
+            position = obj.position;
+            timestamp = obj.timestamp;
+            return *this;
+        }
+        
+        key_buffer_index index;
+        key_position position;
+        timestamp_type timestamp;
+    };
+    
+    // Collection of features related to whether a key is percussively played or not
+    struct PercussivenessFeatures {
+        float percussiveness;                   // Calculated single feature based on everything below
+        Event velocitySpikeMaximum;             // Maximum and minimum points of the initial
+        Event velocitySpikeMinimum;             // velocity spike on a percussive press
+        timestamp_type timeFromStartToSpike;    // How long it took to reach the velocity spike
+        key_velocity areaPrecedingSpike;        // Total sum of velocity values from start to max
+        key_velocity areaFollowingSpike;        // Total sum of velocity values from max to min
+    };
+    
+public:
+	// ***** Constructors *****
+	
+	// Default constructor, passing the buffer on which to trigger
+	KeyPositionTracker(capacity_type capacity, Node<key_position>& keyBuffer);
+	
+	// Copy constructor
+	//KeyPositionTracker(KeyPositionTracker const& obj);
+	
+	// ***** State Access *****
+	
+    // Whether this object is currently tracking states
+    bool engaged() {
+        return engaged_;
+    }
+    
+	// Return the current state (unknown if nothing is in the buffer)
+	int currentState() {
+        return currentState_;
+    }
+    
+    // Information about important recent points
+    Event currentMax() {
+        return Event(currentMaxIndex_, currentMaxPosition_, currentMaxTimestamp_);
+    }
+    Event currentMin() {
+        return Event(currentMinIndex_, currentMinPosition_, currentMinTimestamp_);
+    }
+    Event pressStart() {
+        return Event(startIndex_, startPosition_, startTimestamp_);
+    }
+    Event pressFinish() {
+        return Event(pressIndex_, pressPosition_, pressTimestamp_);
+    }
+    Event releaseStart() {
+        return Event(releaseBeginIndex_, releaseBeginPosition_, releaseBeginTimestamp_);
+    }
+    Event releaseFinish() {
+        return Event(releaseEndIndex_, releaseEndPosition_, releaseEndTimestamp_);
+    }
+    
+    // ***** Key Press Features *****
+    
+    // Velocity for onset and release. The values without an argument use the stored
+    // current escapement point (which is also used for notification of availability).
+    std::pair<timestamp_type, key_velocity> pressVelocity();
+    std::pair<timestamp_type, key_velocity> releaseVelocity();
+    
+    std::pair<timestamp_type, key_velocity> pressVelocity(key_position escapementPosition);
+    std::pair<timestamp_type, key_velocity> releaseVelocity(key_position returnPosition);
+    
+    // Set the threshold where we look for press velocity calculations. It
+    // can be anything up to the press position threshold on the upward side
+    // and anything down to the final release position on the downward side.
+    void setPressVelocityEscapementPosition(key_position pos) {
+        if(pos > kPositionTrackerPressPosition + kPositionTrackerPressHysteresis)
+            pressVelocityEscapementPosition_ = kPositionTrackerPressPosition + kPositionTrackerPressHysteresis;
+        else
+            pressVelocityEscapementPosition_ = pos;
+    }
+    void setReleaseVelocityEscapementPosition(key_position pos) {
+        if(pos < kPositionTrackerReleaseFinishPosition)
+            releaseVelocityEscapementPosition_ = kPositionTrackerReleaseFinishPosition;
+        else
+            releaseVelocityEscapementPosition_ = pos;
+    }
+    
+    
+    // Percussiveness (struck vs. pressed keys)
+    PercussivenessFeatures pressPercussiveness();
+    
+	// ***** Modifiers *****
+    
+    // Register for updates from the key positon buffer
+    void engage();
+    
+    // Unregister for updates from the key position buffer
+    void disengage();
+	
+    // Reset the state back initial values
+	void reset();
+	
+	// ***** Evaluator *****
+	
+    // This method receives triggers whenever a new sample enters the buffer. It updates
+    // the state depending on the profile of the key position.
+	void triggerReceived(TriggerSource* who, timestamp_type timestamp);
+	
+private:
+    // ***** Internal Helper Methods *****
+    
+    // Change the current state
+    void changeState(int newState, timestamp_type timestamp);
+    
+    // Insert a new feature notification
+    void notifyFeature(int notificationType, timestamp_type timestamp);
+    
+    // Work backwards in the key position buffer to find the start/release of a press
+    void findKeyPressStart(timestamp_type timestamp);
+    void findKeyReleaseStart(timestamp_type timestamp);
+    
+    // Generic method to find the most recent crossing of a given point
+    key_buffer_index findMostRecentKeyPositionCrossing(key_position threshold, bool greaterThan, int maxDistance);
+    
+    // Look for the crossing of the release velocity threshold to prepare to send the feature
+    void prepareReleaseVelocityFeature(KeyPositionTracker::key_buffer_index mostRecentIndex, timestamp_type timestamp);
+    
+	// ***** Member Variables *****
+	
+	Node<key_position>& keyBuffer_;		// Raw key position data
+    bool engaged_;                      // Whether we're actively listening to incoming updates
+    int currentState_;                  // Our current state
+    int currentlyAvailableFeatures_;    // Which features can be calculated for the current press
+    
+    // Position tracking information for significant points (minima and maxima)
+    key_position startPosition_;                                // Position of where the key press started
+    timestamp_type startTimestamp_;                             // Timestamp of where the key press started
+    key_buffer_index startIndex_;                               // Index in the buffer where the start occurred
+    key_position pressPosition_;                                // Position of where the key is fully pressed
+    timestamp_type pressTimestamp_;                             // Timestamp of where the key is fully pressed
+    key_buffer_index pressIndex_;                               // Index in the buffer where the press occurred
+    key_position releaseBeginPosition_;                         // Position of where the key release began
+    timestamp_type releaseBeginTimestamp_;                      // Timestamp of where the key release began
+    key_buffer_index releaseBeginIndex_;                        // Index in the buffer of where the key release began
+    key_position releaseEndPosition_;                           // Position of where the key release ended
+    timestamp_type releaseEndTimestamp_;                        // Timestamp of where the key release ended
+    key_buffer_index releaseEndIndex_;                          // Index in the buffer of where the key release ended
+    key_position currentMinPosition_, currentMaxPosition_;      // Running min and max key position
+    timestamp_type currentMinTimestamp_, currentMaxTimestamp_;  // Times for the above positions
+    key_buffer_index currentMinIndex_, currentMaxIndex_;        // Indices in the buffer for the recent min/max
+    key_position lastMinMaxPosition_;                           // Position of the last significant point
+    
+    // Persistent parameters relating to feature calculation
+    key_position pressVelocityEscapementPosition_;              // Position at which onset velocity is calculated
+    key_position releaseVelocityEscapementPosition_;            // Position at which release velocity is calculate
+    key_buffer_index pressVelocityAvailableIndex_;              // When we can calculate press velocity
+    key_buffer_index releaseVelocityAvailableIndex_;            // When we can calculate release velocity
+    bool releaseVelocityWaitingForThresholdCross_;              // Set to true if we need to look for release escapement cross
+    key_buffer_index percussivenessAvailableIndex_;             // When we can calculate percussiveness features
+    
+    /*
+    typedef struct {
+		int runningSum;						// sum of last N points (i.e. mean * N)
+		int runningSumMaxLength;			// the value of N above
+		int runningSumCurrentLength;		// How many values are actually part of the sum right now (transient condition)
+		int startValuesSum;					// sum of the last N start values (to calculate returning quiescent position)
+		int startValuesSumMaxLength;
+		int startValuesSumCurrentLength;
+		
+		int maxVariation;					// The maximum deviation from mean of the last group of samples
+		int flatCounter;					// how many successive samples have been "flat" (minimal change)
+		int currentStartValue;				// values and positions of several key points for active keys
+		int currentStartPosition;
+		int currentMinValue;
+		int currentMinPosition;
+		int currentMaxValue;
+		int currentMaxPosition;
+		int lastKeyPointValue;				// the value of the last important point {start, max, min}
+		
+		deque<keyPointHistory> recentKeyPoints; // the minima and maxima since the key started
+		bool sentPercussiveMidiOn;			// HACK: whether we've sent the percussive MIDI event
+		
+		int pressValue;						// the value at the maximum corresponding to the end of the key press motion
+		int pressPosition;					// the location in the buffer of this event (note: not the timestamp)
+		int releaseValue;					// the value the key held right before release
+		int releasePosition;				// the location in the buffer of the release corner
+	} keyParameters;
+	*/
+};
+
+
+#endif /* defined(__touchkeys__KeyPositionTracker__) */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/TouchKeys/KeyTouchFrame.h	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,84 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+ 
+  KeyTouchFrame.h: class that represents the data of one frame from the TouchKeys.
+*/
+
+#ifndef KEY_TOUCH_FRAME_H
+#define KEY_TOUCH_FRAME_H
+
+#define kWhiteFrontBackCutoff (6.5/19.0)	// Border between 2- and 1-dimensional sensing regions
+
+// This class holds one frame of key touch data, both raw values and unique ID numbers
+// that help connect one frame to the next
+
+class KeyTouchFrame {
+public:
+	KeyTouchFrame() : count(0), locH(-1.0), nextId(0), white(true) {
+		for(int i = 0; i < 3; i++) {
+			ids[i] = -1;
+			locs[i] = -1.0;
+			sizes[i] = 0.0;
+		}
+	}
+	
+	KeyTouchFrame(int newCount, float* newLocs, float *newSizes, float newLocH, bool newWhite)
+	: count(newCount), locH(newLocH), nextId(0), white(newWhite) {
+		for(int i = 0; i < count; i++) {
+			ids[i] = -1;
+			locs[i] = newLocs[i];
+			sizes[i] = newSizes[i];
+		}
+		for(int i = count; i < 3; i++) {
+			ids[i] = -1;
+			locs[i] = -1.0;
+			sizes[i] = 0.0;
+		}
+	}
+	
+	KeyTouchFrame(const KeyTouchFrame& copy) : count(copy.count), locH(copy.locH), nextId(copy.nextId), white(copy.white) {
+		for(int i = 0; i < 3; i++) {
+			ids[i] = copy.ids[i];
+			locs[i] = copy.locs[i];
+			sizes[i] = copy.sizes[i];
+		}
+	}
+	
+	// Horizontal location only makes sense in the front part of the key.  Return the
+	// value if applicable, otherwise -1.
+	float horizontal(int index) const {
+		//if(!white)
+		//	return -1.0;
+		if(index >= count || index > 2 || index < 0)
+			return -1.0;
+		//if(locs[index] < kWhiteFrontBackCutoff) // FIXME: need better hardware-dependent solution to this
+			return locH;
+		//return -1.0;
+	}
+	
+	int count;			// Number of active touches (0-3)
+	int ids[3];			// Unique ID numbers for current touches
+	float locs[3];		// Vertical location of current touches
+	float sizes[3];		// Contact area of current touches
+	float locH;			// Horizontal location (white keys only)
+	int nextId;			// ID number to be used for the next new touch added
+	bool white;			// Whether this is a white key
+};
+
+#endif /* KEY_TOUCH_FRAME_H */
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/TouchKeys/LogPlayback.cpp	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,383 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+ 
+  LogPlayback.cpp: basic functions for playing back a recorded TouchKeys log.
+*/
+
+#include "LogPlayback.h"
+
+LogPlayback::LogPlayback(PianoKeyboard& keyboard, MidiInputController& midi)
+: keyboard_(keyboard), midiInputController_(midi), open_(false), playing_(false), paused_(false),
+  usingTouch_(false), usingMidi_(false), playbackRate_(1.0),
+  nextTouchMidiNote_(0), nextTouchTimestamp_(0), nextMidiTimestamp_(0),
+  lastMidiTimestamp_(0), timestampOffset_(0)
+{
+    // Create a statically bound call to the performMapping() method that
+    // we use each time we schedule a new mapping
+    touchAction_ = boost::bind(&LogPlayback::nextTouchEvent, this);
+    midiAction_ = boost::bind(&LogPlayback::nextMidiEvent, this);
+}
+
+LogPlayback::~LogPlayback()
+{
+    if(open_)
+        closeLogFiles();
+}
+
+// File management. Open a touch and/or MIDI file. Returns true on success.
+// Pass a blank string to either one of the paths to not use that form of data capture
+bool LogPlayback::openLogFiles(string const& touchPath, string const& midiPath) {
+    touchLog_.open (touchPath.c_str(), ios::in | ios::binary);
+    midiLog_.open (midiPath.c_str(), ios::in | ios::binary);
+    
+    usingTouch_ = touchLog_.is_open();
+    usingMidi_ = midiLog_.is_open();
+    
+    // Check for bad file paths
+    if(!usingTouch_ && touchPath != "")
+        return false;
+    if(!usingMidi_ && midiPath != "")
+        return false;
+    if(!usingTouch_ && !usingMidi_)
+        return false;
+    
+    // Set defaults
+    open_ = true;
+    playing_ = paused_ = false;
+    playbackRate_ = 1.0;
+    return true;
+}
+
+// Close the current files
+void LogPlayback::closeLogFiles() {
+    if(!open_)
+        return;
+    if(playing_)
+        stopPlayback();
+    touchLog_.close();
+    midiLog_.close();
+    open_ = playing_ = paused_ = false;
+}
+
+// Start, stop, pause, resume
+void LogPlayback::startPlayback(timestamp_type startingTimestamp) {
+    if(!open_)
+        return;
+    
+    // Start the playback scheduler thread
+    playbackScheduler_.start(0);
+    
+    timestamp_type firstTouchTimestamp, firstMidiTimestamp;
+    
+    // Register actions on the scheduler thread
+    if(usingTouch_) {
+        readNextTouchFrame();
+        firstTouchTimestamp = nextTouchTimestamp_;
+        timestampOffset_ = playbackScheduler_.currentTimestamp() - firstTouchTimestamp;
+    }
+    if(usingMidi_) {
+        readNextMidiFrame();
+        firstMidiTimestamp = nextMidiTimestamp_;
+        lastMidiTimestamp_ = nextMidiTimestamp_; // First timestamp difference is 0
+        
+        // Timestamp offset is to first MIDI event, unless there's an earlier touch event
+        if(!(usingTouch_ && (firstTouchTimestamp < firstMidiTimestamp)))
+            timestampOffset_ = playbackScheduler_.currentTimestamp() - firstMidiTimestamp;
+    }
+    
+    playing_ = true;
+    paused_ = false;
+    
+    if(usingTouch_)
+        playbackScheduler_.schedule(this, touchAction_, playbackScheduler_.currentTimestamp());        
+    if(usingMidi_)
+        playbackScheduler_.schedule(this, midiAction_, playbackScheduler_.currentTimestamp());
+}
+
+void LogPlayback::stopPlayback() {
+    playing_ = paused_ = false;
+    
+    // Stop the playback scheduler thread
+    playbackScheduler_.stop();
+    playbackScheduler_.unschedule(this);
+}
+
+// Pause a currently playing file. Save the pause time so the offset
+// can be recalculated when it resumes
+void LogPlayback::pausePlayback() {
+    if(open_ && playing_) {
+        playbackScheduler_.unschedule(this);
+        
+        // TODO: consider thread safety: what happens if this comes during one of the scheduled calls?
+        
+        paused_ = true;
+        pauseTimestamp_ = playbackScheduler_.currentTimestamp();
+    }
+}
+
+// Resume playback after a pause
+void LogPlayback::resumePlayback() {
+    if(paused_) {
+        paused_ = false;
+        timestamp_type resumeTimestamp = playbackScheduler_.currentTimestamp();
+        
+        // Update the timestamp offset
+        timestampOffset_ += resumeTimestamp - pauseTimestamp_;
+        
+        // Reschedule calls
+        if(usingTouch_)
+            playbackScheduler_.schedule(this, touchAction_, nextTouchTimestamp_ + timestampOffset_);
+        if(usingMidi_)
+            playbackScheduler_.schedule(this, touchAction_, nextMidiTimestamp_ + timestampOffset_);
+    }
+}
+
+// Seek to a timestamp in the file
+void LogPlayback::seekPlayback(timestamp_type newTimestamp) {
+    // Advance through the file until we reach the indicated timestamp
+    
+    if(!playing_ || !open_)
+        return;
+    
+    // Remove any future actions while we perform the seek
+    playbackScheduler_.unschedule(this);
+    //timestamp_diff_type offset = 0;
+    timestamp_type firstUpcomingTimestamp = 0;
+    
+    if(usingTouch_) {
+        //timestamp_type lastTimestamp = nextTouchTimestamp_;
+        
+        // TODO: this assumes the seek is moving forward
+        while(nextTouchTimestamp_ <= newTimestamp) {
+            if(!readNextTouchFrame()) { // EOF or error
+                usingTouch_ = false;
+                if(!usingMidi_)
+                    playing_ = paused_ = false;
+                break;
+            }
+        }
+        
+        // Now we have the first event scheduled after the seek location
+        // Update timestamp offset to continue playback from here.
+
+        //offset = nextTouchTimestamp_ - lastTimestamp;
+        firstUpcomingTimestamp = nextTouchTimestamp_;
+    }
+    if(usingMidi_) {
+        //timestamp_type lastTimestamp = nextMidiTimestamp_;
+        
+        // TODO: this assumes the seek is moving forward
+        while(nextMidiTimestamp_ <= newTimestamp) {
+            if(!readNextMidiFrame()) { // EOF or error
+                usingMidi_ = false;
+                if(!usingTouch_)
+                    playing_ = paused_ = false;
+                break;
+            }
+        }
+        
+        // Now we have the first event scheduled after the seek location
+        // Update timestamp offset to continue playback from here.
+        // Use whichever event came first
+        
+        //if(!(usingTouch_ && (nextMidiTimestamp_ - lastTimestamp) > offset))
+        //    offset = (nextMidiTimestamp_ - lastTimestamp);
+        if(!usingTouch_ || nextMidiTimestamp_ < nextTouchTimestamp_);
+            firstUpcomingTimestamp = nextMidiTimestamp_;
+    }
+    
+    // Update the timestamp offset
+    timestampOffset_ = playbackScheduler_.currentTimestamp() - firstUpcomingTimestamp;
+    
+    if(usingTouch_)
+        playbackScheduler_.schedule(this, touchAction_, nextTouchTimestamp_ + timestampOffset_);
+    if(usingMidi_)
+        playbackScheduler_.schedule(this, midiAction_, nextMidiTimestamp_ + timestampOffset_);
+}
+
+// Change the playback rate (1.0 being the standard speed)
+void LogPlayback::changePlaybackRate(float rate) {
+    playbackRate_ = rate;
+}
+
+// Events the scheduler calls when the right time elapses. Find the
+// next touch or MIDI event and play it back
+timestamp_type LogPlayback::nextTouchEvent() {
+    if(!playing_ || !open_ || paused_)
+        return 0;
+    
+    // TODO: handle playback rate
+    
+    // Play the most recent stored touch frame
+    if(nextTouchMidiNote_ >= 0 && nextTouchMidiNote_ < 128) {
+        // Use PianoKeyboard timestamps for the messages we send since our scheduler
+        // may have a different idea of time.
+        
+        if(nextTouch_.count == 0) {
+            if(keyboard_.key(nextTouchMidiNote_) != 0)
+                keyboard_.key(nextTouchMidiNote_)->touchOff(keyboard_.schedulerCurrentTimestamp());
+            /*
+             // Send raw OSC message if enabled
+             if(sendRawOscMessages_) {
+             keyboard_.sendMessage("/touchkeys/raw-off", "iii",
+             octave, key, frame,
+             LO_ARGS_END );
+             }
+             */
+        }
+        else {
+            if(keyboard_.key(nextTouchMidiNote_) != 0)
+                keyboard_.key(nextTouchMidiNote_)->touchInsertFrame(nextTouch_,
+                                                                keyboard_.schedulerCurrentTimestamp());
+            /*if(sendRawOscMessages_) {
+                keyboard_.sendMessage("/touchkeys/raw", "iiifffffff",
+                                      octave, key, frame,
+                                      sliderPosition[0],
+                                      sliderSize[0],
+                                      sliderPosition[1],
+                                      sliderSize[1],
+                                      sliderPosition[2],
+                                      sliderSize[2],
+                                      sliderPositionH,
+                                      LO_ARGS_END );
+            }*/
+        }
+    }
+    
+    bool newTouchFound = readNextTouchFrame();
+    
+    // Go through next touch frames and send them as long as the timestamp is not in the future
+    while(newTouchFound && (nextTouchTimestamp_ + timestampOffset_) <= playbackScheduler_.currentTimestamp()) {
+        if(nextTouchMidiNote_ >= 0 && nextTouchMidiNote_ < 128) {
+            // Use PianoKeyboard timestamps for the messages we send since our scheduler
+            // may have a different idea of time.
+            
+            if(keyboard_.key(nextTouchMidiNote_) != 0) {
+                if(nextTouch_.count == 0)
+                    keyboard_.key(nextTouchMidiNote_)->touchOff(keyboard_.schedulerCurrentTimestamp());
+                else
+                    keyboard_.key(nextTouchMidiNote_)->touchInsertFrame(nextTouch_,
+                                                                        keyboard_.schedulerCurrentTimestamp());
+            }
+        }
+        
+        readNextTouchFrame();
+    }
+    
+    if(!newTouchFound) { // EOF or error
+        usingTouch_ = false;
+        if(!usingMidi_)
+            playing_ = paused_ = false;
+        return 0;
+    }
+    else // Return the timestamp of the next call
+        return (nextTouchTimestamp_ + timestampOffset_);
+}
+
+timestamp_type LogPlayback::nextMidiEvent() {
+    if(!playing_ || !open_ || paused_)
+        return 0;
+    
+    // TODO: handle playback rate
+    
+    // Play the most recent stored touch frame
+    if(nextMidi_.size() >= 3)
+        midiInputController_.handleIncomingMidiMessage(0, MidiMessage(nextMidi_[0], nextMidi_[1], nextMidi_[2]));
+    //midiInputController_.rtMidiCallback(nextMidiTimestamp_ - lastMidiTimestamp_, &nextMidi_, 0);
+    lastMidiTimestamp_ = nextMidiTimestamp_;
+    
+    bool newMidiEventFound = readNextMidiFrame();
+    
+    // Go through next touch frames and send them as long as the timestamp is not in the future
+    while(newMidiEventFound && (nextMidiTimestamp_ + timestampOffset_) <= playbackScheduler_.currentTimestamp()) {
+        if(nextMidi_.size() >= 3)
+            midiInputController_.handleIncomingMidiMessage(0, MidiMessage(nextMidi_[0], nextMidi_[1], nextMidi_[2]));
+        //midiInputController_.rtMidiCallback(nextMidiTimestamp_ - lastMidiTimestamp_, &nextMidi_, 0);
+        lastMidiTimestamp_ = nextMidiTimestamp_;
+        
+        readNextMidiFrame();
+    }
+    
+    if(!newMidiEventFound) { // EOF or error
+        usingMidi_ = false;
+        if(!usingTouch_)
+            playing_ = paused_ = false;
+        return 0;
+    }
+    else // Return the timestamp of the next call
+        return (nextMidiTimestamp_ + timestampOffset_);
+}
+
+// Retrieve the next key touch frame from the log file
+// Return true if a touch was found, false if EOF or an error occurred
+bool LogPlayback::readNextTouchFrame() {
+    int frameCounter;
+    
+    try {
+        touchLog_.read((char *)&nextTouchTimestamp_, sizeof(timestamp_type));
+        touchLog_.read((char *)&frameCounter, sizeof(int));
+        touchLog_.read((char *)&nextTouchMidiNote_, sizeof(int));
+        touchLog_.read((char *)&nextTouch_, sizeof(KeyTouchFrame));
+    }
+    catch(...) {
+        cout << "error reading touch\n";
+        return false;
+    }
+    if(touchLog_.eof()) {
+        cout << "Touch log playback finished\n";
+        return false;
+    }
+    
+    //cout << "read touch on key " << nextTouchMidiNote_ << " timestamp " << nextTouchTimestamp_ << endl;
+    
+    // TODO: what about frameCounter
+    
+    return true;
+}
+
+// Retrieve the next MIDI frame from the log file
+// Return true if an event was found, false if EOF or an error occurred
+bool LogPlayback::readNextMidiFrame() {
+    int midi0, midi1, midi2;
+    
+    try {
+        midiLog_.read((char*)&nextMidiTimestamp_, sizeof (timestamp_type));
+        midiLog_.read((char*)&midi0, sizeof (int));
+        midiLog_.read((char*)&midi1, sizeof (int));
+        midiLog_.read((char*)&midi2, sizeof (int));
+    }
+    catch(...) {
+        cout << "error reading MIDI\n";
+        return false;
+    }
+    
+    if(midiLog_.eof()) {
+        cout << "MIDI log playback finished\n";
+        return false;
+    }
+    
+    nextMidi_.clear();
+    nextMidi_.push_back((unsigned char)midi0);
+    nextMidi_.push_back((unsigned char)midi1);
+    nextMidi_.push_back((unsigned char)midi2);
+    
+    //cout << "read MIDI data " << (int)midi0 << " " << (int)midi1 << " " << (int)midi2 << endl;
+    
+    return true;
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/TouchKeys/LogPlayback.h	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,92 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+ 
+  LogPlayback.h: basic functions for playing back a recorded TouchKeys log.
+*/
+
+#ifndef __touchkeys__LogPlayback__
+#define __touchkeys__LogPlayback__
+
+#include <iostream>
+#include <fstream>
+#include <vector>
+#include <boost/bind.hpp>
+#include "MidiInputController.h"
+#include "KeyTouchFrame.h"
+#include "PianoKeyboard.h"
+#include "../Utility/Scheduler.h"
+
+using namespace std;
+
+class LogPlayback {
+public:
+    // ***** Constructor and Destructor *****
+    LogPlayback(PianoKeyboard& keyboard, MidiInputController& midi);
+    
+    ~LogPlayback();
+    
+    // ***** Log File Playback *****
+    // File management
+    bool openLogFiles(string const& touchPath, string const& midiPath);
+    void closeLogFiles();
+    
+    // Start, stop, pause, resume
+    void startPlayback(timestamp_type startingTimestamp = 0);
+    void stopPlayback();
+    void pausePlayback();
+    void resumePlayback();
+    
+    // Seek to location and/or change the rate
+    void seekPlayback(timestamp_type newTimestamp);
+    void changePlaybackRate(float rate);
+    
+    // Scheduler action functions
+    timestamp_type nextTouchEvent();
+    timestamp_type nextMidiEvent();
+   
+private:
+    bool readNextTouchFrame();
+    bool readNextMidiFrame();
+    
+	PianoKeyboard& keyboard_;	    // Main keyboard controller
+    MidiInputController& midiInputController_; // MIDI controller
+    Scheduler playbackScheduler_;   // Scheduler thread to send messages
+    Scheduler::action touchAction_; // Scheduler action for playing next touch
+    Scheduler::action midiAction_;  // Scheduler action for playing next MIDI event
+    
+    ifstream touchLog_;           // Log file for key touches
+    ifstream midiLog_;            // Log file for MIDI data
+    
+    bool open_;                   // Whether files are open
+    bool playing_;                // Whether playback is active
+    bool paused_;                 // Whether playback is active, but paused
+    bool usingTouch_, usingMidi_; // Whether touch and MIDI files are present
+    float playbackRate_;          // Current playback rate (default 1.0)
+    
+    KeyTouchFrame nextTouch_;     // Next touch frame to play
+    int nextTouchMidiNote_;       // MIDI note for next touch frame to play
+    timestamp_type nextTouchTimestamp_; // When the next touch happens
+    std::vector<unsigned char> nextMidi_;    // Next MIDI event to play
+    timestamp_type nextMidiTimestamp_;  // When the next MIDI event happens
+    timestamp_type lastMidiTimestamp_;  // When the last MIDI event happened
+    timestamp_type pauseTimestamp_;     // When the playback was paused
+    timestamp_diff_type timestampOffset_;   // Difference between file and current playback time
+};
+
+#endif /* defined(__touchkeys__LogPlayback__) */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/TouchKeys/MidiInputController.cpp	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,344 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+ 
+  MidiInputController.cpp: handles incoming MIDI data and manages input
+  ports. Detailed processing is broken down by keyboard segment; see
+  MidiKeyboardSegment.h/cpp for more.
+*/
+
+
+#include "MidiInputController.h"
+#include "MidiOutputController.h"
+#include "../Mappings/MappingFactory.h"
+
+#undef MIDI_INPUT_CONTROLLER_DEBUG_RAW
+
+// Constructor
+
+MidiInputController::MidiInputController(PianoKeyboard& keyboard) 
+: keyboard_(keyboard), midiOutputController_(0), segmentUniqueIdentifier_(0)
+{    
+    logFileCreated = false;
+    loggingActive = false;
+
+}
+
+void MidiInputController::setMidiOutputController(MidiOutputController* ct) {
+    midiOutputController_ = ct;
+    
+    // Propagate the change to the keyboard segments
+    ScopedLock sl(segmentsMutex_);
+    for(int i = 0; i < segments_.size(); i++) {
+        segments_[i]->setMidiOutputController(ct);
+    }
+}
+
+// ------------------------------------------------------
+// create a new MIDI log file, ready to have data written to it
+void MidiInputController::createLogFile(string midiLog_filename, string path)
+{
+    // indicate that we have created a log file (so we can close it later)
+    logFileCreated = true;
+    
+    if (path.compare("") != 0)
+    {
+        path = path + "/";
+    }
+    
+    midiLog_filename = path + midiLog_filename;
+    
+    char *fileName = (char*)midiLog_filename.c_str();
+    
+    // create output file
+    midiLog.open (fileName, ios::out | ios::binary);
+    midiLog.seekp(0);
+}
+
+// ------------------------------------------------------
+// close the existing log file
+void MidiInputController::closeLogFile()
+{
+    if (logFileCreated)
+    {
+        midiLog.close();
+        logFileCreated = false;
+    }
+}
+
+// ------------------------------------------------------
+// start logging midi data
+void MidiInputController::startLogging()
+{
+    loggingActive = true;
+}
+
+// ------------------------------------------------------
+// stop logging midi data
+void MidiInputController::stopLogging()
+{
+    loggingActive = false;
+}
+
+// Iterate through the available MIDI devices.  Return a vector containing
+// indices and names for each device.  The index will later be passed back
+// to indicate which device to open.
+
+vector<pair<int, string> > MidiInputController::availableMidiDevices() {
+	vector<pair<int, string> > deviceList;
+    
+	try {
+        StringArray deviceStrings = MidiInput::getDevices();
+		
+		for(int i = 0; i < deviceStrings.size(); i++) {
+			pair<int, string> p(i, string(deviceStrings[i].toUTF8()));
+			deviceList.push_back(p);
+		}
+	}
+	catch(...) {
+		deviceList.clear();
+	}
+	
+	return deviceList;
+}
+
+// Enable a new MIDI port according to its index (returned from availableMidiDevices())
+// Returns true on success.
+
+bool MidiInputController::enablePort(int portNumber) {
+	if(portNumber < 0)
+		return false;
+	
+    MidiInput *device = MidiInput::openDevice(portNumber, this);
+        
+    if(device == 0) {
+        cout << "Failed to enable MIDI input port " << portNumber << ")\n";
+        return false;
+    }
+    
+    //cout << "Enabling MIDI input port " << portNumber << " (" << device->getName() << ")\n";
+    device->start();
+
+    // Save the device in the set of ports
+    activePorts_[portNumber] = device;
+
+	return true;
+}
+
+// Enable all current MIDI ports
+
+bool MidiInputController::enableAllPorts() {
+	bool enabledPort = false;
+	vector<pair<int, string> > ports = availableMidiDevices();
+	vector<pair<int, string> >::iterator it = ports.begin();
+	
+	while(it != ports.end()) {
+		// Don't enable MIDI input from our own virtual output
+		if(it->second != string(kMidiVirtualOutputName.toUTF8()))
+			enabledPort |= enablePort((it++)->first);
+		else
+			it++;
+	}
+	
+	return enabledPort;
+}
+
+// Remove a specific MIDI input source and free associated memory
+
+void MidiInputController::disablePort(int portNumber) {
+	if(activePorts_.count(portNumber) <= 0)
+		return;
+	
+	MidiInput *device = activePorts_[portNumber];
+
+    if(device == 0)
+        return;
+    
+	//cout << "Disabling MIDI input port " << portNumber << " (" << device->getName() << ")\n";
+    device->stop();
+    delete device;
+    
+	activePorts_.erase(portNumber);
+}
+
+// Remove all MIDI input sources and free associated memory
+
+void MidiInputController::disableAllPorts() {
+	map<int, MidiInput*>::iterator it;
+	
+	//cout << "Disabling all MIDI input ports\n";
+	
+	it = activePorts_.begin();
+	
+	while(it != activePorts_.end()) {
+        if(it->second == 0) {
+            it++;
+            continue;
+        }
+		it->second->stop();                     // disable port
+		delete it->second;						// free MidiInputCallback
+		it++;
+	}
+	
+	activePorts_.clear();
+}
+
+// Return a list of active ports
+
+vector<int> MidiInputController::activePorts() {
+    vector<int> ports;
+    
+	map<int, MidiInput*>::iterator it;
+    
+    for(it = activePorts_.begin(); it != activePorts_.end(); ++it) {
+        ports.push_back(it->first);
+    }
+    
+    return ports;
+}
+
+// Add a new keyboard segment. Returns a pointer to the newly created segment
+MidiKeyboardSegment* MidiInputController::addSegment(int outputPortNumber,
+                                                          int noteMin, int noteMax,
+                                                          int channelMask) {
+    ScopedLock sl(segmentsMutex_);
+    
+    // Create a new segment and populate its values
+    MidiKeyboardSegment *segment = new MidiKeyboardSegment(keyboard_);
+    
+    segment->setMidiOutputController(midiOutputController_);
+    segment->setOutputPort(outputPortNumber);
+    segment->setNoteRange(noteMin, noteMax);
+    segment->setChannelMask(channelMask);
+    
+    // Add the segment to the vector and return the pointer
+    segments_.push_back(segment);
+    segmentUniqueIdentifier_++;
+    return segment;
+}
+
+// Remove a segment by index or by object
+void MidiInputController::removeSegment(int index) {
+    ScopedLock sl(segmentsMutex_);
+    
+    if(index < 0 || index >= segments_.size())
+        return;
+    
+    MidiKeyboardSegment* segment = segments_[index];
+    delete segment;
+    segments_.erase(segments_.begin() + index);
+    segmentUniqueIdentifier_++;
+}
+
+void MidiInputController::removeSegment(MidiKeyboardSegment* segment) {
+    ScopedLock sl(segmentsMutex_);
+    
+    for(int i = 0; i < segments_.size(); i++) {
+        if(segments_[i] == segment) {
+            delete segment;
+            segments_.erase(segments_.begin() + i);
+            break;
+        }
+    }
+    segmentUniqueIdentifier_++;
+}
+
+void MidiInputController::removeAllSegments() {
+    ScopedLock sl(segmentsMutex_);
+
+    for(int i = 0; i < segments_.size(); i++)
+        delete segments_[i];
+    segments_.clear();
+    segmentUniqueIdentifier_++;
+}
+
+// Disable any currently active notes
+
+void MidiInputController::allNotesOff() {
+    ScopedLock sl(segmentsMutex_);
+    
+    for(int i = 0; i < segments_.size(); i++)
+        segments_[i]->allNotesOff();
+}
+
+// This gets called every time MIDI data becomes available on any input controller. source tells
+// us where the message came from, and may be 0 if being called internally.
+
+void MidiInputController::handleIncomingMidiMessage(MidiInput* source, const MidiMessage& message)
+//void MidiInputController::rtMidiCallback(double deltaTime, vector<unsigned char> *message, int inputNumber)
+{
+	// Juce will give us one MIDI command per callback, which makes processing easier for us.
+
+    // Ignore sysex messages for now
+    if(message.isSysEx())
+        return;
+    
+    // Pull out the raw bytes
+    int dataSize = message.getRawDataSize();
+    if(dataSize <= 0)
+        return;
+    const unsigned char *messageData = message.getRawData();
+	
+    // if logging is active
+    if (loggingActive)
+    {
+        ////////////////////////////////////////////////////////
+        ////////////////////////////////////////////////////////
+        //////////////////// BEGIN LOGGING /////////////////////
+        
+        int midi_channel = (int)(messageData[0]);
+        int midi_number = dataSize > 1 ? (int)(messageData[1]) : 0;
+        int midi_velocity = dataSize > 2 ? (int)(messageData[2]) : 0;
+        timestamp_type timestamp = keyboard_.schedulerCurrentTimestamp();
+        
+        midiLog.write ((char*)&timestamp, sizeof (timestamp_type));
+        midiLog.write ((char*)&midi_channel, sizeof (int));
+        midiLog.write ((char*)&midi_number, sizeof (int));
+        midiLog.write ((char*)&midi_velocity, sizeof (int));
+        
+        ///////////////////// END LOGGING //////////////////////
+        ////////////////////////////////////////////////////////
+        ////////////////////////////////////////////////////////
+    }
+        
+#ifdef MIDI_INPUT_CONTROLLER_DEBUG_RAW
+    if(source == 0)
+        cout << "MIDI Input [internal]: ";
+    else
+        cout << "MIDI Input [" << source->getName() << "]: ";
+	for(int debugPrint = 0; debugPrint < dataSize; debugPrint++)
+		printf("%x ", messageData[debugPrint]);
+	cout << endl;
+#endif /* MIDI_INPUT_CONTROLLER_DEBUG_RAW */
+    
+    ScopedLock ksl(keyboard_.performanceDataMutex_);
+    ScopedLock sl(segmentsMutex_);
+    for(int i = 0; i < segments_.size(); i++) {
+        if(segments_[i]->respondsToMessage(message))
+            segments_[i]->midiHandlerMethod(source, message);
+    }
+}
+
+// Destructor.  Free any existing callbacks
+MidiInputController::~MidiInputController() {
+    if(logFileCreated) {
+        midiLog.close();
+    }
+	disableAllPorts();
+    removeAllSegments();
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/TouchKeys/MidiInputController.h	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,253 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+ 
+  MidiInputController.h: handles incoming MIDI data and manages input
+  ports. Detailed processing is broken down by keyboard segment; see
+  MidiKeyboardSegment.h/cpp for more.
+*/
+
+
+#ifndef MIDI_INPUT_CONTROLLER_H
+#define MIDI_INPUT_CONTROLLER_H
+
+#include <iostream>
+#include <vector>
+#include <map>
+#include <set>
+#include "../JuceLibraryCode/JuceHeader.h"
+#include "PianoKeyboard.h"
+#include "Osc.h"
+#include "MidiKeyboardSegment.h"
+
+using namespace std;
+
+class MidiOutputController;
+
+// MIDI standard messages
+
+enum {
+	kMidiMessageNoteOff = 0x80,
+	kMidiMessageNoteOn = 0x90,
+	kMidiMessageAftertouchPoly = 0xA0,
+	kMidiMessageControlChange = 0xB0,
+	kMidiMessageProgramChange = 0xC0,
+	kMidiMessageAftertouchChannel = 0xD0,
+	kMidiMessagePitchWheel = 0xE0,
+	kMidiMessageSysex = 0xF0,
+	kMidiMessageSysexEnd = 0xF7,
+	kMidiMessageActiveSense = 0xFE,
+	kMidiMessageReset = 0xFF
+};
+
+enum {
+	kMidiControlAllSoundOff = 120,
+	kMidiControlAllControllersOff = 121,
+	kMidiControlLocalControl = 122,
+	kMidiControlAllNotesOff = 123
+};
+
+class MidiInputController : public MidiInputCallback {
+public:
+    /*
+	// Operating modes for MIDI input
+	enum {
+		ModeOff = 0,
+		ModePassThrough,
+		ModeMonophonic,
+		ModePolyphonic,
+		ModeChannelSelect,
+		ModeConstantControllers
+	};
+	
+	// Switch types for Channel Select mode
+	enum {
+		ChannelSelectSwitchTypeUnknown = 0,
+		ChannelSelectSwitchTypeLocation,
+		ChannelSelectSwitchTypeSize,
+		ChannelSelectSwitchTypeNumTouches,
+		ChannelSelectSwitchTypeAngle
+	};
+     */
+	
+public:
+	// Constructor
+	MidiInputController(PianoKeyboard& keyboard);
+
+	
+	// Query available devices
+	vector<pair<int, string> > availableMidiDevices();
+	
+	// Add/Remove MIDI input ports;
+	// Enable methods return true on success (at least one port enabled) 
+	bool enablePort(int portNumber);
+	bool enableAllPorts();
+	void disablePort(int portNumber);
+	void disableAllPorts();
+	vector<int> activePorts();
+
+    //void touchkeyStandaloneTouchBegan(int noteNumber,  Node<KeyTouchFrame>* touchBuffer);
+    //void touchkeyStandaloneTouchEnded(int noteNumber);
+    
+    /*
+	// Set which channels we listen to
+	bool enableChannel(int channelNumber);
+	bool enableAllChannels();
+	void disableChannel(int channelNumber);
+	void disableAllChanels();
+	*/
+    
+	// Set/query the output controller
+	MidiOutputController* midiOutputController() { return midiOutputController_; }
+	void setMidiOutputController(MidiOutputController* ct);
+	
+	// All Notes Off: can be sent by MIDI or controlled programmatically
+	void allNotesOff();
+    
+    // Return the number of keyboard segments, and a specific segment
+    int numSegments() {
+        ScopedLock sl(segmentsMutex_);
+        return segments_.size();
+    }
+    MidiKeyboardSegment* segment(int num) {
+        ScopedLock sl(segmentsMutex_);
+        if(num < 0 || num >= segments_.size())
+            return 0;
+        return segments_[num];
+    }
+    // Return a unique signature which tells us when the MIDI segments have changed,
+    // allowing any listeners to re-query all the segments.
+    int segmentUniqueIdentifier() {
+        return segmentUniqueIdentifier_;
+    }
+
+    // Add a new keyboard segment. Returns a pointer to the newly created segment
+    MidiKeyboardSegment* addSegment(int outputPortNumber, int noteMin = 0, int noteMax = 127, int channelMask = 0xFFFF);
+    
+    // Remove a segment by index or by object
+    void removeSegment(int index);
+    void removeSegment(MidiKeyboardSegment* segment);
+    void removeAllSegments();
+    
+    /*
+	// Change or query the operating mode of the controller
+	int mode() { return mode_; }
+	void setModeOff();
+	void setModePassThrough();
+    void setModeMonophonic();
+	void setModePolyphonic();
+	void setModeChannelSelect(int switchType, int numDivisions, int defaultChannel);
+
+    int polyphony() { return retransmitMaxPolyphony_; }
+    void setPolyphony(int polyphony);
+    bool voiceStealingEnabled() { return useVoiceStealing_; }
+    void setVoiceStealingEnabled(bool enable) { useVoiceStealing_ = enable; }
+    */
+    
+    // Juce MIDI callbacks
+    void handleIncomingMidiMessage(MidiInput* source, const MidiMessage& message);
+    void handlePartialSysexMessage(MidiInput* source,
+                                   const uint8* messageData,
+                                   int numBytesSoFar,
+                                   double timestamp) {}
+	
+	// OSC method: used to get touch callback data from the keyboard
+	// bool oscHandlerMethod(const char *path, const char *types, int numValues, lo_arg **values, void *data);
+    
+    // for logging
+    void createLogFile(string midiLog_filename, string path);
+    void closeLogFile();
+    void startLogging();
+    void stopLogging();
+    
+    bool logFileCreated;
+    bool loggingActive;
+
+	// Destructor
+	~MidiInputController();
+	
+private:
+	// Filtering by channel: return whether this message concerns one of the active channels
+	// we're listening to.
+	// bool messageIsForActiveChannel(const MidiMessage& message);
+	
+    /*
+	// Mode-specific MIDI input handlers
+	void modePassThroughHandler(MidiInput* source, const MidiMessage& message);	
+	void modeMonophonicHandler(MidiInput* source, const MidiMessage& message);
+
+	void modePolyphonicHandler(MidiInput* source, const MidiMessage& message);
+	void modePolyphonicNoteOn(unsigned char note, unsigned char velocity);
+	void modePolyphonicNoteOff(unsigned char note);
+	void modePolyphonicNoteOnCallback(const char *path, const char *types, int numValues, lo_arg **values);
+	
+	void modeChannelSelectHandler(MidiInput* source, const MidiMessage& message);
+	void modeChannelSelectNoteOn(unsigned char note, unsigned char velocity);
+	void modeChannelSelectNoteOff(unsigned char note);
+	void modeChannelSelectNoteOnCallback(const char *path, const char *types, int numValues, lo_arg **values);
+	
+	void modeConstantControllersHandler(MidiInput* source, const MidiMessage& message);
+	
+    // Helper functions for polyphonic mode
+    void modePolyphonicSetupHelper();
+    int oldestNote();
+    int newestNote();
+     */
+    
+	// ***** Member Variables *****
+	
+	PianoKeyboard& keyboard_;						// Reference to main keyboard data
+    MidiOutputController *midiOutputController_;	// Destination for MIDI output
+    
+	map<int, MidiInput*> activePorts_;              // Sources of MIDI data
+    
+    vector<MidiKeyboardSegment*> segments_;         // Segments of the keyboard
+    CriticalSection segmentsMutex_;                 // Mutex protecting the segments list
+    int segmentUniqueIdentifier_;                   // Identifier of when segment structure has changed
+    
+
+    /*
+
+	
+	// Current operating mode of the controller
+	int mode_;
+	
+	// Mapping between input notes and output channels.  Depending on the mode of operation,
+	// each note may be rebroadcast on its own MIDI channel.  Need to keep track of what goes where.
+	// key is MIDI note #, value is output channel (0-15)
+	map<int, int> retransmitChannelForNote_;
+	set<int> retransmitChannelsAvailable_;
+	int retransmitMaxPolyphony_;
+    bool useVoiceStealing_;
+    map<int, timestamp_type> noteOnsetTimestamps_; // When each currently active note began, for stealing
+	
+	// Parameters for Channel Select mode of operation
+	int channelSelectSwitchType_;
+	int channelSelectNumberOfDivisions_;
+	int channelSelectDefaultChannel_;
+	int channelSelectLastOnsetChannel_;
+    */
+    
+    // for logging
+    ofstream midiLog;
+    
+    // for generating timestamps
+    // Scheduler eventScheduler_;
+};
+
+#endif /* MIDI_INPUT_CONTROLLER_H */
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/TouchKeys/MidiKeyboardSegment.cpp	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,1000 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+ 
+  MidiKeyboardSegment.cpp: handles incoming MIDI data and certain input-output
+  mappings for one segment of a keyboard. The keyboard may be divided up into
+  any number of segments with different behaviors. An important role of this
+  class is to manage the output channel allocation when using one MIDI channel
+  per note (for example, to handle polyphonic pitch bend).
+*/
+
+#include "MidiKeyboardSegment.h"
+#include "MidiOutputController.h"
+#include "../Mappings/MappingFactory.h"
+#include "OscMidiConverter.h"
+#include <algorithm>
+#include <string>
+#include <sstream>
+
+const int MidiKeyboardSegment::kMidiControllerDamperPedal = 64;
+const int MidiKeyboardSegment::kPedalActiveValue = 64;
+
+// Constructor
+MidiKeyboardSegment::MidiKeyboardSegment(PianoKeyboard& keyboard)
+: keyboard_(keyboard), outputPortNumber_(0), mappingFactorySplitter_(keyboard),
+  mappingFactoryUniqueIdentifier_(0),
+  mode_(ModeOff), channelMask_(0),
+  noteMin_(0), noteMax_(127), outputChannelLowest_(0), outputTransposition_(0),
+  damperPedalEnabled_(true), touchkeyStandaloneMode_(false),
+  usesKeyboardChannelPressure_(false), usesKeyboardPitchWheel_(true), usesKeyboardMidiControllers_(true),
+  pitchWheelRange_(2.0), useVoiceStealing_(false)
+{
+	// Register for OSC messages from the internal keyboard source
+	setOscController(&keyboard_);
+    keyboard_.setMappingFactory(this, &mappingFactorySplitter_);
+    
+    setAllControllerActionsTo(kControlActionBlock);
+    resetControllerValues();
+    
+    for(int i = 0; i < 128; i++)
+        noteOnsetTimestamps_[i] = 0;
+}
+
+// Destructor
+MidiKeyboardSegment::~MidiKeyboardSegment() {
+    removeAllMappingFactories();
+    keyboard_.removeMappingFactory(this);
+}
+
+bool MidiKeyboardSegment::respondsToMessage(const MidiMessage& message) {
+    int channel = message.getChannel();
+
+    // If the message is not something universal, check if it matches our channel
+    if(channel > 0) {
+        if(!(channelMask_ && (1 << (channel - 1))))
+            return false;
+    }
+    
+    // If the message has a note number, see if it's in range
+    if(message.isNoteOn() || message.isNoteOff() || message.isAftertouch()) {
+        int noteNumber = message.getNoteNumber();
+        if(noteNumber < noteMin_ || noteNumber > noteMax_)
+            return false;
+    }
+    
+    return true;
+}
+
+bool MidiKeyboardSegment::respondsToNote(int noteNumber) {
+    if(noteNumber < noteMin_ || noteNumber > noteMax_)
+        return false;
+    return true;
+}
+
+// Listen on a given MIDI channel
+void MidiKeyboardSegment::enableChannel(int channelNumber) {
+    if(channelNumber >= 0 && channelNumber < 16)
+        channelMask_ |= (1 << channelNumber);
+}
+
+// Listen on all MIDI channels
+void MidiKeyboardSegment::enableAllChannels() {
+    channelMask_ = 0xFFFF;
+}
+
+// Disable listening to a specific MIDI channel
+void MidiKeyboardSegment::disableChannel(int channelNumber) {
+    if(channelNumber >= 0 && channelNumber < 16)
+        channelMask_ &= ~(1 << channelNumber);
+}
+
+// Disable all MIDI channels
+void MidiKeyboardSegment::disableAllChanels() {
+    channelMask_ = 0;
+}
+
+// Set the range of notes we listen to. Sets the range to between
+// minNote and maxNote, inclusive.
+void MidiKeyboardSegment::setNoteRange(int minNote, int maxNote) {
+    // Sanity check
+    if(minNote > maxNote)
+        return;
+    if(minNote < 0)
+        noteMin_ = 0;
+    else if(minNote > 127)
+        noteMin_ = 127;
+    else
+        noteMin_ = minNote;
+
+    if(maxNote < 0)
+        noteMax_ = 0;
+    else if(maxNote > 127)
+        noteMax_ = 127;
+    else
+        noteMax_ = maxNote;
+    
+}
+
+// Set the MIDI pitch wheel range
+void MidiKeyboardSegment::setMidiPitchWheelRange(float semitones, bool send) {
+    pitchWheelRange_ = semitones;
+    
+    if(send)
+        sendMidiPitchWheelRange();
+}
+
+// Send the MIDI pitch wheel range RPN
+// If in polyphonic mode, send to all channels; otherwise send only
+// to the channel in question.
+void MidiKeyboardSegment::sendMidiPitchWheelRange() {
+    if(mode_ == ModePolyphonic) {
+        for(int i = outputChannelLowest_; i < outputChannelLowest_ + retransmitMaxPolyphony_; i++)
+            sendMidiPitchWheelRangeHelper(i);
+    }
+    else
+        sendMidiPitchWheelRangeHelper(outputChannelLowest_);
+}
+
+
+// Enable TouchKeys standalone mode (no MIDI input, touch triggers note)
+void MidiKeyboardSegment::enableTouchkeyStandaloneMode() {
+    if(touchkeyStandaloneMode_)
+        return;
+    
+    addOscListener("/touchkeys/on");
+    addOscListener("/touchkeys/off");
+    touchkeyStandaloneMode_ = true;
+}
+
+// Disable TouchKeys standalone mode (no MIDI input, touch triggers note)
+void MidiKeyboardSegment::disableTouchkeyStandaloneMode() {
+    if(!touchkeyStandaloneMode_)
+        return;
+
+    removeOscListener("/touchkeys/on");
+    removeOscListener("/touchkeys/off");
+    touchkeyStandaloneMode_ = false;
+}
+
+// Disable any currently active notes
+void MidiKeyboardSegment::allNotesOff() {
+	// TODO: implement me
+}
+
+// Reset controller values to defaults
+void MidiKeyboardSegment::resetControllerValues() {
+    // Most controls default to 0
+    for(int i = 0; i < kControlMax; i++)
+        controllerValues_[i] = 0;
+    // ...except pitch wheel, which defaults to center
+    controllerValues_[kControlPitchWheel] = 8192;
+}
+
+// Set the operating mode of the controller.  The mode determines the behavior in
+// response to incoming MIDI data.
+
+void MidiKeyboardSegment::setMode(int mode) {
+    if(mode == ModePassThrough)
+        setModePassThrough();
+    else if(mode == ModeMonophonic)
+        setModeMonophonic();
+    else if(mode == ModePolyphonic)
+        setModePolyphonic();
+    else
+        setModeOff();
+}
+
+void MidiKeyboardSegment::setModeOff() {
+	allNotesOff();
+	removeAllOscListeners();
+    setAllControllerActionsTo(kControlActionBlock);
+	mode_ = ModeOff;
+}
+
+void MidiKeyboardSegment::setModePassThrough() {
+	allNotesOff();
+	removeAllOscListeners();
+    setAllControllerActionsTo(kControlActionPassthrough);
+	mode_ = ModePassThrough;
+}
+
+void MidiKeyboardSegment::setModeMonophonic() {
+	allNotesOff();
+	removeAllOscListeners();
+    setAllControllerActionsTo(kControlActionPassthrough);
+	mode_ = ModeMonophonic;
+}
+
+void MidiKeyboardSegment::setModePolyphonic() {
+	// First turn off any notes in the current mode
+	allNotesOff();
+	removeAllOscListeners();
+    setAllControllerActionsTo(kControlActionBroadcast);
+	
+	// Register a callback for touchkey data.  When we get a note-on message,
+	// we request this callback occur once touch data is available.  In this mode,
+	// we know the eventual channel before any touch data ever occurs: thus, we
+	// only listen to the MIDI onset itself, which happens after all the touch
+	// data is sent out.
+	addOscListener("/midi/noteon");
+    
+	mode_ = ModePolyphonic;
+	
+    if(retransmitMaxPolyphony_ < 1)
+        retransmitMaxPolyphony_ = 1;
+    modePolyphonicSetupHelper();
+}
+
+// Set the maximum polyphony, affecting polyphonic mode only
+void MidiKeyboardSegment::setPolyphony(int polyphony) {
+    // First turn off any notes if this affects current polyphonic mode
+    // (other modes unaffected so we can make these changes in background)
+    if(mode_ == ModePolyphonic)
+        allNotesOff();
+    
+    if(polyphony < 1)
+        retransmitMaxPolyphony_ = 1;
+    else if(polyphony > 16)
+        retransmitMaxPolyphony_ = 16;
+    else
+        retransmitMaxPolyphony_ = polyphony;
+    modePolyphonicSetupHelper();
+}
+
+// Set whether the damper pedal is enabled or not
+void MidiKeyboardSegment::setDamperPedalEnabled(bool enable) {
+    if(damperPedalEnabled_ && !enable) {
+        // Pedal was enabled before, now it isn't. Clear out any notes
+        // currently in the pedal so they can be retaken.
+        damperPedalWentOff();
+    }
+    
+    damperPedalEnabled_ = enable;
+}
+
+// Handle an incoming MIDI message
+void MidiKeyboardSegment::midiHandlerMethod(MidiInput* source, const MidiMessage& message) {
+    // Log the timestamps of note onsets and releases, regardless of the mode
+    // of processing
+    if(message.isNoteOn()) {
+        if(message.getNoteNumber() >= 0 && message.getNoteNumber() < 128)
+            noteOnsetTimestamps_[message.getNoteNumber()] = keyboard_.schedulerCurrentTimestamp();
+    }
+    else if(message.isNoteOff()) {
+        // Remove the onset timestamp unless we have the specific condition:
+        // (damper pedal enabled) && (pedal is down) && (polyphonic mode)
+        // In this condition, onsets will be removed when note goes off
+        if(message.getNoteNumber() >= 0 && message.getNoteNumber() < 128) {
+            if(!damperPedalEnabled_ || controllerValues_[kMidiControllerDamperPedal] < kPedalActiveValue || mode_ != ModePolyphonic) {
+                noteOnsetTimestamps_[message.getNoteNumber()] = 0;
+            }
+        }
+    }
+    else if(message.isAllNotesOff() || message.isAllSoundOff()) {
+        for(int i = 0; i < 128; i++)
+            noteOnsetTimestamps_[i] = 0;
+    }
+    
+    // Log the values of incoming control changes in case mappings need to use them later
+    if(message.isController() && !(message.isAllNotesOff() || message.isAllSoundOff())) {
+        // Handle damper pedal specially: it may affect note allocation
+        if(message.getControllerNumber() == kMidiControllerDamperPedal) {
+            if(message.getControllerValue() < kPedalActiveValue &&
+               controllerValues_[kMidiControllerDamperPedal] >= kPedalActiveValue) {
+                damperPedalWentOff();
+            }
+        }
+        
+        if(message.getControllerNumber() >= 0 && message.getControllerNumber() < 128) {
+            if(usesKeyboardMidiControllers_) {
+                controllerValues_[message.getControllerNumber()] = message.getControllerValue();
+                handleControlChangeRetransit(message.getControllerNumber(), message);
+            }
+        }
+    }
+    else if(message.isChannelPressure()) {
+        if(usesKeyboardChannelPressure_) {
+            controllerValues_[kControlChannelAftertouch] = message.getChannelPressureValue();
+            handleControlChangeRetransit(kControlChannelAftertouch, message);
+        }
+    }
+    else if(message.isPitchWheel()) {
+        if(usesKeyboardPitchWheel_) {
+            controllerValues_[kControlPitchWheel] = message.getPitchWheelValue();
+            handleControlChangeRetransit(kControlPitchWheel, message);
+        }
+    }
+    else {
+        // Process the message differently depending on the current mode
+        switch(mode_) {
+            case ModePassThrough:
+                modePassThroughHandler(source, message);
+                break;
+            case ModeMonophonic:
+                modeMonophonicHandler(source, message);
+                break;
+            case ModePolyphonic:
+                modePolyphonicHandler(source, message);
+                break;
+            case ModeOff:
+            default:
+                // Ignore message
+                break;
+        }
+    }
+}
+
+// OscHandler method which parses incoming OSC messages we've registered for.  In this case,
+// we use OSC callbacks to find out about touch data for notes we want to trigger.
+
+bool MidiKeyboardSegment::oscHandlerMethod(const char *path, const char *types, int numValues, lo_arg **values, void *data) {
+    if(touchkeyStandaloneMode_) {
+        if(!strcmp(path, "/touchkeys/on") && numValues > 0) {
+            int noteNumber = values[0]->i;
+            if(noteNumber >= 0 && noteNumber < 128) {
+                // Generate MIDI note on for this message
+                MidiMessage msg(MidiMessage::noteOn(1, noteNumber, (uint8)64));
+                midiHandlerMethod(0, msg);
+            }
+            return true;
+        }
+        else if(!strcmp(path, "/touchkeys/off") && numValues > 0) {
+            int noteNumber = values[0]->i;
+            if(noteNumber >= 0 && noteNumber < 128) {
+                // Generate MIDI note off for this message
+                MidiMessage msg(MidiMessage::noteOff(1, noteNumber));
+                midiHandlerMethod(0, msg);
+            }
+            return true;
+        }
+    }
+    
+	if(mode_ == ModePolyphonic) {
+		modePolyphonicNoteOnCallback(path, types, numValues, values);
+	}
+	
+	return true;
+}
+
+// Acquire an OSC-MIDI converter. If a converter for this control already exists,
+// return it. If not, create it. This method keeps track of how many objects have
+// acquired the converter. When all acquirers have released ihe converter, it is
+// destroyed.
+OscMidiConverter* MidiKeyboardSegment::acquireOscMidiConverter(int controlId) {
+    OscMidiConverter *converter;
+    
+    if(oscMidiConverters_.count(controlId) == 0) {
+        converter = new OscMidiConverter(keyboard_, *this, controlId);
+        converter->setMidiOutputController(midiOutputController_);
+        
+        oscMidiConverters_[controlId] = converter;
+        oscMidiConverterReferenceCounts_[controlId] = 1;
+    }
+    else {
+        if(oscMidiConverterReferenceCounts_.count(controlId) == 0) {
+            cerr << "BUG: found a converter with missing reference counter\n";
+        }
+        else if(oscMidiConverterReferenceCounts_[controlId] <= 0) {
+            cerr << "BUG: found a converter with no references\n";
+            oscMidiConverterReferenceCounts_[controlId] = 1;
+        }
+        else
+            oscMidiConverterReferenceCounts_[controlId]++;
+        converter = oscMidiConverters_[controlId];
+    }
+    
+    return converter;
+}
+
+// Release a previously acquired OSC-MIDI converter. This call must be paired with
+// acquireOscMidiConverter.
+void MidiKeyboardSegment::releaseOscMidiConverter(int controlId) {
+    if(oscMidiConverters_.count(controlId) == 0 ||
+       oscMidiConverterReferenceCounts_.count(controlId) == 0) {
+        cerr << "BUG: releasing a missing OSC-MIDI converter\n";
+        return;
+    }
+    
+    oscMidiConverterReferenceCounts_[controlId]--;
+    if(oscMidiConverterReferenceCounts_[controlId] == 0) {
+        // This was the last release of this converter. Delete it.
+        OscMidiConverter *converter = oscMidiConverters_[controlId];
+        delete converter;
+        oscMidiConverters_.erase(controlId);
+        oscMidiConverterReferenceCounts_.erase(controlId);
+    }
+}
+
+// Helper predicate function for filtering strings
+inline bool char_is_not_alphanumeric(int c) {
+    return !std::isalnum(c);
+}
+
+// Create a new mapping factory for this segment. A pointer should be passed in
+// of a newly-allocated object. It will be released upon removal.
+void MidiKeyboardSegment::addMappingFactory(MappingFactory* factory, bool autoGenerateName) {
+    // Check for duplicates-- can't add the same factory twice
+    vector<MappingFactory*>::iterator it;
+    for(it = mappingFactories_.begin(); it != mappingFactories_.end(); ++it) {
+        if(*it == factory)
+            return;
+    }
+    
+    // If the name should autogenerate, find a unique name for this factory
+    if(autoGenerateName) {
+        std::string baseName = factory->factoryTypeName();
+        
+        // Remove spaces, newlines, other invalid characters, leaving only alphanumerics
+        baseName.erase(std::remove_if(baseName.begin(), baseName.end(), char_is_not_alphanumeric),
+                       baseName.end());
+        std::transform(baseName.begin(), baseName.end(), baseName.begin(), ::tolower);
+
+        // Now look for an OSC path with this name. If found, add a number to the end, incrementing
+        // it until a unique name is found
+        std::string name = baseName;
+        bool isUnique = false;
+        int appendDigit = 2;
+        
+        while(!isUnique) {
+            isUnique = true;
+            
+            for(int i = 0; i < mappingFactories_.size(); i++) {
+                std::string compareName = mappingFactories_[i]->getName();
+                int lastSeparator = compareName.find_last_of('/');
+                if((lastSeparator != std::string::npos) && (lastSeparator < compareName.size() - 1))
+                    compareName = compareName.substr(lastSeparator + 1);
+                
+                if(name == compareName) {
+                    isUnique = false;
+                    
+                    // Try a new name with a new digit at the end...
+                    std::stringstream ss;
+                    ss << baseName << appendDigit;
+                    name = ss.str();
+                    appendDigit++;
+                    
+                    break;
+                }
+            }
+        }
+        
+        factory->setName(name);
+    }
+    
+    // Add factory to internal vector, and add it to splitter class
+    mappingFactories_.push_back(factory);
+    mappingFactorySplitter_.addFactory(factory);
+    mappingFactoryUniqueIdentifier_++;
+}
+
+// Remove a mapping factory, releasing the associated object.
+void MidiKeyboardSegment::removeMappingFactory(MappingFactory* factory) {
+    vector<MappingFactory*>::iterator it;
+    
+    for(it = mappingFactories_.begin(); it != mappingFactories_.end(); ++it) {
+        if(*it == factory) {
+            mappingFactorySplitter_.removeFactory(factory);
+            delete factory;
+            mappingFactories_.erase(it);
+            break;
+        }
+    }
+    
+    mappingFactoryUniqueIdentifier_++;
+}
+
+// Remove all mapping factories, releasing each one
+void MidiKeyboardSegment::removeAllMappingFactories() {
+    vector<MappingFactory*>::iterator it;
+    
+    mappingFactorySplitter_.removeAllFactories();
+    for(it = mappingFactories_.begin(); it != mappingFactories_.end(); ++it) {
+        delete *it;
+    }
+    
+    mappingFactories_.clear();
+    mappingFactoryUniqueIdentifier_++;
+}
+
+// Return a list of current mapping factories.
+vector<MappingFactory*> const& MidiKeyboardSegment::mappingFactories(){
+    return mappingFactories_;
+}
+
+// Mode-specific MIDI handlers.  These methods handle incoming MIDI data according to the rules
+// defined by a particular mode of operation.
+
+// Pass-Through: Retransmit any input data to the output unmodified.
+void MidiKeyboardSegment::modePassThroughHandler(MidiInput* source, const MidiMessage& message) {
+    // Check if there is a note on or off, and update the keyboard class accordingly
+    if(message.isNoteOn()) {
+        int note = message.getNoteNumber();
+        if(keyboard_.key(note) != 0)
+            keyboard_.key(note)->midiNoteOn(this, message.getVelocity(), message.getChannel() - 1,
+                                            keyboard_.schedulerCurrentTimestamp());
+        
+        // Retransmit, possibly with transposition
+        if(midiOutputController_ != 0) {
+            MidiMessage newMessage = MidiMessage::noteOn(message.getChannel(), message.getNoteNumber() + outputTransposition_, message.getVelocity());
+            midiOutputController_->sendMessage(outputPortNumber_, newMessage);
+        }
+    }
+    else if(message.isNoteOff()) {
+        int note = message.getNoteNumber();
+        if(keyboard_.key(note) != 0)
+            keyboard_.key(note)->midiNoteOff(this, keyboard_.schedulerCurrentTimestamp());
+        
+        // Retransmit, possibly with transposition
+        if(midiOutputController_ != 0) {
+            MidiMessage newMessage = MidiMessage::noteOff(message.getChannel(), message.getNoteNumber() + outputTransposition_);
+            midiOutputController_->sendMessage(outputPortNumber_, newMessage);
+        }
+    }
+    else if(message.isAftertouch()) { // Polyphonic aftertouch: adjust to transposition
+        if(midiOutputController_ != 0) {
+            MidiMessage newMessage = MidiMessage::aftertouchChange(message.getChannel(), message.getNoteNumber() + outputTransposition_, message.getAfterTouchValue());
+            midiOutputController_->sendMessage(outputPortNumber_, newMessage);
+        }
+    }
+    else if(midiOutputController_ != 0) // Anything else goes through unchanged
+        midiOutputController_->sendMessage(outputPortNumber_, message);
+}
+
+// Monophonic: all MIDI messages pass through to the output, which is assumed to be a monosynth.
+// However the most recent key which determines the currently sounding note will have its mapping
+// active; all others are suspended.
+void MidiKeyboardSegment::modeMonophonicHandler(MidiInput* source, const MidiMessage& message) {
+    if(message.isNoteOn()) {
+        // First suspend any other mappings. This might include the current note if the touch
+        // data has caused a mapping to be created.
+        if(keyboard_.mappingFactory(this) != 0) {
+            keyboard_.mappingFactory(this)->suspendAllMappings();
+        }
+        
+        // And turn on note on MIDI controller
+        if(midiOutputController_ != 0) {
+            MidiMessage newMessage = MidiMessage::noteOn(message.getChannel(), message.getNoteNumber() + outputTransposition_, message.getVelocity());
+            midiOutputController_->sendMessage(outputPortNumber_, newMessage);
+        }
+        
+        // Now start the next one
+        int note = message.getNoteNumber();
+        if(keyboard_.key(note) != 0)
+            keyboard_.key(note)->midiNoteOn(this, message.getVelocity(),
+                                            message.getChannel() - 1, keyboard_.schedulerCurrentTimestamp());
+        
+        // Now resume the current note's mapping
+        if(keyboard_.mappingFactory(this) != 0) {
+            keyboard_.mappingFactory(this)->resumeMapping(note, true);
+        }
+    }
+    else if(message.isNoteOff()) {
+        // First stop this note
+        int note = message.getNoteNumber();
+        if(keyboard_.key(note) != 0)
+            keyboard_.key(note)->midiNoteOff(this, keyboard_.schedulerCurrentTimestamp());
+        
+        // Then reactivate the most recent note's mappings
+        if(keyboard_.mappingFactory(this) != 0) {
+            int newest = newestNote();
+            if(newest >= 0)
+                keyboard_.mappingFactory(this)->resumeMapping(newest, true);
+        }
+        
+        // And turn off note on MIDI controller
+        if(midiOutputController_ != 0) {
+            MidiMessage newMessage = MidiMessage::noteOff(message.getChannel(), message.getNoteNumber() + outputTransposition_, message.getVelocity());
+            midiOutputController_->sendMessage(outputPortNumber_, newMessage);
+        }
+    }
+}
+
+// Polyphonic: Each incoming note gets its own unique MIDI channel so its controllers
+// can be manipulated separately (e.g. by touchkey data).  Keep track of available channels
+// and currently active notes.
+void MidiKeyboardSegment::modePolyphonicHandler(MidiInput* source, const MidiMessage& message) {
+	
+    if(message.getRawDataSize() <= 0)
+        return;
+    const unsigned char *rawData = message.getRawData();
+    
+	if(rawData[0] == kMidiMessageReset) {
+		// Reset state and pass along to all relevant channels
+		
+		retransmitChannelForNote_.clear();	// Clear current note information
+		retransmitChannelsAvailable_.clear();
+        retransmitNotesHeldInPedal_.clear();
+		for(int i = 0; i < retransmitMaxPolyphony_; i++) {
+			retransmitChannelsAvailable_.insert(i);
+		}
+		if(midiOutputController_ != 0)
+			midiOutputController_->sendReset(outputPortNumber_);
+	}
+    else if(message.isNoteOn()) {
+        if(retransmitChannelForNote_.count(message.getNoteNumber()) > 0
+           && retransmitNotesHeldInPedal_.count(message.getNoteNumber()) == 0) {
+            // Case (2)-- retrigger an existing note
+            if(midiOutputController_ != 0) {
+                midiOutputController_->sendNoteOn(outputPortNumber_, retransmitChannelForNote_[message.getNoteNumber()],
+                                                  message.getNoteNumber() + outputTransposition_, message.getVelocity());
+            }
+        }
+        else {
+            // New note
+            modePolyphonicNoteOn(message.getNoteNumber(), message.getVelocity());
+        }
+    }
+    else if(message.isNoteOff()) {
+        modePolyphonicNoteOff(message.getNoteNumber());
+    }
+    else if(message.isAllNotesOff() || message.isAllSoundOff()) {
+        retransmitChannelForNote_.clear();	// Clear current note information
+        retransmitChannelsAvailable_.clear();
+        retransmitNotesHeldInPedal_.clear();
+        for(int i = 0; i < retransmitMaxPolyphony_; i++)
+            retransmitChannelsAvailable_.insert(i);
+    }
+    else if(message.isAftertouch()) { // polyphonic aftertouch
+        if(retransmitChannelForNote_.count(message.getNoteNumber()) > 0) {
+            int retransmitChannel = retransmitChannelForNote_[message.getNoteNumber()];
+            if(midiOutputController_ != 0) {
+                midiOutputController_->sendAftertouchPoly(outputPortNumber_, retransmitChannel,
+                                                          message.getNoteNumber() + outputTransposition_, message.getAfterTouchValue());
+            }
+        }
+    }
+}
+
+// Handle note on message in polyphonic mode.  Allocate a new channel
+// for this note and rebroadcast it.
+void MidiKeyboardSegment::modePolyphonicNoteOn(unsigned char note, unsigned char velocity) {
+    int newChannel = -1;
+    
+    if(retransmitNotesHeldInPedal_.count(note) > 0) {
+        // For notes that are still sounding in the pedal, reuse the same MIDI channel
+        // they had before.
+        if(retransmitChannelForNote_.count(note) > 0)
+            newChannel = retransmitChannelForNote_[note];
+        else {
+            cout << "BUG: note " << note << " held in pedal but has no channel\n";
+            retransmitNotesHeldInPedal_.erase(note);
+            return;
+        }
+        
+        // No longer held in pedal: it will be an active note again with the same channel
+        retransmitNotesHeldInPedal_.erase(note);
+    }
+    else {
+        // Otherwise, allocate a new channel to this note
+        if(retransmitChannelsAvailable_.size() == 0) {
+            if(damperPedalEnabled_) {
+                // First priority is always to take a note that is being sustained
+                // in the pedal but not actively held. This is true whether or not
+                // voice stealing is enabled.
+                int oldNote = oldestNoteInPedal();
+                int oldChannel = -1;
+                if(retransmitChannelForNote_.count(oldNote) > 0)
+                    oldChannel = retransmitChannelForNote_[oldNote];
+                if(oldNote >= 0) {
+                    cout << "Stealing note " << oldNote << " from pedal for note " << (int)note << endl;
+                    modePolyphonicNoteOff(oldNote, true);
+                    if(oldChannel >= 0) {
+                        //midiOutputController_->sendControlChange(outputPortNumber_, oldChannel, kMidiControllerDamperPedal, 0);
+                        //midiOutputController_->sendControlChange(outputPortNumber_, oldChannel, kMidiControlAllNotesOff, 0);
+                        //midiOutputController_->sendControlChange(outputPortNumber_, oldChannel, kMidiControlAllSoundOff, 0);
+                        //midiOutputController_->sendControlChange(outputPortNumber_, oldChannel, kMidiControllerDamperPedal,
+                        //                                         controllerValues_[kMidiControllerDamperPedal]);
+                    }
+                }
+            }
+            
+            // Now try again...
+            if(retransmitChannelsAvailable_.size() == 0) {
+                if(useVoiceStealing_) {
+                    // Find the voice with the oldest timestamp and turn it off
+                    int oldNote = oldestNote();
+                    int oldChannel = -1;
+                    if(retransmitChannelForNote_.count(oldNote) > 0)
+                        oldChannel = retransmitChannelForNote_[oldNote];
+                    if(oldNote < 0) {
+                        // Shouldn't happen...
+                        cout << "No notes present, but no MIDI output channel available for note " << (int)note << endl;
+                        return;
+                    }
+                    cout << "Stealing note " << oldNote << " for note " << (int)note << endl;
+                    modePolyphonicNoteOff(oldNote, true);
+                    if(oldChannel >= 0) {
+                        //midiOutputController_->sendControlChange(outputPortNumber_, oldChannel, kMidiControllerDamperPedal, 0);
+                        //midiOutputController_->sendControlChange(outputPortNumber_, oldChannel, kMidiControlAllNotesOff, 0);
+                        //midiOutputController_->sendControlChange(outputPortNumber_, oldChannel, kMidiControlAllSoundOff, 0);
+                        //midiOutputController_->sendControlChange(outputPortNumber_, oldChannel, kMidiControllerDamperPedal,
+                        //                                         controllerValues_[kMidiControllerDamperPedal]);
+                    }
+                }
+                else {
+                    // No channels available.  Print a warning and finish
+                    cout << "No MIDI output channel available for note " << (int)note << endl;
+                    return;
+                }
+            }
+        }
+        
+        // Request the first available channel
+        newChannel = *retransmitChannelsAvailable_.begin();
+        retransmitChannelsAvailable_.erase(newChannel);
+        retransmitChannelForNote_[note] = newChannel;
+	}
+    
+	if(keyboard_.key(note) != 0) {
+		keyboard_.key(note)->midiNoteOn(this, velocity, newChannel, keyboard_.schedulerCurrentTimestamp());
+	}
+	
+	// The above function will cause a callback to be generated, which in turn will generate
+	// the Note On message.
+}
+
+// Handle note off message in polyphonic mode.  Release any channel
+// associated with this note.
+void MidiKeyboardSegment::modePolyphonicNoteOff(unsigned char note, bool forceOff) {    
+	// If no channel associated with this note, ignore it
+	if(retransmitChannelForNote_.count(note) == 0) {
+        if(note >= 0 && note < 128)
+            noteOnsetTimestamps_[note] = 0;
+		return;
+    }
+    
+	if(keyboard_.key(note) != 0) {
+		keyboard_.key(note)->midiNoteOff(this, keyboard_.schedulerCurrentTimestamp());
+	}
+	
+	// Send a Note Off message to the appropriate channel
+	if(midiOutputController_ != 0) {
+		midiOutputController_->sendNoteOff(outputPortNumber_, retransmitChannelForNote_[note], note + outputTransposition_);
+	}
+	
+    // If the pedal is enabled and currently active, don't re-enable this channel
+    // just yet. Instead, let the note continue ringing until we have to steal it later.
+    if(damperPedalEnabled_ && controllerValues_[kMidiControllerDamperPedal] >= kPedalActiveValue && !forceOff) {
+        retransmitNotesHeldInPedal_.insert(note);
+    }
+    else {
+        // Otherwise release the channel mapping associated with this note
+        if(retransmitNotesHeldInPedal_.count(note) > 0)
+            retransmitNotesHeldInPedal_.erase(note);
+        retransmitChannelsAvailable_.insert(retransmitChannelForNote_[note]);
+        retransmitChannelForNote_.erase(note);
+        if(note >= 0 && note < 128)
+            noteOnsetTimestamps_[note] = 0;
+    }
+}
+
+// Callback function after we request a note on.  PianoKey class will respond
+// with touch data (if available within a specified timeout), or with a frame
+// indicating an absence of touch data.  Once we receive this, we can send the
+// MIDI note on message.
+
+void MidiKeyboardSegment::modePolyphonicNoteOnCallback(const char *path, const char *types, int numValues, lo_arg **values) {
+	if(numValues < 3)	// Sanity check: first 3 values hold MIDI information
+		return;
+	if(types[0] != 'i' || types[1] != 'i' || types[2] != 'i')
+		return;
+	
+	int midiNote = values[0]->i;
+	int midiChannel = values[1]->i;
+	int midiVelocity = values[2]->i;
+	
+	if(midiNote < 0 || midiNote > 127)
+		return;
+    // If there are multiple segments of the keyboard active, there may be OSC
+    // messages generated from keys that didn't come from us. Don't grab them by mistake.
+    // FIXME: the real fix here is to include a source ID with the OSC message
+    if(!respondsToNote(midiNote))
+        return;
+	
+	// Send the Note On message to the correct channel
+	if(midiOutputController_ != 0) {
+		midiOutputController_->sendNoteOn(outputPortNumber_, midiChannel, midiNote + outputTransposition_, midiVelocity);
+	}
+}
+
+// Private helper method to handle changes in polyphony
+void MidiKeyboardSegment::modePolyphonicSetupHelper() {
+    if(retransmitMaxPolyphony_ > 16)
+		retransmitMaxPolyphony_ = 16;	// Limit polyphony to 16 (number of MIDI channels
+    retransmitChannelsAvailable_.clear();
+	for(int i = outputChannelLowest_; i < outputChannelLowest_ + retransmitMaxPolyphony_; i++)
+		retransmitChannelsAvailable_.insert(i);
+	retransmitChannelForNote_.clear();
+    retransmitNotesHeldInPedal_.clear();
+}
+
+// Find the oldest onset of the currently playing notes. Used for voice stealing.
+// Returns -1 if no notes are playing.
+int MidiKeyboardSegment::oldestNote() {
+    int oldestNoteNumber = -1;
+    timestamp_type oldestTimestamp = missing_value<timestamp_type>::missing();
+    
+    for(int i = 0; i < 128; i++) {
+        if(missing_value<timestamp_type>::isMissing(oldestTimestamp) && noteOnsetTimestamps_[i] != 0) {
+            oldestNoteNumber = i;
+            oldestTimestamp = noteOnsetTimestamps_[i];
+        }
+        else if(noteOnsetTimestamps_[i] < oldestTimestamp && noteOnsetTimestamps_[i] != 0) {
+            oldestNoteNumber = i;
+            oldestTimestamp = noteOnsetTimestamps_[i];
+        }
+    }
+    
+    return oldestNoteNumber;
+}
+
+// Finds the oldest onset of the notes currently finished but sustaining in the pedal.
+// Used for voice stealing. Returns -1 if no notes are held in the pedal.
+int MidiKeyboardSegment::oldestNoteInPedal() {
+    if(!damperPedalEnabled_ || retransmitNotesHeldInPedal_.empty())
+        return -1;
+    
+    set<int>::iterator it;
+    int oldestNoteNumber = -1;
+    timestamp_type oldestTimestamp = missing_value<timestamp_type>::missing();
+    
+    cout << "notes in pedal: ";
+    
+    for(it = retransmitNotesHeldInPedal_.begin(); it != retransmitNotesHeldInPedal_.end(); ++it) {
+        int note = *it;
+        timestamp_type timestamp;
+        if(noteOnsetTimestamps_[note] != 0)
+            timestamp = noteOnsetTimestamps_[note];
+        else
+            timestamp = 0; // Why is there a note held in pedal with no onset? Steal it!
+        cout << note << " (" << timestamp << ") ";
+        
+        if(missing_value<timestamp_type>::isMissing(oldestTimestamp)) {
+            oldestNoteNumber = note;
+            oldestTimestamp = timestamp;
+        }
+        else if(timestamp < oldestTimestamp) {
+            oldestNoteNumber = note;
+            oldestTimestamp = timestamp;
+        }
+    }
+    cout << endl;
+    
+    return oldestNoteNumber;
+}
+
+// Find the newest onset of the currently playing notes. Used for monophonic mode.
+// Returns -1 if no notes are playing.
+int MidiKeyboardSegment::newestNote() {
+    int newestNoteNumber = -1;
+    timestamp_type newestTimestamp = missing_value<timestamp_type>::missing();
+    
+    for(int i = 0; i < 128; i++) {
+        if(missing_value<timestamp_type>::isMissing(newestTimestamp) && noteOnsetTimestamps_[i] != 0) {
+            newestNoteNumber = i;
+            newestTimestamp = noteOnsetTimestamps_[i];
+        }
+        else if(noteOnsetTimestamps_[i] > newestTimestamp && noteOnsetTimestamps_[i] != 0) {
+            newestNoteNumber = i;
+            newestTimestamp = noteOnsetTimestamps_[i];
+        }
+    }
+    
+    return newestNoteNumber;
+}
+
+// Given a controller number (including special "controllers" channel-pressure and pitch-wheel),
+// retransit or not to outgoing MIDI channels depending on the current behaviour defined in
+// controllerActions_.
+void MidiKeyboardSegment::handleControlChangeRetransit(int controllerNumber, const MidiMessage& message) {
+    if(midiOutputController_ == 0)
+        return;
+    if(controllerActions_[controllerNumber] == kControlActionPassthrough) {
+        // Tell OSC-MIDI converter to resend if present, otherwise pass through
+        if(oscMidiConverters_.count(controllerNumber) != 0) {
+            oscMidiConverters_[controllerNumber]->resend(message.getChannel() - 1);
+        }
+        else {
+            // KLUDGE
+            if(controllerNumber == 64) {
+                MidiMessage newMessage = MidiMessage::controllerEvent(message.getChannel(), 67, 127 - message.getControllerValue());
+                midiOutputController_->sendMessage(outputPortNumber_, newMessage);
+            }
+            else {
+                // Send this control change through unchanged
+                midiOutputController_->sendMessage(outputPortNumber_, message);
+            }
+        }
+    }
+    else if(controllerActions_[controllerNumber] == kControlActionBroadcast) {
+        // Send this control change to all active channels
+        MidiMessage newMessage(message); // Modifiable copy of the original message
+        
+        if(oscMidiConverters_.count(controllerNumber) != 0) {
+            for(int i = 0; i < retransmitMaxPolyphony_; i++)
+                oscMidiConverters_[controllerNumber]->resend(i);
+        }
+        else {
+            for(int i = 0; i < retransmitMaxPolyphony_; i++) {
+                newMessage.setChannel(i + 1); // Juce uses 1-16, we use 0-15
+                midiOutputController_->sendMessage(outputPortNumber_, newMessage);
+            }
+        }
+    }
+    else if(controllerActions_[controllerNumber] == kControlActionSendToLatest) {
+        // Send this control change to the channel of the most recent note
+        int noteNumber = newestNote();
+        if(noteNumber < 0)
+            return;
+        if(keyboard_.key(noteNumber) != 0) {
+            int channel = keyboard_.key(noteNumber)->midiChannel();
+            
+            if(oscMidiConverters_.count(controllerNumber) != 0)
+                oscMidiConverters_[controllerNumber]->resend(channel);
+            else {
+                MidiMessage newMessage(message); // Modifiable copy of the original message
+                newMessage.setChannel(channel + 1);  // Juce uses 1-16, we use 0-15
+                midiOutputController_->sendMessage(outputPortNumber_, newMessage);
+            }
+        }
+    }
+    else {} // Block or unknown action
+}
+
+// Set all controllers to behave a particular way when messages received
+void MidiKeyboardSegment::setAllControllerActionsTo(int action) {
+    for(int i = 0; i < kControlMax; i++)
+        controllerActions_[i] = action;
+}
+
+// Pedal went off. If we're saving notes in the pedal, release them
+void MidiKeyboardSegment::damperPedalWentOff() {
+    if(!damperPedalEnabled_)
+        return;
+    // Go through a list of any notes currently in the damper pedal and release them
+    set<int>::iterator it;
+    for(it = retransmitNotesHeldInPedal_.begin(); it != retransmitNotesHeldInPedal_.end(); ++it) {
+        int note = *it;
+        cout << "releasing note " << note << " on channel " << retransmitChannelForNote_[note] << endl;
+        retransmitChannelsAvailable_.insert(retransmitChannelForNote_[note]);
+        retransmitChannelForNote_.erase(note);
+        noteOnsetTimestamps_[note] = 0;
+    }
+    retransmitNotesHeldInPedal_.clear();
+}
+
+// Handle the actual sending of the pitch wheel range RPN to a specific channel
+void MidiKeyboardSegment::sendMidiPitchWheelRangeHelper(int channel) {
+    if(midiOutputController_ == 0)
+        return;
+    
+    // Find number of semitones and cents
+    int majorRange = (int)floorf(pitchWheelRange_);
+    int minorRange = (int)(100.0 * (pitchWheelRange_ - floorf(pitchWheelRange_)));
+    
+    // Set RPN controller = 0
+    midiOutputController_->sendControlChange(outputPortNumber_, channel, 101, 0);
+    midiOutputController_->sendControlChange(outputPortNumber_, channel, 100, 0);
+    // Set data value MSB/LSB for bend range in semitones
+    midiOutputController_->sendControlChange(outputPortNumber_, channel, 6, majorRange);
+    midiOutputController_->sendControlChange(outputPortNumber_, channel, 38, minorRange);
+    // Set RPN controller back to 16383
+    midiOutputController_->sendControlChange(outputPortNumber_, channel, 101, 127);
+    midiOutputController_->sendControlChange(outputPortNumber_, channel, 100, 127);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/TouchKeys/MidiKeyboardSegment.h	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,288 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+ 
+  MidiKeyboardSegment.h: handles incoming MIDI data and certain input-output
+  mappings for one segment of a keyboard. The keyboard may be divided up into
+  any number of segments with different behaviors. An important role of this
+  class is to manage the output channel allocation when using one MIDI channel
+  per note (for example, to handle polyphonic pitch bend).
+*/
+
+
+#ifndef __TouchKeys__MidiKeyboardSegment__
+#define __TouchKeys__MidiKeyboardSegment__
+
+#include <iostream>
+#include <vector>
+#include <map>
+#include <set>
+#include "../JuceLibraryCode/JuceHeader.h"
+#include "PianoKeyboard.h"
+#include "../Mappings/MappingFactorySplitter.h"
+
+class OscMidiConverter;
+
+// This class handles the processing of MIDI input data for a particular
+// segment of the keyboard. It defines the processing mode and stores certain
+// state information about active notes for this particular part of the keyboard.
+// The MidiInputController class will use one or more of these segments to define
+// keyboard behavior. In the case of a split keyboard arrangement, MIDI channel
+// or note number might determine which segment takes ownership of a particular note.
+
+class MidiKeyboardSegment : public OscHandler {
+private:
+    static const int kMidiControllerDamperPedal;
+    static const int kPedalActiveValue;
+    
+public:
+	// Operating modes for MIDI input on this segment
+	enum {
+		ModeOff = 0,
+		ModePassThrough,
+		ModeMonophonic,
+		ModePolyphonic
+	};
+	
+    // The MIDI Pitch Wheel is not handled by control change like the others,
+    // but it is something we will want to map to.  Use a special control number
+    // to designate mapping OSC to the Pitch Wheel.  Use 14 bit values when mapping
+    // to this control. Similarly, we might want to map to channel aftertouch value.
+    // The mechanics here are identical to 7-bit controllers.
+    enum {
+        kControlDisabled = -1,
+        kControlPitchWheel = 128,
+        kControlChannelAftertouch,
+        kControlPolyphonicAftertouch,
+        kControlMax
+    };
+    
+    enum {
+        kControlActionPassthrough = 0,
+        kControlActionBroadcast,
+        kControlActionSendToLatest,
+        kControlActionBlock
+    };
+    
+public:
+	// Constructor
+	MidiKeyboardSegment(PianoKeyboard& keyboard);
+    
+    // Destructor
+    ~MidiKeyboardSegment();
+ 
+    // Set/query the output controller
+	MidiOutputController* midiOutputController() { return midiOutputController_; }
+	void setMidiOutputController(MidiOutputController* ct) { midiOutputController_ = ct; }
+	
+    // Check whether this MIDI message is for this segment
+    bool respondsToMessage(const MidiMessage& message);
+    bool respondsToNote(int noteNumber);
+    
+    // Set which channels we listen to
+	void enableChannel(int channelNumber);
+	void enableAllChannels();
+	void disableChannel(int channelNumber);
+	void disableAllChanels();
+    void setChannelMask(int channelMask) { channelMask_ = channelMask; }
+    
+    // Set which notes we listen to
+    void setNoteRange(int minNote, int maxNote);
+    std::pair<int, int> noteRange() { return std::pair<int,int>(noteMin_, noteMax_); }
+    
+    // Set whether or not we use aftertouch, pitchwheel or other controls
+    // directly from the keyboard
+    bool usesKeyboardChannnelPressure() { return usesKeyboardChannelPressure_; }
+    void setUsesKeyboardChannelPressure(bool use) {
+        usesKeyboardChannelPressure_ = use;
+        // Reset to default if not using
+        if(!use)
+            controllerValues_[kControlChannelAftertouch] = 0;
+    }
+    
+    bool usesKeyboardPitchWheel() { return usesKeyboardPitchWheel_; }
+    void setUsesKeyboardPitchWheel(bool use) {
+        usesKeyboardPitchWheel_ = use;
+        // Reset to default if not using
+        if(!use)
+            controllerValues_[kControlPitchWheel] = 8192;
+    }
+    
+    bool usesKeyboardMIDIControllers() { return usesKeyboardMidiControllers_; }
+    void setUsesKeyboardMIDIControllers(bool use) {
+        usesKeyboardMidiControllers_ = use;
+        // Reset to default if not using
+        if(!use) {
+            for(int i = 0; i < 128; i++)
+                controllerValues_[i] = 0;
+        }
+    }
+    
+    // Get or set the MIDI pitch wheel range in semitones, and optionally send an RPN
+    // message announcing its new value.
+    float midiPitchWheelRange() { return pitchWheelRange_; }
+    void setMidiPitchWheelRange(float semitones, bool send = false);
+    void sendMidiPitchWheelRange();
+    
+    // TouchKeys standalone mode generates MIDI note onsets from touch data
+    // without needing a MIDI keyboard
+    void enableTouchkeyStandaloneMode();
+    void disableTouchkeyStandaloneMode();
+    bool touchkeyStandaloneModeEnabled() { return touchkeyStandaloneMode_; }
+    
+    // All Notes Off: can be sent by MIDI or controlled programmatically
+	void allNotesOff();
+    
+    // Query the value of a controller
+    int controllerValue(int index) {
+        if(index < 0 || index >= kControlMax)
+            return 0;
+        return controllerValues_[index];
+    }
+    
+    // Reset MIDI controller values to defaults
+    void resetControllerValues();
+	
+	// Change or query the operating mode of the controller
+	int mode() { return mode_; }
+    void setMode(int mode);
+	void setModeOff();
+	void setModePassThrough();
+    void setModeMonophonic();
+	void setModePolyphonic();
+    
+    // Get/set polyphony and voice stealing for polyphonic mode
+    int polyphony() { return retransmitMaxPolyphony_; }
+    void setPolyphony(int polyphony);
+    bool voiceStealingEnabled() { return useVoiceStealing_; }
+    void setVoiceStealingEnabled(bool enable) { useVoiceStealing_ = enable; }
+    
+    // Get/set the number of the output port that messages on this segment should go to
+    int outputPort() { return outputPortNumber_; }
+    void setOutputPort(int port) { outputPortNumber_ = port; }
+    
+    // Set the minimum MIDI channel that should be used for output (0-15)
+    int outputChannelLowest() { return outputChannelLowest_; }
+    void setOutputChannelLowest(int ch) { outputChannelLowest_ = ch; }
+    
+    // Get set the output transposition in semitones, relative to input MIDI notes
+    int outputTransposition() { return outputTransposition_; }
+    void setOutputTransposition(int trans) { outputTransposition_ = trans; }
+    
+    // Whether the damper pedal is enabled in note channel allocation
+    bool damperPedalEnabled() { return damperPedalEnabled_; }
+    void setDamperPedalEnabled(bool enable);
+    
+    // MIDI handler routine
+    void midiHandlerMethod(MidiInput* source, const MidiMessage& message);
+    
+    // OSC method: used to get touch callback data from the keyboard
+	bool oscHandlerMethod(const char *path, const char *types, int numValues, lo_arg **values, void *data);
+    
+    // **** Mapping-related methods *****
+    
+    // OSC-MIDI converters: request and release methods. The acquire method
+    // will create a converter if it does not already exist, or return an existing
+    // one if it does. The release method will release the object when the
+    // acquirer no longer needs it.    
+    OscMidiConverter* acquireOscMidiConverter(int controlId);
+    void releaseOscMidiConverter(int controlId);
+    
+    // Create a new mapping factory for this segment. A pointer should be passed in
+    // of a newly-allocated object. It will be released upon removal.
+    void addMappingFactory(MappingFactory* factory, bool autoGenerateName = false);
+    
+    // Remove a mapping factory, releasing the associated object.
+    void removeMappingFactory(MappingFactory* factory);
+    
+    // Remove all mapping factories, releasing each one
+    void removeAllMappingFactories();
+    
+    // Return a list of current mapping factories.
+    vector<MappingFactory*> const& mappingFactories();
+    
+    // Return a unique identifier of the mapping state, so we know when something has changed
+    int mappingFactoryUniqueIdentifier() { return mappingFactoryUniqueIdentifier_; }
+
+private:
+	// Mode-specific MIDI input handlers
+	void modePassThroughHandler(MidiInput* source, const MidiMessage& message);
+	void modeMonophonicHandler(MidiInput* source, const MidiMessage& message);
+    
+	void modePolyphonicHandler(MidiInput* source, const MidiMessage& message);
+	void modePolyphonicNoteOn(unsigned char note, unsigned char velocity);
+	void modePolyphonicNoteOff(unsigned char note, bool forceOff = false);
+	void modePolyphonicNoteOnCallback(const char *path, const char *types, int numValues, lo_arg **values);
+
+    // Helper functions for polyphonic mode
+    void modePolyphonicSetupHelper();
+    int oldestNote();
+    int oldestNoteInPedal();
+    int newestNote();
+    
+    // Methods for managing controllers
+    void handleControlChangeRetransit(int controllerNumber, const MidiMessage& message);
+    void setAllControllerActionsTo(int action);
+    
+    // Handle action of the damper pedal: when released, clear out any notes held there
+    void damperPedalWentOff();
+    
+    // Send pitch wheel range to a specific channel
+    void sendMidiPitchWheelRangeHelper(int channel);
+    
+    // ***** Member Variables *****
+    
+    PianoKeyboard& keyboard_;						// Reference to main keyboard data
+	MidiOutputController *midiOutputController_;	// Destination for MIDI output
+    int outputPortNumber_;                          // Which port to use on the output controller
+    vector<MappingFactory*> mappingFactories_;      // Collection of mappings for this segment
+    MappingFactorySplitter mappingFactorySplitter_; // ...and a splitter class to facilitate communication
+    int mappingFactoryUniqueIdentifier_;            // Unique ID indicating mapping factory changes
+
+	int mode_;                                      // Current operating mode of the segment
+    unsigned int channelMask_;                      // Which channels we listen to (1 bit per channel)
+    int noteMin_, noteMax_;                         // Ranges of the notes we respond to
+    int outputChannelLowest_;                       // Lowest (or only) MIDI channel we send to
+    int outputTransposition_;                       // Transposition of notes at output
+    bool damperPedalEnabled_;                       // Whether to handle damper pedal events in allocating channels
+    bool touchkeyStandaloneMode_;                   // Whether we emulate MIDI data from TouchKeys
+    bool usesKeyboardChannelPressure_;              // Whether this segment passes aftertouch from the keyboard
+    bool usesKeyboardPitchWheel_;                   // Whether this segment passes pitchwheel from the keyboard
+    bool usesKeyboardMidiControllers_;              // Whether this segment passes other controllers
+    float pitchWheelRange_;                         // Range of MIDI pitch wheel (in semitones)
+    
+    int controllerValues_[kControlMax];             // Values of MIDI controllers from input device
+    int controllerActions_[kControlMax];            // What to do with MIDI CCs when they come in
+    
+    // Mapping between input notes and output channels.  Depending on the mode of operation,
+	// each note may be rebroadcast on its own MIDI channel.  Need to keep track of what goes where.
+	// key is MIDI note #, value is output channel (0-15)
+	map<int, int> retransmitChannelForNote_;
+	set<int> retransmitChannelsAvailable_;
+    set<int> retransmitNotesHeldInPedal_;
+	int retransmitMaxPolyphony_;
+    bool useVoiceStealing_;
+    timestamp_type noteOnsetTimestamps_[128];       // When each currently active note began, for stealing
+    
+    // OSC-MIDI conversion objects for use with data mapping. These are stored in each
+    // keyboard segment and specific mapping factories can request one when needed.
+    map<int, OscMidiConverter*> oscMidiConverters_;
+    map<int, int> oscMidiConverterReferenceCounts_;
+};
+
+#endif /* defined(__TouchKeys__MidiKeyboardSegment__) */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/TouchKeys/MidiOutputController.cpp	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,276 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+ 
+  MidiOutputController.cpp: handles outgoing MIDI messages and manages output
+  ports; uses Juce MIDI library functions.
+*/
+
+#include "MidiOutputController.h"
+
+#define MIDI_OUTPUT_CONTROLLER_DEBUG_RAW
+
+// Constructor
+MidiOutputController::MidiOutputController()
+{
+}
+
+// Iterate through the available MIDI devices.  Return a vector containing
+// indices and names for each device.  The index will later be passed back
+// to indicate which device to open.
+
+vector<pair<int, string> > MidiOutputController::availableMidiDevices() {
+	vector<pair<int, string> > deviceList;
+    
+	try {
+        StringArray deviceStrings = MidiOutput::getDevices();
+		
+		for(int i = 0; i < deviceStrings.size(); i++) {
+			pair<int, string> p(i, string(deviceStrings[i].toUTF8()));
+			deviceList.push_back(p);
+		}
+	}
+	catch(...) {
+		deviceList.clear();
+	}
+	
+	return deviceList;
+}
+
+// Open a new MIDI output port, given an index related to the list from
+// availableMidiDevices().  Returns true on success.
+
+bool MidiOutputController::enablePort(int identifier, int deviceNumber) {
+	if(deviceNumber < 0)
+		return false;
+    
+    // Check if there is a port for this identifier, and disable it if so
+    if(activePorts_.count(identifier) > 0)
+        disablePort(identifier);
+	
+    MidiOutput *device = MidiOutput::openDevice(deviceNumber);
+    
+    if(device == 0) {
+        cout << "Failed to enable MIDI output port " << deviceNumber << ")\n";
+        return false;
+    }
+    
+    //cout << "Enabling MIDI output port " << deviceNumber << " for ID " << identifier << "\n";
+    
+    // Save the device in the set of ports
+    MidiOutputControllerRecord record;
+    record.portNumber = deviceNumber;
+    record.output = device;
+    
+    activePorts_[identifier] = record;//{deviceNumber, device};
+    
+	return true;
+}
+
+bool MidiOutputController::enableVirtualPort(int identifier, const char *name) {
+    /*
+    // Try to create a new port
+    MidiOutput* device = MidiOutput::createNewDevice(kMidiVirtualOutputName);
+    if(device == 0)
+        return false;
+    
+    midiOut_ = device;
+    portNumber_ = kMidiVirtualOutputPortNumber;
+    
+	return true;
+    */
+    return 0; // TODO: implement me
+}
+
+void MidiOutputController::disablePort(int identifier) {
+	if(activePorts_.count(identifier) <= 0)
+		return;
+	
+	MidiOutput *device = activePorts_[identifier].output;
+    
+    if(device == 0)
+        return;
+    
+	//cout << "Disabling MIDI output " << activePorts_[identifier].portNumber << " for ID " << identifier << "\n";
+    delete device;
+    
+	activePorts_.erase(identifier);
+}
+
+void MidiOutputController::disableAllPorts() {
+    std::map<int, MidiOutputControllerRecord>::iterator it;
+	
+	//cout << "Disabling all MIDI output ports\n";
+	
+	it = activePorts_.begin();
+	
+	while(it != activePorts_.end()) {
+        if(it->second.output == 0)
+            continue;
+		delete it->second.output;						// free MidiInputCallback
+		it++;
+	}
+	
+	activePorts_.clear();
+}
+
+int MidiOutputController::enabledPort(int identifier) {
+	if(activePorts_.count(identifier) <= 0)
+		return -1;
+    return activePorts_[identifier].portNumber;
+}
+
+std::vector<std::pair<int, int> > MidiOutputController::enabledPorts() {
+    std::vector<std::pair<int, int> > ports;
+    std::map<int, MidiOutputControllerRecord>::iterator it;
+
+    for(it = activePorts_.begin(); it != activePorts_.end(); ++it) {
+        ports.push_back(std::pair<int,int>(it->first, it->second.portNumber));
+    }
+    
+    return ports;
+}
+
+/*
+// Open a new MIDI output port, given an index related to the list from
+// availableMidiDevices().  Returns true on success.
+
+bool MidiOutputController::openPort(int portNumber) {
+	// Close any previously open port
+    closePort();
+    
+    // Try to open the port
+    MidiOutput* device = MidiOutput::openDevice(portNumber);
+    if(device == 0)
+        return false;
+    
+    midiOut_ = device;
+    portNumber_ = portNumber;
+    
+    return true;
+}
+
+// Open a virtual MIDI port that other applications can connect to.
+// Returns true on success.
+
+bool MidiOutputController::openVirtualPort() {
+	// Close any previously open port
+    closePort();
+    
+    // Try to create a new port
+    MidiOutput* device = MidiOutput::createNewDevice(kMidiVirtualOutputName);
+    if(device == 0)
+        return false;
+    
+    midiOut_ = device;
+    portNumber_ = kMidiVirtualOutputPortNumber;
+    
+	return true;
+}
+
+// Close a currently open MIDI port
+void MidiOutputController::closePort() {
+    if(midiOut_ != 0) {
+        delete midiOut_;
+    }
+    
+    midiOut_ = 0;
+    portNumber_ = kMidiOutputNotOpen;
+}
+ */
+
+// Send a MIDI Note On message
+void MidiOutputController::sendNoteOn(int port, unsigned char channel, unsigned char note, unsigned char velocity) {
+	sendMessage(port,
+                MidiMessage((int)((channel & 0x0F) | kMidiMessageNoteOn),
+                            (int)(note & 0x7F),
+                            (int)(velocity & 0x7F)));
+}
+
+// Send a MIDI Note Off message
+void MidiOutputController::sendNoteOff(int port, unsigned char channel, unsigned char note, unsigned char velocity) {
+	sendMessage(port,
+                MidiMessage((int)((channel & 0x0F) | kMidiMessageNoteOff),
+                            (int)(note & 0x7F),
+                            (int)(velocity & 0x7F)));
+}
+
+// Send a MIDI Control Change message
+void MidiOutputController::sendControlChange(int port, unsigned char channel, unsigned char control, unsigned char value) {
+	sendMessage(port,
+                MidiMessage((int)((channel & 0x0F) | kMidiMessageControlChange),
+                            (int)(control & 0x7F),
+                            (int)(value & 0x7F)));
+}
+
+// Send a MIDI Program Change message
+void MidiOutputController::sendProgramChange(int port, unsigned char channel, unsigned char value) {
+	sendMessage(port,
+                MidiMessage((int)((channel & 0x0F) | kMidiMessageProgramChange),
+                            (int)(value & 0x7F)));
+}
+
+// Send a Channel Aftertouch message
+void MidiOutputController::sendAftertouchChannel(int port, unsigned char channel, unsigned char value) {
+	sendMessage(port,
+                MidiMessage((int)((channel & 0x0F) | kMidiMessageAftertouchChannel),
+                            (int)(value & 0x7F)));
+}
+
+// Send a Polyphonic Aftertouch message
+void MidiOutputController::sendAftertouchPoly(int port, unsigned char channel, unsigned char note, unsigned char value) {
+	sendMessage(port,
+                MidiMessage((int)((channel & 0x0F) | kMidiMessageAftertouchPoly),
+                            (int)(note & 0x7F),
+                            (int)(value & 0x7F)));
+}
+
+// Send a Pitch Wheel message
+void MidiOutputController::sendPitchWheel(int port, unsigned char channel, unsigned int value) {
+	sendMessage(port,
+                MidiMessage((int)((channel & 0x0F) | kMidiMessagePitchWheel),
+                            (int)(value & 0x7F),
+                            (int)((value >> 7) & 0x7F)));
+}
+
+// Send a MIDI system reset message
+void MidiOutputController::sendReset(int port) {
+	sendMessage(port, MidiMessage(kMidiMessageReset));
+}
+
+// Send a generic MIDI message (pre-formatted data)
+void MidiOutputController::sendMessage(int port, const MidiMessage& message) {
+#ifdef MIDI_OUTPUT_CONTROLLER_DEBUG_RAW
+    int dataSize = message.getRawDataSize();
+    const unsigned char *data = message.getRawData();
+    
+	cout << "MIDI Output " << port << ": ";
+	for(int debugPrint = 0; debugPrint < dataSize; debugPrint++)
+		printf("%x ", data[debugPrint]);
+	cout << endl;
+#endif /* MIDI_OUTPUT_CONTROLLER_DEBUG_RAW */
+    
+	if(activePorts_.count(port) == 0) {
+#ifdef MIDI_OUTPUT_CONTROLLER_DEBUG_RAW
+        cout << "MIDI Output: no port on " << port << endl;
+#endif
+		return;
+    }
+    
+	activePorts_[port].output->sendMessageNow(message);
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/TouchKeys/MidiOutputController.h	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,93 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+ 
+  MidiOutputController.h: handles outgoing MIDI messages and manages output
+  ports; uses Juce MIDI library functions.
+*/
+
+#ifndef MIDI_OUTPUT_CONTROLLER_H
+#define MIDI_OUTPUT_CONTROLLER_H
+
+#include <map>
+#include "MidiInputController.h"
+
+const juce::String kMidiVirtualOutputName = "TouchKeys";
+
+using namespace std;
+
+class MidiOutputController {
+private:
+    struct MidiOutputControllerRecord {
+        int portNumber;
+        MidiOutput *output;
+    };
+    
+public:
+    enum {
+        kMidiVirtualOutputPortNumber = -2,
+        kMidiOutputNotOpen = -1
+    };
+	
+	// Constructor
+	MidiOutputController();
+	
+	// Query available devices
+	vector<pair<int, string> > availableMidiDevices();
+	
+	// Methods to connect/disconnect from a target port
+    bool enablePort(int identifier, int deviceNumber);
+    bool enableVirtualPort(int identifier, const char *name);
+	void disablePort(int identifier);
+	void disableAllPorts();
+    
+    int enabledPort(int identifier);
+    std::vector<std::pair<int, int> > enabledPorts();
+   
+	//bool openPort(int portNumber);
+	//bool openVirtualPort();
+	//void closePort();
+	
+	//bool isOpen() { return (midiOut_ != 0); }
+    //int activePort() { return portNumber_; }
+    //int numActivePorts() { return 1; } // TODO: implement me
+	
+	// Send MIDI messages
+	void sendNoteOn(int port, unsigned char channel, unsigned char note, unsigned char velocity);
+    void sendNoteOff(int port, unsigned char channel, unsigned char note, unsigned char velocity = 64);
+	void sendControlChange(int port, unsigned char channel, unsigned char control, unsigned char value);
+	void sendProgramChange(int port, unsigned char channel, unsigned char value);
+	void sendAftertouchChannel(int port, unsigned char channel, unsigned char value);
+	void sendAftertouchPoly(int port, unsigned char channel, unsigned char note, unsigned char value);
+	void sendPitchWheel(int port, unsigned char channel, unsigned int value);
+	void sendReset(int port);
+	
+	// Generic pre-formed messages
+	void sendMessage(int port, const MidiMessage& message);
+	
+	// Destructor
+	~MidiOutputController() { disableAllPorts(); }
+	
+private:
+	//MidiOutput *midiOut_;	// Output instance from Juce; 0 if not open
+    //int portNumber_;        // Which port is currently open
+    
+    std::map<int, MidiOutputControllerRecord> activePorts_;              // Destinations for MIDI data
+};
+
+#endif /* MIDI_OUTPUT_CONTROLLER_H */
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/TouchKeys/Osc.cpp	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,511 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+ 
+  Osc.cpp: classes for handling reception and transmission of OSC messages,
+  using the liblo library.
+*/
+
+#include "Osc.h"
+
+#undef DEBUG_OSC
+
+#pragma mark OscHandler
+
+OscHandler::~OscHandler()
+{
+	if(oscController_ != NULL)	// Remove (individually) each listener
+	{
+		set<string>::iterator it;
+		
+		for(it = oscListenerPaths_.begin(); it != oscListenerPaths_.end(); ++it)
+		{
+#ifdef DEBUG_OSC
+			cout << "Deleting path " << *it << endl;
+#endif
+			
+			string pathToRemove = *it;
+			oscController_->removeListener(pathToRemove, this);
+		}
+	}
+}
+
+#pragma mark -- Private Methods
+
+// Call this internal method to add a listener to the OSC controller.  Returns true on success.
+
+bool OscHandler::addOscListener(const string& path)
+{
+	if(oscController_ == NULL)
+		return false;
+	if(oscListenerPaths_.count(path) > 0)
+		return false;
+	oscListenerPaths_.insert(path);
+	oscController_->addListener(path, this);
+	return true;
+}
+
+bool OscHandler::removeOscListener(const string& path)
+{
+	if(oscController_ == NULL)
+		return false;
+	if(oscListenerPaths_.count(path) == 0)
+		return false;
+	oscController_->removeListener(path, this);
+	oscListenerPaths_.erase(path);
+	return true;
+}
+
+bool OscHandler::removeAllOscListeners()
+{
+	if(oscController_ == NULL)
+		return false;
+	set<string>::iterator it = oscListenerPaths_.begin();
+	
+	while(it != oscListenerPaths_.end()) {
+		removeOscListener(*it++);
+	}
+	
+	return true;
+}
+
+#pragma mark OscMessageSource
+
+// Adds a specific object listening for a specific OSC message.  The object will be
+// added to the internal map from strings to objects.  All messages are preceded by
+// a global prefix (typically "/mrp").  Returns true on success.
+
+bool OscMessageSource::addListener(const string& path, OscHandler *object)
+{
+	if(object == NULL)
+		return false;
+    
+#ifdef OLD_OSC_MESSAGE_SOURCE
+	double before = Time::getMillisecondCounterHiRes();
+	oscListenerMutex_.enterWrite();
+    cout << "addListener(): took " << Time::getMillisecondCounterHiRes() - before << "ms to acquire mutex\n";
+	noteListeners_.insert(pair<string, OscHandler*>(path, object));
+	oscListenerMutex_.exitWrite();
+#else
+    ScopedLock sl(oscUpdaterMutex_);
+    
+    // Add this object to the insertion list
+    noteListenersToAdd_.insert(std::pair<string, OscHandler*>(path, object));
+#endif
+    
+#ifdef DEBUG_OSC
+	cout << "Added OSC listener to path '" << path << "'\n";
+#endif
+	
+	return true;
+}
+
+// Removes a specific object from listening to a specific OSC message.
+// Returns true if at least one path was removed.
+
+bool OscMessageSource::removeListener(const string& path, OscHandler *object)
+{
+	if(object == NULL)
+		return false;
+	
+	bool removedAny = false;
+	
+#ifdef OLD_OSC_MESSAGE_SOURCE    
+	oscListenerMutex_.enterWrite(); // Lock the mutex so no incoming messages happen in the middle
+	
+	multimap<string, OscHandler*>::iterator it;
+	pair<multimap<string, OscHandler*>::iterator,multimap<string, OscHandler*>::iterator> ret;
+	
+	// Every time we remove an element from the multimap, the iterator is potentially corrupted.  Realistically
+	// there should never be more than one entry with the same object and same path (we check this on insertion).
+	
+	ret = noteListeners_.equal_range(path);
+	
+	it = ret.first;
+	while(it != ret.second)
+	{
+		if(it->second == object)
+		{
+			noteListeners_.erase(it++);
+			removedAny = true;
+			break;
+		}
+		else
+			++it;
+	}
+	
+	oscListenerMutex_.exitWrite();
+#else
+    ScopedLock sl(oscUpdaterMutex_);
+    
+    // Add this object to the removal list
+    noteListenersToRemove_.insert(std::pair<string, OscHandler*>(path, object));
+    
+    // Also remove this object from anything on the add list, so it doesn't
+    // get put back in by a previous add call.
+    pair<multimap<string, OscHandler*>::iterator,multimap<string, OscHandler*>::iterator> ret;
+    multimap<string, OscHandler*>::iterator it;
+    
+    ret = noteListenersToAdd_.equal_range(path);
+    it = ret.first;
+    while(it != ret.second) {
+        if(it->second == object) {
+            noteListenersToAdd_.erase(it++);
+            //break;
+        }
+        else
+            ++it;
+    }
+    
+    removedAny = true; // FIXME: do we still need this?
+#endif
+    
+#ifdef DEBUG_OSC
+	if(removedAny)
+		cout << "Removed OSC listener from path '" << path << "'\n";	
+	else
+		cout << "Removal failed to find OSC listener on path '" << path << "'\n";
+#endif
+	
+	return removedAny;
+}
+
+// Removes an object from all OSC messages it was listening to.  Returns true if object
+// was found and removed.
+
+bool OscMessageSource::removeListener(OscHandler *object)
+{
+	if(object == NULL)
+		return false;
+
+	bool removedAny = false;
+
+#ifdef OLD_OSC_MESSAGE_SOURCE
+	oscListenerMutex_.enterWrite();	// Lock the mutex so no incoming messages happen in the middle
+	
+	multimap<string, OscHandler*>::iterator it;
+
+	// Every time we remove an element from the multimap, the iterator is potentially corrupted.  Realistically
+	// there should never be more than one entry with the same object and same path (we check this on insertion).
+	
+	it = noteListeners_.begin();
+	while(it != noteListeners_.end())
+	{
+		if(it->second == object)
+		{
+			noteListeners_.erase(it++);
+			removedAny = true;
+			//break;
+		}
+		else
+			++it;
+	}
+	
+	oscListenerMutex_.exitWrite();
+#else
+    ScopedLock sl(oscUpdaterMutex_);
+    
+    // Add this object to the removal list
+    noteListenersForBlanketRemoval_.insert(object);
+    
+    // Also remove this object from anything on the add list, so it doesn't
+    // get put back in by a previous add call.
+    multimap<string, OscHandler*>::iterator it;
+    it = noteListenersToAdd_.begin();
+    while(it != noteListenersToAdd_.end()) {
+        if(it->second == object) {
+            noteListenersToAdd_.erase(it++);
+        }
+        else
+            ++it;
+    }
+    
+    removedAny = true; // FIXME: do we still need this?
+#endif
+	
+#ifdef DEBUG_OSC
+	if(removedAny)
+		cout << "Removed OSC listener from all paths\n";	
+	else
+		cout << "Removal failed to find OSC listener on any path\n";
+#endif
+	
+	return removedAny;
+}
+
+// Propagate changes to the listeners to the main noteListeners_ object
+
+void OscMessageSource::updateListeners()
+{
+    ScopedLock sl2(oscListenerMutex_);    
+    ScopedLock sl(oscUpdaterMutex_);
+	multimap<string, OscHandler*>::iterator it;
+    
+    // Step 1: remove any objects that need complete removal from all paths
+    set<OscHandler*>::iterator blanketRemovalIterator;
+    for(blanketRemovalIterator = noteListenersForBlanketRemoval_.begin();
+        blanketRemovalIterator != noteListenersForBlanketRemoval_.end();
+        ++blanketRemovalIterator) {
+        it = noteListeners_.begin();
+        while(it != noteListeners_.end()) {
+            if(it->second == *blanketRemovalIterator) {
+                noteListeners_.erase(it++);
+            }
+            else
+                ++it;
+        }
+    }
+    
+    // Step 2: remove any specific path listeners
+    for(it = noteListenersToRemove_.begin(); it != noteListenersToRemove_.end(); ++it) {
+        pair<multimap<string, OscHandler*>::iterator,multimap<string, OscHandler*>::iterator> ret;
+        multimap<string, OscHandler*>::iterator it2;
+        string const& path = it->first;
+        OscHandler *object = it->second;
+        
+        // Find all the objects that match this string and remove ones that correspond to this particular OscHandler
+        ret = noteListeners_.equal_range(path);
+        
+        it2 = ret.first;
+        while(it2 != ret.second)
+        {
+            if(it2->second == object) {
+                noteListeners_.erase(it2++);
+                //break;
+            }
+            else
+                ++it2;
+        }
+    }
+
+    // Step 3: add any listeners
+    for(it = noteListenersToAdd_.begin(); it != noteListenersToAdd_.end(); ++it) {
+        noteListeners_.insert(pair<string, OscHandler*>(it->first, it->second));
+    }
+    
+    // Step 4: clear the buffers of pending listeners
+    noteListenersForBlanketRemoval_.clear();
+    noteListenersToRemove_.clear();
+    noteListenersToAdd_.clear();
+}
+
+#pragma mark OscReceiver
+
+// OscReceiver::handler()
+// The main handler method for incoming OSC messages.  From here, we farm out the processing depending
+// on the path.  In general all our paths should start with /mrp.  Return 0 if the message has been
+// adequately handled, 1 otherwise (so the server can look for other functions to pass it to).
+
+int OscReceiver::handler(const char *path, const char *types, lo_arg **argv, int argc, lo_message msg, void *data)
+{
+	bool matched = false;
+	
+	string pathString(path);	
+	
+	if(useThru_)
+	{
+		// Rebroadcast any matching messages
+		
+		if(!pathString.compare(0, thruPrefix_.length(), thruPrefix_))
+			lo_send_message(thruAddress_, path, msg);
+	}
+	
+	// Check if the incoming message matches the global prefix for this program.  If not, discard it.
+	if(pathString.compare(0, globalPrefix_.length(), globalPrefix_))
+	{
+#ifdef DEBUG_OSC
+		cout << "OSC message '" << path << "' received\n";
+#endif
+		return 1;
+	}
+	
+    // Update the list of OSC listeners to propagate any changes
+    updateListeners();
+    
+	// Lock the mutex so the list of listeners doesn't change midway through
+    oscListenerMutex_.enter();
+    //oscListenerMutex_.enterRead();
+	
+	// Now remove the global prefix and compare the rest of the message to the registered handlers.
+	multimap<string, OscHandler*>::iterator it;
+	pair<multimap<string, OscHandler*>::iterator,multimap<string, OscHandler*>::iterator> ret;
+	string truncatedPath = pathString.substr(globalPrefix_.length(), 
+											 pathString.length() - globalPrefix_.length());
+	ret = noteListeners_.equal_range(truncatedPath);
+	
+	it = ret.first;
+	while(it != ret.second)
+	{
+		OscHandler *object = (*it++).second;
+		
+#ifdef DEBUG_OSC
+		cout << "Matched OSC path '" << path << "' to handler " << object << endl;
+#endif
+		object->oscHandlerMethod(truncatedPath.c_str(), types, argc, argv, data);
+		matched = true;
+	}
+	
+    //oscListenerMutex_.exitRead();
+    oscListenerMutex_.exit();
+    
+	if(matched)		// This message has been handled
+		return 0;
+	
+	printf("Unhandled OSC path: <%s>\n", path);
+	
+#ifdef DEBUG_OSC
+    for (int i=0; i<argc; i++) {
+		printf("arg %d '%c' ", i, types[i]);
+		lo_arg_pp((lo_type)types[i], argv[i]);
+		printf("\n");
+    }
+#endif
+	
+    return 1;
+}
+
+#pragma mark OscTransmitter
+
+// Add a new transmit address.  Returns the index of the new address.
+
+int OscTransmitter::addAddress(const char * host, const char * port, int proto)
+{
+	lo_address addr = lo_address_new_with_proto(proto, host, port);
+	
+	if(addr == 0)
+		return -1;
+	addresses_.push_back(addr);
+	
+	return (int)addresses_.size() - 1;
+}
+
+// Delete a current transmit address
+
+void OscTransmitter::removeAddress(int index)
+{
+	if(index >= addresses_.size() || index < 0)
+		return;
+	addresses_.erase(addresses_.begin() + index);
+}
+
+// Delete all destination addresses
+
+void OscTransmitter::clearAddresses()
+{
+	vector<lo_address>::iterator it = addresses_.begin();
+	
+	while(it != addresses_.end()) {
+		lo_address_free(*it++);
+	}
+	
+	addresses_.clear();
+}
+
+void OscTransmitter::sendMessage(const char * path, const char * type, ...)
+{
+    if(!enabled_)
+        return;
+    
+	va_list v;
+	
+	va_start(v, type);
+	lo_message msg = lo_message_new();
+	lo_message_add_varargs(msg, type, v);
+
+	/*if(debugMessages_) {
+		cout << path << " " << type << ": ";
+		
+		lo_arg **args = lo_message_get_argv(msg);
+		
+		for(int i = 0; i < lo_message_get_argc(msg); i++) {
+			switch(type[i]) {
+				case 'i':
+					cout << args[i]->i << " ";
+					break;
+				case 'f':
+					cout << args[i]->f << " ";
+					break;
+				default:
+					cout << "? ";
+			}
+		}
+		
+		cout << endl;
+		//lo_message_pp(msg);
+	}*/
+	
+	sendMessage(path, type, msg);
+
+	lo_message_free(msg);
+	va_end(v);
+}
+
+void OscTransmitter::sendMessage(const char * path, const char * type, const lo_message& message)
+{
+    if(!enabled_)
+        return;
+    
+    if(debugMessages_) {
+        cout << path << " " << type << " ";
+
+        int argc = lo_message_get_argc(message);
+        lo_arg **argv = lo_message_get_argv(message);
+        for (int i=0; i<argc; i++) {
+            lo_arg_pp((lo_type)type[i], argv[i]);
+            cout << " ";
+        }
+        cout << endl;
+    }
+    
+	// Send message to everyone who's currently listening
+	for(vector<lo_address>::iterator it = addresses_.begin(); it != addresses_.end(); it++) {
+		lo_send_message(*it, path, message);
+	}
+}
+
+// Send an array of bytes as an OSC message.  Bytes will be sent as a blob.
+
+void OscTransmitter::sendByteArray(const char * path, const unsigned char * data, int length)
+{
+    if(!enabled_)
+        return;
+	if(length == 0)
+		return;
+	
+	lo_blob b = lo_blob_new(length, data);
+	
+	lo_message msg = lo_message_new();
+	lo_message_add_blob(msg, b);
+	
+	if(debugMessages_) {
+		cout << path << " ";
+		lo_message_pp(msg);
+	}
+	
+	// Send message to everyone who's currently listening
+	for(vector<lo_address>::iterator it = addresses_.begin(); it != addresses_.end(); it++) {
+		lo_send_message(*it, path, msg);
+	}	
+	
+	lo_blob_free(b);
+}
+
+OscTransmitter::~OscTransmitter()
+{
+	clearAddresses();
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/TouchKeys/Osc.h	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,165 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+ 
+  Osc.h: classes for handling reception and transmission of OSC messages,
+  using the liblo library.
+*/
+
+#ifndef OSC_H
+#define OSC_H
+
+#include <iostream>
+#include <iomanip>
+#include <fstream>
+#include <set>
+#include <map>
+#include <string>
+#include <vector>
+#include <set>
+#include "../JuceLibraryCode/JuceHeader.h"
+#include "lo/lo.h"
+
+using namespace std;
+
+class OscMessageSource;
+
+// This is an abstract base class implementing a single function oscHandlerMethod().  Objects that
+// want to register to receive OSC messages should inherit from OscHandler.  Notice that all listener
+// add/remove methods are private or protected.  The subclass of OscHandler should add any relevant 
+// listeners, or optionally expose a public interface to add listeners.  (Never call the methods in
+// OscMessageSource externally.)
+
+class OscHandler
+{
+public:
+	OscHandler() : oscController_(NULL) {}
+	
+	// The OSC controller will call this method when it gets a matching message that's been registered
+	virtual bool oscHandlerMethod(const char *path, const char *types, int numValues, lo_arg **values, void *data) = 0;
+	void setOscController(OscMessageSource *c) { oscController_ = c; }
+	
+	virtual ~OscHandler();	// In the destructor, remove all OSC listeners
+protected:
+	bool addOscListener(const string& path);
+	bool removeOscListener(const string& path);
+	bool removeAllOscListeners();
+	
+	OscMessageSource *oscController_;
+	set<string> oscListenerPaths_;
+};
+
+// Base class for anything that acts as a source of OSC messages.  Could be
+// received externally or internally generated.
+
+class OscMessageSource
+{
+	friend class OscHandler;
+	
+public:
+	OscMessageSource() {}
+	
+protected:
+	bool addListener(const string& path, OscHandler *object);		// Add a listener object for a specific path
+	bool removeListener(const string& path, OscHandler *object);	// Remove a listener object	from a specific path
+	bool removeListener(OscHandler *object);						// Remove a listener object from all paths
+	
+    void updateListeners();                                         // Propagate changes to the listeners to the main object
+    
+	//ReadWriteLock oscListenerMutex_;                // This mutex protects the OSC listener table from being modified mid-message
+	CriticalSection oscListenerMutex_;                // This mutex protects the OSC listener table from being modified mid-message
+    CriticalSection oscUpdaterMutex_;                 // This mutex controls the insertion of objects in add/removeListener
+    
+	multimap<string, OscHandler*> noteListeners_;	// Map from OSC path name to handler (possibly multiple handlers per object)
+    multimap<string, OscHandler*> noteListenersToAdd_;    // Collection of listeners to add on the next cycle
+    multimap<string, OscHandler*> noteListenersToRemove_; // Collection of listeners to remove on the next cycle
+    set<OscHandler*> noteListenersForBlanketRemoval_;     // Collection of listeners to remove from all paths
+};
+
+// This class specifically implements OSC messages coming from external sources
+
+class OscReceiver : public OscMessageSource
+{
+public:
+	OscReceiver(lo_server_thread thread, const char *prefix) {
+		oscServerThread_ = thread;
+		globalPrefix_.assign(prefix);
+		useThru_ = false;
+		lo_server_thread_add_method(thread, NULL, NULL, OscReceiver::staticHandler, (void *)this);
+	}	
+	
+	void setThruAddress(lo_address thruAddr, const char *prefix) {
+		thruAddress_ = thruAddr;
+		thruPrefix_.assign(prefix);
+		useThru_ = true;
+	}
+	
+	// staticHandler() is called by liblo with new OSC messages.  Its only function is to pass control
+	// to the object-specific handler method, which has access to all internal variables.
+	
+	int handler(const char *path, const char *types, lo_arg **argv, int argc, lo_message msg, void *data);
+	static int staticHandler(const char *path, const char *types, lo_arg **argv, int argc, lo_message msg, void *userData) {
+		return ((OscReceiver *)userData)->handler(path, types, argv, argc, msg, userData);
+	}	
+	
+	~OscReceiver() {
+		lo_server_thread_del_method(oscServerThread_, NULL, NULL);
+	}	
+	
+private:
+	lo_server_thread oscServerThread_;		// Thread that handles received OSC messages
+	
+	// OSC thru
+	bool useThru_;							// Whether or not we retransmit any messages
+	lo_address thruAddress_;				// Address to which we retransmit
+	string thruPrefix_;						// Prefix that must be matched to be retransmitted
+	
+	// State variables
+	string globalPrefix_;					// Prefix for all OSC paths	
+};
+
+class OscTransmitter
+{
+public:
+	OscTransmitter() : enabled_(true), debugMessages_(false) {}
+    
+    // Enable or disable transmission
+    void setEnabled(bool enable) { enabled_ = enable; }
+    bool enabled() { return enabled_; }
+	
+	// Add and remove addresses to send to
+	int addAddress(const char * host, const char * port, int proto = LO_UDP);
+	void removeAddress(int index);
+	void clearAddresses();
+    vector<lo_address> addresses() { return addresses_; }
+	
+	void sendMessage(const char * path, const char * type, ...);
+	void sendMessage(const char * path, const char * type, const lo_message& message);
+	void sendByteArray(const char * path, const unsigned char * data, int length);
+	
+	void setDebugMessages(bool debug) { debugMessages_ = debug; }
+	
+	~OscTransmitter();
+	
+private:
+	vector<lo_address> addresses_;
+    bool enabled_;
+	bool debugMessages_;
+};
+
+#endif // OSC_H
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/TouchKeys/OscMidiConverter.cpp	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,510 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+ 
+  OscMidiConverter.cpp: converts incoming OSC messages to outgoing MIDI
+  messages with adjustable ranges and parameters.
+*/
+#include "OscMidiConverter.h"
+#include "MidiKeyboardSegment.h"
+
+#undef DEBUG_OSC_MIDI_CONVERTER
+
+// Main constructor: set up OSC reception from the keyboard
+OscMidiConverter::OscMidiConverter(PianoKeyboard& keyboard, MidiKeyboardSegment& segment, int controllerId) :
+  keyboard_(keyboard), keyboardSegment_(segment), midiOutputController_(0),
+  controller_(controllerId), lastUniqueId_(0),
+  pitchWheelRange_(2.0), incomingController_(MidiKeyboardSegment::kControlDisabled)
+{
+	setOscController(&keyboard_);
+    
+    for(int i = 0; i < 16; i++) {
+        currentValue_[i] = 0;
+        lastOutputValue_[i] = -1;
+    }
+}
+
+// Set the type of MIDI message (CC, Pitch Wheel, Aftertouch)
+// that this particular object is in charge of controlling.
+// Optionally specify the default, range, and whether to use a
+// 14 bit controller.
+void OscMidiConverter::setMidiMessageType(int defaultValue, int minValue,
+                                          int maxValue, int centerValue, bool use14BitControl) {
+    /*if(controller < 0 || controller >= MidiKeyboardSegment::kControlMax) {
+        controller_ = MidiKeyboardSegment::kControlDisabled;
+        return;
+    }
+    
+    keyboardSegment_ = segment;*/
+    
+    // Otherwise, send defaults on the current controller to all channels and update to the new values
+    //for(int i = 0; i < 16; i++)
+    //    sendDefaultValue(i);
+    
+    // Clear any existing active inputs, but not the mappings themselves
+    lastValues_.clear();
+    
+    //controller_ = controller;
+    if(defaultValue >= 0)
+        controlDefaultValue_ = defaultValue;
+    else {
+        // Default value for MIDI CCs and aftertouch is 0
+        // Default value for MIDI pitch wheel is 8192
+        if(controller_ == MidiKeyboardSegment::kControlPitchWheel)
+            controlDefaultValue_ = 8192;
+        else
+            controlDefaultValue_ = 0;
+    }
+    
+    if(centerValue >= 0)
+        controlCenterValue_ = centerValue;
+    else
+        controlCenterValue_ = controlDefaultValue_;
+    
+    // Pitch wheel is always 14 bit. Aftertouch is always 7 bit.
+    // Other CCs are selectable (though according to MIDI spec not every
+    // controller has an LSB defined). And above 96 using "control+32"
+    // for fine adjust no longer makes sense
+    if(controller_ < 96)
+        controllerIs14Bit_ = use14BitControl;
+    else if(controller_ == MidiKeyboardSegment::kControlPitchWheel)
+        controllerIs14Bit_ = true;
+    else
+        controllerIs14Bit_ = false;
+   
+    // Handle the outer ranges of the control, providing defaults if unspecified
+    if(controllerIs14Bit_) {
+        if(maxValue < 0 || maxValue > 16383)
+            controlMaxValue_ = 16383;
+        else
+            controlMaxValue_ = maxValue;
+        if(minValue < 0 || minValue > 16383)
+            controlMinValue_ = 0;
+        else
+            controlMinValue_ = minValue;
+    }
+    else {
+        if(maxValue < 0 || maxValue > 127)
+            controlMaxValue_ = 127;
+        else
+            controlMaxValue_ = maxValue;
+        if(minValue < 0 || minValue > 127)
+            controlMinValue_ = 0;
+        else
+            controlMinValue_ = minValue;
+    }
+}
+
+void OscMidiConverter::listenToIncomingControl(int controller, int centerValue, bool use14BitControl) {
+    if(controller < 0 || controller >= MidiKeyboardSegment::kControlMax) {
+        incomingController_ = MidiKeyboardSegment::kControlDisabled;
+        return;
+    }
+    
+    incomingController_ = controller;
+    
+    // Assign the center value that will be subtacted from the incoming controller
+    if(centerValue >= 0)
+        incomingControllerCenterValue_ = centerValue;
+    else if(controller == MidiKeyboardSegment::kControlPitchWheel)
+        incomingControllerCenterValue_ = 8192; // Pitch wheel is centered at 8192
+    else
+        incomingControllerCenterValue_ = 0;
+    
+    // Pitch wheel is always 14 bit. Aftertouch is always 7 bit.
+    // Other CCs are selectable (though according to MIDI spec not every
+    // controller has an LSB defined). And above 96 using "control+32"
+    // for fine adjust no longer makes sense
+    if(controller < 96)
+        incomingControllerIs14Bit_ = use14BitControl;
+    else if(controller == MidiKeyboardSegment::kControlPitchWheel)
+        incomingControllerIs14Bit_ = true;
+    else
+        incomingControllerIs14Bit_ = false;
+}
+
+// Resend the most recent value
+void OscMidiConverter::resend(int channel) {
+    sendCurrentValue(keyboardSegment_.outputPort(), channel, -1, true);
+}
+
+// Send the default value on the specified channel.
+void OscMidiConverter::sendDefaultValue(int channel) {    
+    if(midiOutputController_ == 0 || controller_ == MidiKeyboardSegment::kControlDisabled)
+        return;
+    
+    // Modulate the default value by the value of the incoming controller,
+    // if one is enabled.
+    int defaultValue = controlDefaultValue_;
+    if(incomingController_ != MidiKeyboardSegment::kControlDisabled)
+        defaultValue += keyboardSegment_.controllerValue(incomingController_) - incomingControllerCenterValue_;
+    
+    if(controller_ == MidiKeyboardSegment::kControlPitchWheel) {
+        sendPitchWheelRange(keyboardSegment_.outputPort(), channel);
+        midiOutputController_->sendPitchWheel(keyboardSegment_.outputPort(), channel, defaultValue);
+    }
+    else if(controller_ == MidiKeyboardSegment::kControlChannelAftertouch) {
+        midiOutputController_->sendAftertouchChannel(keyboardSegment_.outputPort(), channel, defaultValue);
+    }
+    else if(controller_ == MidiKeyboardSegment::kControlPolyphonicAftertouch) {
+        // TODO
+    }
+    else if(controllerIs14Bit_) {
+        midiOutputController_->sendControlChange(keyboardSegment_.outputPort(), channel, controller_, defaultValue >> 7);
+        midiOutputController_->sendControlChange(keyboardSegment_.outputPort(), channel, controller_ + 32, defaultValue & 0x7F);
+    }
+    else
+        midiOutputController_->sendControlChange(keyboardSegment_.outputPort(), channel, controller_, defaultValue);
+}
+
+int OscMidiConverter::currentControllerValue(int channel) {
+    float controlValue = (float)controlCenterValue_ + (float)controlMinValue_
+    + currentValue_[channel]*(float)(controlMaxValue_ - controlMinValue_);
+    
+    if(incomingController_ != MidiKeyboardSegment::kControlDisabled)
+        controlValue += keyboardSegment_.controllerValue(incomingController_) - incomingControllerCenterValue_;
+    
+#ifdef DEBUG_OSC_MIDI_CONVERTER
+    std::cout << "current value " << currentValue_[channel] << " corresponds to " << controlValue << std::endl;
+#endif
+    int roundedControlValue = (int)roundf(controlValue);
+    if(roundedControlValue > controlMaxValue_)
+        roundedControlValue = controlMaxValue_;
+    if(roundedControlValue < controlMinValue_)
+        roundedControlValue = controlMinValue_;
+    
+    return roundedControlValue;
+}
+
+// Add a new OSC input to this MIDI control
+void OscMidiConverter::addControl(const string& oscPath, int oscParamNumber, float oscMinValue,
+                                  float oscMaxValue, float oscCenterValue, int outOfRangeBehavior) {
+	// First remove any existing mapping with these exact parameters
+	removeControl(oscPath);
+
+#ifdef DEBUG_OSC_MIDI_CONVERTER
+    std::cout << "OscMidiConverter: adding path " << oscPath << std::endl;
+#endif
+    
+	// Insert the mapping
+	OscInput input;
+    
+    input.oscParamNumber = oscParamNumber;
+    input.oscMinValue = oscMinValue;
+    input.oscMaxValue = oscMaxValue;
+    
+    // Calculate the normalized center value which will be subtracted
+    // from the scaled input. Do this once now to save computation.
+    if(oscMinValue == oscMaxValue)
+        input.oscScaledCenterValue = 0;
+    else {
+        input.oscScaledCenterValue = (oscCenterValue - oscMinValue) / (oscMaxValue - oscMinValue);
+        if(input.oscScaledCenterValue < 0)
+            input.oscScaledCenterValue = 0;
+        if(input.oscScaledCenterValue > 1.0)
+            input.oscScaledCenterValue = 1.0;
+    }
+    input.outOfRangeBehavior = outOfRangeBehavior;
+    input.uniqueId = lastUniqueId_++;
+    inputs_[oscPath] = input;
+	
+	// Register for the relevant OSC message
+	addOscListener(oscPath);
+}
+
+// Remove an existing OSC input
+void OscMidiConverter::removeControl(const string& oscPath) {
+    // Find the affected control and its ID
+    if(inputs_.count(oscPath) == 0)
+        return;
+    int controlId = inputs_[oscPath].uniqueId;
+    
+#ifdef DEBUG_OSC_MIDI_CONVERTER
+    std::cout << "OscMidiConverter: removing path " << oscPath << std::endl;
+#endif
+    
+    // Look for any active inputs on this channel
+    for(int i = 0; i < 16; i++) {
+        int channelModulatedId = idWithChannel(i, controlId);
+        if(lastValues_.count(channelModulatedId) == 0)
+            continue;
+        
+        // Found a last value. Subtract it off and get new value
+        float lastValueThisChannel = lastValues_[channelModulatedId];
+        currentValue_[i] -= lastValueThisChannel;
+        
+        // Remove this value from the set of active inputs
+        lastValues_.erase(channelModulatedId);
+        
+        // Send the new value after removing this one
+        sendCurrentValue(keyboardSegment_.outputPort(), i, -1, true);
+    }
+    
+    // Having removed any active inputs, now remove the control itself
+    // TODO: mutex protection
+    inputs_.erase(oscPath);
+    
+    removeOscListener(oscPath);
+}
+
+void OscMidiConverter::removeAllControls() {
+    // Clear all active inputs and send default values to all channels
+    for(int i = 0; i < 16; i++)
+        sendDefaultValue(i);
+    lastValues_.clear();
+    inputs_.clear();
+    lastUniqueId_ = 0;
+    removeAllOscListeners();
+}
+
+// Update the minimum input value of an existing path
+void OscMidiConverter::setControlMinValue(const string& oscPath, float newValue) {
+    if(inputs_.count(oscPath) == 0)
+        return;
+    inputs_[oscPath].oscMinValue = newValue;
+}
+
+// Update the maximum input value of an existing path
+void OscMidiConverter::setControlMaxValue(const string& oscPath, float newValue) {
+    if(inputs_.count(oscPath) == 0)
+        return;
+    inputs_[oscPath].oscMaxValue = newValue;
+}
+
+// Update the center input value of an existing path
+void OscMidiConverter::setControlCenterValue(const string& oscPath, float newValue) {
+    if(inputs_.count(oscPath) == 0)
+        return;
+    float minValue, maxValue, scaledCenterValue;
+    minValue = inputs_[oscPath].oscMinValue;
+    maxValue = inputs_[oscPath].oscMaxValue;
+    
+    scaledCenterValue = (newValue - minValue) / (maxValue - minValue);
+    if(scaledCenterValue < 0)
+        scaledCenterValue = 0;
+    if(scaledCenterValue > 1.0)
+        scaledCenterValue = 1.0;
+    
+    inputs_[oscPath].oscScaledCenterValue = scaledCenterValue;
+}
+
+// Update the out of range behavior for an existing path
+void OscMidiConverter::setControlOutOfRangeBehavior(const string& oscPath, int newBehavior) {
+    if(inputs_.count(oscPath) == 0)
+        return;
+    inputs_[oscPath].outOfRangeBehavior = newBehavior;
+}
+
+// Reset any active previous values on the given channel
+// 'send' indicated whether to send the value when finished
+// even if it hasn't changed
+void OscMidiConverter::clearLastValues(int channel, bool send) {
+    std::map<int, float>::iterator it = lastValues_.begin();
+    
+    bool erased = false;
+    
+    while(it != lastValues_.end()) {
+        // Look for any last values matching this channel
+        if((it->first & 0x0F) == channel) {
+            lastValues_.erase(it++);
+            erased = true;
+        }
+        else
+            it++;
+    }
+ 
+    currentValue_[channel] = 0;
+    lastOutputValue_[channel] = -1;
+    
+    // If any last values were erased and send is enabled,
+    // resend the current values
+    if(erased || send)
+        sendDefaultValue(channel);
+}
+
+// OSC Handler, called by the data source (PianoKeyboard in this case). Check path against stored
+// inputs and map to MIDI accordingly
+
+bool OscMidiConverter::oscHandlerMethod(const char *path, const char *types, int numValues, lo_arg **values, void *data) {
+#ifdef DEBUG_OSC_MIDI_CONVERTER
+    std::cout << "OscMidiConverter: received path " << path << std::endl;
+#endif
+	if(midiOutputController_ == 0 || controller_ == MidiKeyboardSegment::kControlDisabled)
+		return false;
+    
+    //double before = Time::getMillisecondCounterHiRes();  // DEBUG
+    
+	// First value should always be MIDI note number (integer type) so we can retrieve
+	// information from the PianoKeyboard class
+	if(numValues < 1)
+		return false;
+	if(types[0] != 'i')
+		return false;
+	int midiNoteNumber = values[0]->i;
+    
+	// Get the MIDI retransmission channel from the note number
+	if(keyboard_.key(midiNoteNumber) == 0)
+		return false;
+	int midiChannel = keyboard_.key(midiNoteNumber)->midiChannel();
+	if(midiChannel < 0 || midiChannel > 15) {
+#ifdef DEBUG_OSC_MIDI_CONVERTER
+        std::cout << "OscMidiConverter: no retransmission channel on note " << midiNoteNumber << std::endl;
+#endif
+		return false;
+    }
+    
+    // Find the relevant input and make sure this OSC message has enough parameters
+    if(inputs_.count(path) == 0)
+        return false;
+    OscInput const& input = inputs_[path];
+    if(input.oscParamNumber >= numValues)
+        return false;
+    
+    // Find the relevant input value from this OSC message
+    float oscParamValue;
+    if(types[input.oscParamNumber] == 'f')
+        oscParamValue = values[input.oscParamNumber]->f;
+    else if(types[input.oscParamNumber] == 'i')
+        oscParamValue = (float)values[input.oscParamNumber]->i;
+    else
+        return false;
+    
+    // Scale input to a 0-1 range, the to the output range
+    float scaledValue = (oscParamValue - input.oscMinValue) / (input.oscMaxValue - input.oscMinValue);
+
+#ifdef DEBUG_OSC_MIDI_CONVERTER
+    std::cout << "port " << keyboardSegment_.outputPort() << " received input " << oscParamValue << " which scales to " << scaledValue << std::endl;
+#endif
+    
+    // Figure out what to do with an out of range value...
+    if(scaledValue < 0.0 || scaledValue > 1.0) {
+        if(input.outOfRangeBehavior == kOutOfRangeClip) {
+            if(scaledValue < 0.0)
+                scaledValue = 0.0;
+            if(scaledValue > 1.0)
+                scaledValue = 1.0;
+        }
+        else if(input.outOfRangeBehavior == kOutOfRangeExtrapolate) {
+            // Do nothing
+        }
+        else	// Ignore or unknown behavior
+            return false;
+    }
+    
+    //double midpoint = Time::getMillisecondCounterHiRes(); // DEBUG
+    
+    // Now subtract the normalized center value, which may put the range outside 0-1
+    // but this is expected. For example, in a pitch wheel situation, we might move
+    // to a range of -0.5 to 0.5.
+    scaledValue -= input.oscScaledCenterValue;
+
+    // Look for previous input with this path and channel and remove it
+    int channelModulatedId = idWithChannel(midiChannel, input.uniqueId);
+    if(lastValues_.count(channelModulatedId) > 0) {
+        // Found a last value. Subtract it off and replace with our current value
+        float lastValueThisChannel = lastValues_[channelModulatedId];
+        currentValue_[midiChannel] -= lastValueThisChannel;
+#ifdef DEBUG_OSC_MIDI_CONVERTER
+        std::cout << "found and removed " << lastValueThisChannel << ", now have " << currentValue_[midiChannel] << std::endl;
+#endif
+    }
+    
+    lastValues_[channelModulatedId] = scaledValue;
+    currentValue_[midiChannel] += scaledValue;
+
+    // Send the total current value as a MIDI controller
+    sendCurrentValue(keyboardSegment_.outputPort(), midiChannel, midiNoteNumber, false);
+    
+    //double after = Time::getMillisecondCounterHiRes(); // DEBUG
+    
+    //std::cout << "OscMidiConverter: total " << after - before << "ms, of which " << after - midpoint << " in 2nd half\n";
+    
+	return true;
+}
+
+// Send a MIDI RPN message to set the pitch wheel range
+void OscMidiConverter::sendPitchWheelRange(int port, int channel) {
+    if(midiOutputController_ == 0)
+        return;
+    
+    // KLUDGE: fix by using MidiKeyboardSegment version
+    
+    // Find number of semitones and cents
+    int majorRange = (int)floorf(pitchWheelRange_);
+    int minorRange = (int)(100.0 * (pitchWheelRange_ - floorf(pitchWheelRange_)));
+    
+    // Set RPN controller = 0
+    midiOutputController_->sendControlChange(port, channel, 101, 0);
+    midiOutputController_->sendControlChange(port, channel, 100, 0);
+    // Set data value MSB/LSB for bend range in semitones
+    midiOutputController_->sendControlChange(port, channel, 6, majorRange);
+    midiOutputController_->sendControlChange(port, channel, 38, minorRange);
+    // Set RPN controller back to 16383
+    midiOutputController_->sendControlChange(port, channel, 101, 127);
+    midiOutputController_->sendControlChange(port, channel, 100, 127);
+}
+
+// Send the current sum value of all OSC inputs as a MIDI message
+void OscMidiConverter::sendCurrentValue(int port, int channel, int note, bool force) {
+    if(midiOutputController_ == 0 || channel < 0 || channel > 15)
+        return;
+    
+    // TODO: what about values that are centered by default e.g. pitch?
+    /*float controlValue = (float)controlCenterValue_ + (float)controlMinValue_
+                            + currentValue_[channel]*(float)(controlMaxValue_ - controlMinValue_);
+    
+    if(incomingController_ != MidiKeyboardSegment::kControlDisabled)
+        controlValue += keyboardSegment_.controllerValue(incomingController_) - incomingControllerCenterValue_;
+    
+    std::cout << "sending value " << currentValue_[channel] << " as " << controlValue << std::endl;
+    int roundedControlValue = (int)roundf(controlValue);
+    if(roundedControlValue > controlMaxValue_)
+        roundedControlValue = controlMaxValue_;
+    if(roundedControlValue < controlMinValue_)
+        roundedControlValue = controlMinValue_;*/
+    
+    int roundedControlValue = currentControllerValue(channel);
+    
+    // If this is the same ultimate CC value as before, don't resend unless forced
+    if(roundedControlValue == lastOutputValue_[channel] && !force)
+        return;
+    lastOutputValue_[channel] = roundedControlValue;
+    
+    // Four cases: Pitch Wheel messages, aftertouch, 14-bit controls (major and minor controllers), ordinary 7-bit controls
+    if(controller_ == MidiKeyboardSegment::kControlPitchWheel) {
+        midiOutputController_->sendPitchWheel(port, channel, roundedControlValue);
+    }
+    else if(controller_ == MidiKeyboardSegment::kControlChannelAftertouch) {
+        midiOutputController_->sendAftertouchChannel(port, channel, roundedControlValue);
+    }
+    else if(controller_ == MidiKeyboardSegment::kControlPolyphonicAftertouch && note >= 0) {
+        midiOutputController_->sendAftertouchPoly(port, channel, note, roundedControlValue);
+    }
+    else if(controllerIs14Bit_) {
+        // LSB for controllers 0-31 are found on controllers 32-63
+        int lsb = roundedControlValue & 0x007F;
+        int msb = (roundedControlValue >> 7) & 0x007F;
+        
+        midiOutputController_->sendControlChange(port, channel, controller_, msb);
+        midiOutputController_->sendControlChange(port, channel, controller_ + 32, lsb);
+    }
+    else {	// 7 bit
+        midiOutputController_->sendControlChange(port, channel, controller_, roundedControlValue);
+    }
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/TouchKeys/OscMidiConverter.h	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,152 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+ 
+  OscMidiConverter.h: converts incoming OSC messages to outgoing MIDI
+  messages with adjustable ranges and parameters.
+*/
+
+#ifndef __touchkeys__OscMidiConverter__
+#define __touchkeys__OscMidiConverter__
+
+#include <vector>
+#include "Osc.h"
+#include "MidiOutputController.h"
+#include "PianoKeyboard.h"
+#include "MidiKeyboardSegment.h"
+
+/* OscMidiConverter
+ *
+ * This class handles the sending of MIDI output messages of a particular type
+ * (control change, aftertouch, pitch wheel) in response to incoming OSC messages.
+ * Each object takes responsibility for one type of MIDI output but can take
+ * several types of OSC message to control it.
+ */
+
+class OscMidiConverter : public OscHandler {
+public:
+    // Behavior for out-of-range inputs.
+    enum {
+        kOutOfRangeIgnore = 0,
+        kOutOfRangeClip,
+        kOutOfRangeExtrapolate
+    };
+
+private:
+    // Structure holding information about a given OSC source
+	struct OscInput {
+        int uniqueId;               // ID to keep track of recent values
+		int oscParamNumber;         // Parameter number in the OSC message we map
+		float oscMinValue;          // Min and max of its input range
+		float oscMaxValue;
+        float oscScaledCenterValue; // Value of the input that should correspond to control center,
+                                    // pre-normalized to 0-1 range
+		int outOfRangeBehavior;     // What happens at the edge of the range
+	};
+    
+public:
+	// Constructor
+	OscMidiConverter(PianoKeyboard& keyboard, MidiKeyboardSegment& segment, int controllerId);
+	
+	// ***** MIDI methods *****
+    
+    // Provide a reference to the MidiOutputController which actually sends the messages
+	void setMidiOutputController(MidiOutputController* m) { midiOutputController_ = m; }
+	
+    // Set which control this object is handling. If minimum or maximum values are specified,
+    // then the control will never exceed this range no matter what OSC messages come in.
+    // use14BitControl specifies whether a 14-bit (paired) MIDI CC message should be used,
+    // for relevant values of message.
+    void setMidiMessageType(int defaultValue = -1, int minValue = -1,
+                            int maxValue = -1, int centerValue = -1, bool use14BitControl = false);
+    
+    // Specifically when MIDI Pitch Wheel is used as the destination, this sets the range
+    // in semitones which requires a separate MIDI RPN message to be sent to each channel.
+    void setMidiPitchWheelRange(float semitones) { pitchWheelRange_ = semitones; }
+    
+    // Set whether this converter passes through incoming CC messages from the MIDI input,
+    // and if so, which one. Doesn't need to be the same CC coming in as going out.
+    void listenToIncomingControl(int controller, int centerValue = -1, bool use14BitControl = false);
+    
+    // Force a resend of the current value
+    void resend(int channel);
+    
+    // Send the default controller value on the specified channel. Typically used
+    // before note onset.
+    void sendDefaultValue(int channel);
+    
+    // Return the current value of the MIDI controller without sending it
+    int currentControllerValue(int channel);
+    
+	// ***** OSC methods *****
+	
+    // This message specifies an OSC path to be mapped to MIDI, along with its input ranges which
+    // correspond to the complete specified MIDI range.
+	void addControl(const string& oscPath, int oscParamNumber, float oscMinValue, float oscMaxValue,
+                    float oscCenterValue, int outOfRangeBehavior);
+	void removeControl(const string& oscPath);
+	void removeAllControls();
+    
+    // These methods update the range for an existing control
+    void setControlMinValue(const string& oscPath, float newValue);
+    void setControlMaxValue(const string& oscPath, float newValue);
+    void setControlCenterValue(const string& oscPath, float newValue);
+    void setControlOutOfRangeBehavior(const string& oscPath, int newBehavior);
+	
+    // Reset any active previous values on the given channel
+    void clearLastValues(int channel, bool send = true);
+    
+	// OSC Handler Method: called by PianoKeyboard (or other OSC source)
+	bool oscHandlerMethod(const char *path, const char *types, int numValues, lo_arg **values, void *data);
+	
+	// Destructor
+	~OscMidiConverter() {}
+	
+private:
+    // ***** Private Methods *****
+    int idWithChannel(int channel, int inputId) { return (inputId << 4) + channel; }
+    void sendPitchWheelRange(int port, int channel);
+    void sendCurrentValue(int port, int channel, int note, bool force);
+    
+	// ***** Member Variables *****
+	
+	PianoKeyboard& keyboard_;						// Main piano keyboard controller
+    MidiKeyboardSegment& keyboardSegment_;          // Which segment of the keyboard this mapping is using
+	MidiOutputController* midiOutputController_;	// Class handling MIDI output
+    
+    int controller_;                                // Which MIDI control to use
+    bool controllerIs14Bit_;                        // Whether to use a paired MIDI CC
+    int controlMinValue_, controlMaxValue_;         // Ranges control can take
+    int controlCenterValue_;                        // The center value to use when all OSC inputs are 0
+    int controlDefaultValue_;                       // Default value for the control on new notes
+
+    int lastUniqueId_;                              // Global unique ID for input messages
+    float pitchWheelRange_;                         // Range of MIDI pitch wheel (if used)
+    
+    int incomingController_;                         // Which controller we listen to from the MIDI input
+    bool incomingControllerIs14Bit_;                // Whether the input controller is 14 bit
+    int incomingControllerCenterValue_;             // The center value to subtract from the incoming controller
+    
+    std::map<std::string, OscInput> inputs_;        // OSC sources for this MIDI output
+    std::map<int, float> lastValues_;               // Recently received values from each OSC input
+    
+    float currentValue_[16];                        // Current sum value of all inputs for each channel
+    int lastOutputValue_[16];                       // The last value we sent out; saved to avoid duplicate messages
+};
+
+#endif /* defined(__touchkeys__OscMidiConverter__) */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/TouchKeys/PianoKey.cpp	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,821 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+ 
+  PianoKey.cpp: handles the operation of a single key on the keyboard,
+  including fusing touch and MIDI data. Also has hooks for continuous
+  key angle.
+*/
+
+#include "PianoKey.h"
+#include "PianoKeyboard.h"
+#include "../Mappings/MappingFactory.h"
+
+#undef TOUCHKEYS_LEGACY_OSC
+
+// Default constructor
+PianoKey::PianoKey(PianoKeyboard& keyboard, int noteNumber, int bufferLength) 
+: TriggerDestination(), keyboard_(keyboard), noteNumber_(noteNumber),
+  midiNoteIsOn_(false), midiChannel_(-1), midiOutputPort_(0), midiVelocity_(0),
+  midiAftertouch_(bufferLength), midiOnTimestamp_(0), midiOffTimestamp_(0),
+  positionBuffer_(bufferLength),
+  idleDetector_(kPianoKeyIdleBufferLength, positionBuffer_, kPianoKeyDefaultIdlePositionThreshold,
+              kPianoKeyDefaultIdleActivityThreshold, kPianoKeyDefaultIdleCounter),
+  positionTracker_(kPianoKeyPositionTrackerBufferLength, positionBuffer_),
+  stateBuffer_(kPianoKeyStateBufferLength), state_(kKeyStateToBeInitialized),
+  touchSensorsArePresent_(true), touchIsActive_(false), 
+  touchBuffer_(bufferLength), touchIsWaiting_(false), touchWaitingSource_(0),
+  touchWaitingTimestamp_(0),
+  touchTimeoutInterval_(kPianoKeyDefaultTouchTimeoutInterval)
+{    
+	enable();
+	registerForTrigger(&idleDetector_);
+}
+
+// Destructor
+PianoKey::~PianoKey() {
+    // Remove any mappings we've created
+    //keyboard_.removeMapping(noteNumber_);
+}
+
+
+// Disable the key from sending events.  Do this by removing anything that
+// listens to its status.
+void PianoKey::disable() {
+	ScopedLock sl(stateMutex_);
+    
+	if(state_ == kKeyStateDisabled) {
+		return;
+	}
+	// No longer run the idle comparator.  This ensures that no further state
+	// changes take place, and that the idle coefficient is not calculated.
+	//idleComparator_.clearTriggers();
+	
+	terminateActivity();
+	changeState(kKeyStateDisabled);	
+}
+
+// Start listening for key activity.  This will allow the state to transition to
+// idle, and then to active as appropriate.
+void PianoKey::enable() {
+	ScopedLock sl(stateMutex_);
+    
+	if(state_ != kKeyStateDisabled) {
+		return;
+	}
+	changeState(kKeyStateUnknown);	
+}
+
+// Reset the key to its default state
+void PianoKey::reset() {
+	ScopedLock sl(stateMutex_);
+	
+	terminateActivity();		// Stop any current activity
+	positionBuffer_.clear();	// Clear all history
+	stateBuffer_.clear();
+	idleDetector_.clear();
+	changeState(kKeyStateUnknown);	// Reinitialize with unknown state
+}
+
+// Insert a new sample in the key buffer
+void PianoKey::insertSample(key_position pos, timestamp_type ts) {
+    positionBuffer_.insert(pos, ts);
+    
+    if((timestamp_diff_type)ts - (timestamp_diff_type)timeOfLastGuiUpdate_ > kPianoKeyGuiUpdateInterval) {
+        timeOfLastGuiUpdate_ = ts;
+        if(keyboard_.gui() != 0) {
+            keyboard_.gui()->setAnalogValueForKey(noteNumber_, pos);
+        }
+    }
+    
+    /*if((timestamp_diff_type)ts - (timestamp_diff_type)timeOfLastDebugPrint_ > 1.0) {
+        timeOfLastDebugPrint_ = ts;
+        key_position kmin = missing_value<key_position>::missing(), kmax = missing_value<key_position>::missing();
+        key_position mean = 0;
+        int count = 0;
+        Node<key_position>::iterator it = positionBuffer_.begin();
+        while(it != positionBuffer_.end()) {
+            if(missing_value<key_position>::isMissing(*it))
+               continue;
+            if(missing_value<key_position>::isMissing(kmin) || *it < kmin)
+                kmin = *it;
+            if(missing_value<key_position>::isMissing(kmax) || *it > kmax)
+                kmax = *it;
+            mean += *it;
+            it++;
+            count++;
+        }
+        mean /= (key_position)count;
+        
+        key_position var = 0;
+        it = positionBuffer_.begin();
+        while(it != positionBuffer_.end()) {
+            if(missing_value<key_position>::isMissing(*it))
+                continue;
+            var += (*it - mean)*(*it - mean);
+            it++;
+        }
+        var /= (key_position)count;
+        
+        std::cout << "Key " << noteNumber_ << " mean " << mean << " var " << var << std::endl;
+    }*/
+}
+
+// If a key is active, force it to become idle, stopping any processes that it has created
+void PianoKey::forceIdle() {
+	ScopedLock sl(stateMutex_);
+    
+	if(state_ == kKeyStateDisabled || state_ == kKeyStateIdle) {
+		return;
+	}
+	terminateActivity();
+	changeState(kKeyStateIdle);
+}
+
+// Handle triggers sent when specific conditions are met (called by various objects)
+
+void PianoKey::triggerReceived(TriggerSource* who, timestamp_type timestamp) {
+	ScopedLock sl(stateMutex_);
+	
+	if(who == &idleDetector_) {
+		//std::cout << "Key " << noteNumber_ << ": IdleDetector says: " << idleDetector_.latest() << std::endl;
+		
+		if(idleDetector_.latest() == kIdleDetectorIdle) {
+            cout << "Key " << noteNumber_ << " --> Idle\n";
+            
+            keyboard_.tellAllMappingFactoriesKeyMotionIdle(noteNumber_, midiNoteIsOn_, touchIsActive_,
+                                                           &touchBuffer_, &positionBuffer_, &positionTracker_);
+            
+            // Remove any mapping present on this key
+            //keyboard_.removeMapping(noteNumber_);
+            
+            positionTracker_.disengage();
+            unregisterForTrigger(&positionTracker_);
+			terminateActivity();
+			changeState(kKeyStateIdle);
+            keyboard_.setKeyLEDColorRGB(noteNumber_, 0, 0, 0);
+		}
+		else if(idleDetector_.latest() == kIdleDetectorActive && state_ != kKeyStateUnknown) {
+            cout << "Key " << noteNumber_ << " --> Active\n";
+            
+            keyboard_.tellAllMappingFactoriesKeyMotionActive(noteNumber_, midiNoteIsOn_, touchIsActive_,
+                                                             &touchBuffer_, &positionBuffer_, &positionTracker_);
+            
+			// Only allow transition to active from a known previous state
+			// TODO: set up min/max listener
+			// TODO: may want to change the parameters on the idleDetector
+			changeState(kKeyStateActive);
+            //keyboard_.setKeyLEDColorRGB(noteNumber_, 1.0, 0.0, 0);
+            
+            // Engage the position tracker that handles specific measurement of key states
+            registerForTrigger(&positionTracker_);
+            positionTracker_.reset();
+            positionTracker_.engage();
+            
+            // Allocate a new mapping that converts key position gestures to sound
+            // control messages. TODO: how do we handle this with the TouchKey data too?
+            //MRPMapping *mapping = new MRPMapping(keyboard_, noteNumber_, &touchBuffer_,
+            //                                   &positionBuffer_, &positionTracker_);
+            //MIDIKeyPositionMapping *mapping = new MIDIKeyPositionMapping(keyboard_, noteNumber_, &touchBuffer_,
+            //                                                             &positionBuffer_, &positionTracker_);
+            //keyboard_.addMapping(noteNumber_, mapping);
+            //mapping->setPercussivenessMIDIChannel(1);
+            //mapping->engage();
+		}
+	}
+    else if(who == &positionTracker_ && !positionTracker_.empty()) {
+        KeyPositionTrackerNotification notification = positionTracker_.latest();
+        
+        if(notification.type == KeyPositionTrackerNotification::kNotificationTypeStateChange) {
+            int positionTrackerState = notification.state;
+            
+            KeyPositionTracker::Event recentEvent;
+            std::pair<timestamp_type, key_velocity> velocityInfo;
+            cout << "Key " << noteNumber_ << " --> State " << positionTrackerState << endl;
+            
+            switch(positionTrackerState) {
+                case kPositionTrackerStatePartialPressAwaitingMax:
+                    //keyboard_.setKeyLEDColorRGB(noteNumber_, 1.0, 0.0, 0);
+                    recentEvent = positionTracker_.pressStart();
+                    cout << "  start = (" << recentEvent.index << ", " << recentEvent.position << ", " << recentEvent.timestamp << ")\n";
+                    break;
+                case kPositionTrackerStatePartialPressFoundMax:
+                    //keyboard_.setKeyLEDColorRGB(noteNumber_, 1.0, 0.6, 0);
+                    recentEvent = positionTracker_.currentMax();
+                    cout << "  max = (" << recentEvent.index << ", " << recentEvent.position << ", " << recentEvent.timestamp << ")\n";
+                    break;
+                case kPositionTrackerStatePressInProgress:                    
+                    //keyboard_.setKeyLEDColorRGB(noteNumber_, 0.8, 0.8, 0);
+                    velocityInfo = positionTracker_.pressVelocity();
+                    cout << "  escapement time = " << velocityInfo.first << " velocity = " << velocityInfo.second << endl;
+                    break;
+                case kPositionTrackerStateDown:
+                    //keyboard_.setKeyLEDColorRGB(noteNumber_, 0, 1.0, 0);
+                    recentEvent = positionTracker_.pressStart();
+                    cout << "  start = (" << recentEvent.index << ", " << recentEvent.position << ", " << recentEvent.timestamp << ")\n";
+                    recentEvent = positionTracker_.pressFinish();
+                    cout << "  finish = (" << recentEvent.index << ", " << recentEvent.position << ", " << recentEvent.timestamp << ")\n";
+                    velocityInfo = positionTracker_.pressVelocity();
+                    cout << "  escapement time = " << velocityInfo.first << " velocity = " << velocityInfo.second << endl;
+                    
+                    if(keyboard_.graphGUI() != 0) {
+                        keyboard_.graphGUI()->setKeyPressStart(positionTracker_.pressStart().position, positionTracker_.pressStart().timestamp);
+                        keyboard_.graphGUI()->setKeyPressFinish(positionTracker_.pressFinish().position, positionTracker_.pressFinish().timestamp);
+                        keyboard_.graphGUI()->copyKeyDataFromBuffer(positionBuffer_, positionTracker_.pressStart().index - 10,
+                                                                    positionBuffer_.endIndex());
+                    }
+                    break;
+                case kPositionTrackerStateReleaseInProgress:
+                    //keyboard_.setKeyLEDColorRGB(noteNumber_, 0, 0, 1.0);
+                    recentEvent = positionTracker_.releaseStart();
+                    cout << "  start = (" << recentEvent.index << ", " << recentEvent.position << ", " << recentEvent.timestamp << ")\n";
+                    if(keyboard_.graphGUI() != 0) {
+                        keyboard_.graphGUI()->setKeyReleaseStart(positionTracker_.releaseStart().position, positionTracker_.releaseStart().timestamp);
+                        keyboard_.graphGUI()->copyKeyDataFromBuffer(positionBuffer_, positionTracker_.pressStart().index - 10,
+                                                                    positionBuffer_.endIndex());
+                        //keyboard_.graphGUI()->copyKeyDataFromBuffer(testFilter_, testFilter_.indexNearestTo(positionBuffer_.timestampAt(positionTracker_.pressStart().index - 10)), testFilter_.endIndex());
+                    }
+                    break;
+                case kPositionTrackerStateReleaseFinished:
+                    //keyboard_.setKeyLEDColorRGB(noteNumber_, 0.5, 0, 1.0);
+                    recentEvent = positionTracker_.releaseFinish();
+                    cout << "  finish = (" << recentEvent.index << ", " << recentEvent.position << ", " << recentEvent.timestamp << ")\n";
+                    if(keyboard_.graphGUI() != 0) {
+                        keyboard_.graphGUI()->setKeyReleaseStart(positionTracker_.releaseStart().position, positionTracker_.releaseStart().timestamp);
+                        keyboard_.graphGUI()->setKeyReleaseFinish(positionTracker_.releaseFinish().position, positionTracker_.releaseFinish().timestamp);
+                        keyboard_.graphGUI()->copyKeyDataFromBuffer(positionBuffer_, positionTracker_.pressStart().index - 10,
+                                                                    positionBuffer_.endIndex());
+                        //keyboard_.graphGUI()->copyKeyDataFromBuffer(testFilter_, testFilter_.indexNearestTo(positionBuffer_.timestampAt(positionTracker_.pressStart().index - 10)), testFilter_.endIndex());
+                    }
+                    break;
+                default:
+                    break;
+            }
+        }
+    }
+}
+
+// Update the current state
+
+void PianoKey::changeState(key_state newState) {
+	if(!positionBuffer_.empty())
+		changeState(newState, positionBuffer_.latestTimestamp());
+	else
+		changeState(newState, 0);
+}
+
+void PianoKey::changeState(key_state newState, timestamp_type timestamp) {
+	stateBuffer_.insert(newState, timestamp);
+	state_ = newState;
+}
+
+// Stop any activity that's currently taking place on account of the key motion
+
+void PianoKey::terminateActivity() {
+	
+}
+
+#pragma mark MIDI Methods
+// ***** MIDI Methods *****
+
+// Note On message from associated MIDI keyboard: record channel we should use
+// for the duration of this note, as well as the note's velocity
+void PianoKey::midiNoteOn(MidiKeyboardSegment *who, int velocity, int channel, timestamp_type timestamp) {
+	midiNoteIsOn_ = true;
+	midiChannel_ = channel;
+	midiVelocity_ = velocity;
+	midiOnTimestamp_ = timestamp;
+
+    if(keyboard_.mappingFactory(who) != 0)
+        keyboard_.mappingFactory(who)->midiNoteOn(noteNumber_, touchIsActive_, (idleDetector_.idleState() == kIdleDetectorActive),
+                                               &touchBuffer_, &positionBuffer_, &positionTracker_);
+
+    // Start the note right away unless we either need to wait for a touch or a delay has been enforced
+	if((touchIsActive_ && !touchBuffer_.empty()) || !touchSensorsArePresent_ || (touchTimeoutInterval_ == 0)) {
+        midiNoteOnHelper(who);
+	}
+	else {
+		// If touch isn't yet active, we might delay the note onset for a short
+		// time to wait for it.
+		
+		// Cases:
+		//   (1) Touch was active --> send above messages and tell the MidiController to go ahead
+		//      (a) Channel Selection mode: OSC messages will be used to choose a channel; messages
+		//          that translate to control changes need to be archived and resent once the channel is known
+		//      (b) Polyphonic and other modes: OSC messages used to generate control changes; channel
+		//          is already known
+		//   (2) Touch not yet active --> schedule a note on for a specified time interval in the future
+		//      (a) Touch event arrives on this key before that --> send OSC and call the MidiController,
+		//          removing the scheduled future event
+		//      (b) No touch event arrives --> future event triggers without touch info, call MidiController
+		//          and tell it to use defaults
+        touchWaitingSource_ = who;
+		touchIsWaiting_ = true;
+		touchWaitingTimestamp_ = keyboard_.schedulerCurrentTimestamp() + touchTimeoutInterval_;
+		keyboard_.scheduleEvent(this, 
+								boost::bind(&PianoKey::touchTimedOut, this),
+								touchWaitingTimestamp_);
+	}
+}
+
+// This private method does the real work of midiNoteOn().  It's separated because under certain
+// circumstances, the helper function is called in a delayed manner, after a touch has been received.
+void PianoKey::midiNoteOnHelper(MidiKeyboardSegment *who) {
+	touchIsWaiting_ = false;
+    touchWaitingSource_ = 0;
+
+	if(!touchBuffer_.empty()) {
+		const KeyTouchFrame& frame(touchBuffer_.latest());
+		int indexOfFirstTouch = 0;
+		
+		// Find which touch happened first so we can report its location
+		for(int i = 0; i < frame.count; i++) {
+			if(frame.ids[i] < frame.ids[indexOfFirstTouch])
+				indexOfFirstTouch = i;
+		}
+		
+		// Send a message reporting the touch location of the first touch and the
+		// current number of touches.  The target (either MidiInputController or external)
+		// may use this to change its behavior independently of later changes in touch.
+		
+		keyboard_.sendMessage("/touchkeys/preonset", "iiiiiiffiffifff",
+							  noteNumber_, midiChannel_, midiVelocity_,	// MIDI data
+							  frame.count, indexOfFirstTouch,	// General information: how many touches, which was first?
+							  frame.ids[0], frame.locs[0], frame.sizes[0], // Specific touch information
+							  frame.ids[1], frame.locs[1], frame.sizes[1],
+							  frame.ids[2], frame.locs[2], frame.sizes[2],
+							  frame.locH, LO_ARGS_END);
+
+		// ----
+		// The above function will trigger the callback in MidiInputController, if it is enabled.
+		// Therefore, the calls below will take place after MidiInputController has handled its callback.
+		// ----
+
+#ifdef TOUCHKEYS_LEGACY_OSC
+		// Send move and resize gestures for each active touch
+		for(int i = 0; i < frame.count; i++) {
+			keyboard_.sendMessage("/touchkeys/move", "iiff", noteNumber_, frame.ids[i],
+								  frame.locs[i], frame.horizontal(i), LO_ARGS_END);
+			keyboard_.sendMessage("/touchkeys/resize", "iif", noteNumber_, frame.ids[i],
+								  frame.sizes[i], LO_ARGS_END);										
+		}
+		
+		// If more than one touch is present, resend any pinch and slide gestures
+		// before we start.
+		if(frame.count == 2) {
+			float newCentroid = (frame.locs[0] + frame.locs[1]) / 2.0;
+			float newWidth = frame.locs[1] - frame.locs[0];	
+			
+			keyboard_.sendMessage("/touchkeys/twofinger/pinch", "iiif",
+								  noteNumber_, frame.ids[0], frame.ids[1], newWidth, LO_ARGS_END);
+			keyboard_.sendMessage("/touchkeys/twofinger/slide", "iiif",
+								  noteNumber_, frame.ids[0], frame.ids[1], newCentroid, LO_ARGS_END);			
+		}
+		else if(frame.count == 3) {
+			float newCentroid = (frame.locs[0] + frame.locs[1] + frame.locs[2]) / 3.0;
+			float newWidth = frame.locs[2] - frame.locs[0];
+			
+			keyboard_.sendMessage("/touchkeys/threefinger/pinch", "iiiif",
+								  noteNumber_, frame.ids[0], frame.ids[1], frame.ids[2], newWidth, LO_ARGS_END);
+			keyboard_.sendMessage("/touchkeys/threefinger/slide", "iiiif",
+								  noteNumber_, frame.ids[0], frame.ids[1], frame.ids[2], newCentroid, LO_ARGS_END);			
+		}
+#endif
+	}
+    
+    // Before the note starts, inform the mapping factory in case there are default values to be sent out.
+    if(keyboard_.mappingFactory(who) != 0)
+        keyboard_.mappingFactory(who)->noteWillBegin(noteNumber_, midiChannel_, midiVelocity_);
+	
+	keyboard_.sendMessage("/midi/noteon", "iii", noteNumber_, midiChannel_, midiVelocity_, LO_ARGS_END);
+    
+    // Update GUI if it is available. TODO: fix the ordering problem for real!
+	if(keyboard_.gui() != 0 && midiNoteIsOn_) {
+		keyboard_.gui()->setMidiActive(noteNumber_, true);
+	}
+}
+
+// Note Off message from associated MIDI keyboard.  Clear all old MIDI state.
+void PianoKey::midiNoteOff(MidiKeyboardSegment *who, timestamp_type timestamp) {
+	midiNoteIsOn_ = false;
+	midiVelocity_ = 0;
+	midiChannel_ = -1;
+	midiAftertouch_.clear();
+	midiOffTimestamp_ = timestamp;
+    
+    if(keyboard_.mappingFactory(who) != 0)
+        keyboard_.mappingFactory(who)->midiNoteOff(noteNumber_, touchIsActive_, (idleDetector_.idleState() == kIdleDetectorActive),
+                                               &touchBuffer_, &positionBuffer_, &positionTracker_);
+    
+	keyboard_.sendMessage("/midi/noteoff", "ii", noteNumber_, midiChannel_, LO_ARGS_END);
+    
+    // Update GUI if it is available
+	if(keyboard_.gui() != 0) {
+		keyboard_.gui()->setMidiActive(noteNumber_, false);
+	}
+}
+
+// Aftertouch (either channel or polyphonic) message from associated MIDI keyboard
+void PianoKey::midiAftertouch(MidiKeyboardSegment *who, int value, timestamp_type timestamp) {
+	if(!midiNoteIsOn_)
+		return;
+	midiAftertouch_.insert(value, timestamp);
+	
+	keyboard_.sendMessage("/midi/aftertouch-poly", "iii", noteNumber_, midiChannel_, value, LO_ARGS_END);
+}
+
+#pragma mark Touch Methods
+// ***** Touch Methods *****
+
+// Insert a new frame of touchkey data, making any necessary status changes
+// (i.e. touch active, possibly changing number of active touches)
+
+void PianoKey::touchInsertFrame(KeyTouchFrame& newFrame, timestamp_type timestamp) {
+    if(!touchSensorsArePresent_)
+        return;
+    
+	// First check if the key was previously inactive.  If so, send a message
+	// that the touch has begun
+	if(!touchIsActive_) {
+		keyboard_.sendMessage("/touchkeys/on", "i", noteNumber_, LO_ARGS_END);
+        keyboard_.tellAllMappingFactoriesTouchBegan(noteNumber_, midiNoteIsOn_, (idleDetector_.idleState() == kIdleDetectorActive),
+                                                    &touchBuffer_, &positionBuffer_, &positionTracker_);
+    }
+	
+	touchIsActive_ = true;
+	
+	// If previous touch frames are present on this key, check the preceding
+	// frame to see if the state has changed in any important ways
+	if(!touchBuffer_.empty()) {
+		const KeyTouchFrame& lastFrame(touchBuffer_.latest());
+		
+		// Next ID is the touch ID that should be used for any new touches.  Scoop this
+		// info from the last frame.
+		
+		newFrame.nextId = lastFrame.nextId;
+		
+		// Assign ID numbers to each touch.  This is easy if the number of touches
+		// from the previous frame to this one stayed the same, somewhat more complex
+		// if a touch was added or removed.
+		
+		if(newFrame.count > lastFrame.count) {
+			// One or more points have been added.  Match the new points to the old ones to figure out
+			// which points have been added, versus which moved from before.
+			
+			std::set<int> availableNewPoints;
+			for(int i = 0; i < newFrame.count; i++)
+				availableNewPoints.insert(i);
+			
+			std::list<int> ordering(touchMatchClosestPoints(lastFrame.locs, newFrame.locs, 3, 0, availableNewPoints, 0.0).second);
+			
+			// ordering tells us the index of the new point corresponding to each old index,
+			// e.g. {2, 0, 1} --> old point 0 goes to new point 2, old point 1 goes to new point 0, ...
+			
+			// new points are still in ascending position order, so we use this matching to assign unique IDs
+			// and send relevant "add" messages
+			
+			int counter = 0;
+			for(std::list<int>::iterator it = ordering.begin(); it != ordering.end(); ++it) {
+				newFrame.ids[*it] = lastFrame.ids[counter];
+				
+				if(newFrame.ids[*it] < 0) {
+					// Matching to a negative ID means the touch is new
+					
+					newFrame.ids[*it] = newFrame.nextId++;
+					touchAdd(newFrame, *it, timestamp);
+				}
+				else {
+#ifdef TOUCHKEYS_LEGACY_OSC
+					// Send "move" messages for the points that have moved
+                    if(fabsf(newFrame.locs[*it] - lastFrame.locs[counter]) > 0 /*moveThreshold_*/)
+						keyboard_.sendMessage("/touchkeys/move", "iiff", noteNumber_, newFrame.ids[*it],
+													 newFrame.locs[*it], newFrame.horizontal(*it), LO_ARGS_END);
+					if(fabsf(newFrame.sizes[*it] - lastFrame.sizes[counter]) > 0 /*resizeThreshold_*/)
+						keyboard_.sendMessage("/touchkeys/resize", "iif", noteNumber_, newFrame.ids[*it],
+													 newFrame.sizes[*it], LO_ARGS_END);
+#endif
+				}
+				
+				counter++;
+			}			
+		}
+		else if(newFrame.count < lastFrame.count) {
+			// One or more points have been removed.  Match the new points to the old ones to figure out
+			// which points have been removed, versus which moved from before.
+			
+			std::set<int> availableNewPoints;
+			for(int i = 0; i < 3; i++)		
+				availableNewPoints.insert(i);
+			
+			std::list<int> ordering(touchMatchClosestPoints(lastFrame.locs, newFrame.locs, 3, 0, availableNewPoints, 0.0).second);
+			
+			// ordering tells us the index of the new point corresponding to each old index,
+			// e.g. {2, 0, 1} --> old point 0 goes to new point 2, old point 1 goes to new point 0, ...
+			
+			// new points are still in ascending position order, so we use this matching to assign unique IDs
+			// and send relevant "add" messages
+			
+			int counter = 0;
+			for(std::list<int>::iterator it = ordering.begin(); it != ordering.end(); ++it) {
+				if(*it < newFrame.count) {
+					// Old index {counter} matches a valid new touch
+					
+					newFrame.ids[*it] = lastFrame.ids[counter];	// Match IDs for currently active touches
+					
+#ifdef TOUCHKEYS_LEGACY_OSC
+					// Send "move" messages for the points that have moved
+					if(fabsf(newFrame.locs[*it] - lastFrame.locs[counter]) > 0 /*moveThreshold_*/)
+						keyboard_.sendMessage("/touchkeys/move", "iiff", noteNumber_, newFrame.ids[*it],
+													 newFrame.locs[*it], newFrame.horizontal(*it), LO_ARGS_END);
+					if(fabsf(newFrame.sizes[*it] - lastFrame.sizes[counter]) > 0 /*resizeThreshold_*/)
+						keyboard_.sendMessage("/touchkeys/resize", "iif", noteNumber_, newFrame.ids[*it],
+													 newFrame.sizes[*it], LO_ARGS_END);
+#endif
+				}
+				else if(lastFrame.ids[counter] >= 0) {
+					// Old index {counter} matches an invalid new index, meaning a touch has been removed.
+					touchRemove(lastFrame, lastFrame.ids[counter], newFrame.count, timestamp);
+				}
+				
+				counter++;
+			}			
+		}
+		else {
+			// Same number of touches as before.  Touches are always stored in increasing order,
+			// so we just need to copy these over, maintaining the same ID numbers.
+			
+			for(int i = 0; i < newFrame.count; i++) {
+				newFrame.ids[i] = lastFrame.ids[i];
+				
+#ifdef TOUCHKEYS_LEGACY_OSC
+				// Send "move" messages for the points that have moved
+				if(fabsf(newFrame.locs[i] - lastFrame.locs[i]) > 0 /*moveThreshold_*/)
+					keyboard_.sendMessage("/touchkeys/move", "iiff", noteNumber_, newFrame.ids[i],
+												 newFrame.locs[i], newFrame.horizontal(i), LO_ARGS_END);
+				if(fabsf(newFrame.sizes[i] - lastFrame.sizes[i]) > 0 /*resizeThreshold_*/)
+					keyboard_.sendMessage("/touchkeys/resize", "iif", noteNumber_, newFrame.ids[i],
+												 newFrame.sizes[i], LO_ARGS_END);
+#endif
+			}
+			
+			// If the number of touches has stayed the same, look for multi-finger gestures (pinch and slide)
+			if(newFrame.count > 1) {
+				touchMultiFingerGestures(lastFrame, newFrame, timestamp);
+			}
+		}
+	}
+	else {
+		// With no previous frame to compare to, assign IDs to each active touch sequentially
+		
+		newFrame.nextId = 0;
+		for(int i = 0; i < newFrame.count; i++) {
+			newFrame.ids[i] = newFrame.nextId++;
+			touchAdd(newFrame, i, timestamp);
+		}
+	}
+	
+	// Add the new touch
+	touchBuffer_.insert(newFrame, timestamp);
+    
+	if(touchIsWaiting_) {
+		// If this flag was set, we were waiting for a touch to occur before taking further
+		// action.  A timeout will have been scheduled, which we should clear.
+		keyboard_.unscheduleEvent(this, touchWaitingTimestamp_);
+		
+		// Send the queued up MIDI/OSC events
+		midiNoteOnHelper(touchWaitingSource_);
+	}
+	
+	// Update GUI if it is available
+	if(keyboard_.gui() != 0) {
+		keyboard_.gui()->setTouchForKey(noteNumber_, newFrame);
+	}
+}
+
+// This is called when all touch is removed from a key.  Clear out the previous state
+
+void PianoKey::touchOff(timestamp_type timestamp) {
+	if(!touchIsActive_ || !touchSensorsArePresent_)
+		return;
+    
+    keyboard_.tellAllMappingFactoriesTouchEnded(noteNumber_, midiNoteIsOn_, (idleDetector_.idleState() == kIdleDetectorActive),
+                                               &touchBuffer_, &positionBuffer_, &positionTracker_);
+    
+	touchEvents_.clear();
+	
+	// Create a new event that records the timestamp of the idle event
+	// and the last frame before it occurred (but only if we have data
+	// on at least one frame before idle occurred)
+	if(!touchBuffer_.empty()) {
+		KeyTouchEvent event = { kTouchEventIdle, timestamp, touchBuffer_.latest() };
+		touchEvents_.insert(std::pair<int, KeyTouchEvent>(-1, event));
+	}		
+	
+    // Insert a blank touch frame into the buffer so anyone listening knows the touch has gone off
+    KeyTouchFrame emptyFrame;
+    emptyFrame.count = 0;
+    touchBuffer_.insert(emptyFrame, timestamp);
+    
+	// Send a message that the touch has ended
+	touchIsActive_ = false;
+	touchBuffer_.clear();
+    keyboard_.sendMessage("/touchkeys/off", "i", noteNumber_, LO_ARGS_END);
+	// Update GUI if it is available
+	if(keyboard_.gui() != 0) {
+		keyboard_.gui()->clearTouchForKey(noteNumber_);
+	}
+}
+
+// This function is called when we time out waiting for a touch on the given note
+
+timestamp_type PianoKey::touchTimedOut() {
+	//cout << "Touch timed out on note " << noteNumber_ << endl;
+	
+	// Do all the things we were planning to do once the touch was received.
+	midiNoteOnHelper(touchWaitingSource_);
+    
+    return 0;
+}
+
+// Recursive function for matching old and new frames of touch locations, each with up to (count) points
+//
+// Example: old points 1-3, new points A-C
+//   1A  *2A*  3A
+//  *1B*  2B   3B
+//   1C   2C  *3C*
+
+std::pair<float, std::list<int> > PianoKey::touchMatchClosestPoints(const float* oldPoints, const float *newPoints, float count,
+															   int oldIndex, std::set<int>& availableNewPoints, float currentTotalDistance) {
+	if(availableNewPoints.size() == 0)	// Shouldn't happen but prevent an infinite loop
+		throw new std::exception;
+	
+	// End case: only one possible point available
+	if(availableNewPoints.size() == 1) {
+		int newIndex = *(availableNewPoints.begin());
+		
+		std::list<int> singleOrder;
+		singleOrder.push_front(newIndex);
+		
+		if(oldPoints[oldIndex] < 0.0 || newPoints[newIndex] < 0.0) {
+			//if(verbose_ >= 4)
+			//	cout << " -> [" << newIndex << "] (" << currentTotalDistance + 100.0 << ")\n";
+			
+			// Return the distance between the last old point and the only available new point
+			return std::pair<float, std::list<int> > (currentTotalDistance + 100.0, singleOrder);			
+		}
+		else {
+			//if(verbose_ >= 4)
+			//	cout << " -> [" << newIndex << "] (" << currentTotalDistance + (oldPoints[oldIndex] - newPoints[newIndex])*(oldPoints[oldIndex] - newPoints[newIndex]) << ")\n";
+			
+			// Return the distance between the last old point and the only available new point
+			return std::pair<float, std::list<int> > (currentTotalDistance + (oldPoints[oldIndex] - newPoints[newIndex])*(oldPoints[oldIndex] - newPoints[newIndex]), singleOrder);
+		}
+	}
+	
+	float minVal = INFINITY;
+	std::set<int> newPointsCopy(availableNewPoints);
+	std::set<int>::iterator it;
+	std::list<int> order;
+	
+	// Go through all available new points
+	for(it = availableNewPoints.begin(); it != availableNewPoints.end(); ++it) {
+		// Temporarily remove (and test) one point and recursively call ourselves
+		newPointsCopy.erase(*it);
+		
+		float dist;
+		if(newPoints[*it] >= 0.0 && oldPoints[oldIndex] >= 0.0)
+			dist = (oldPoints[oldIndex] - newPoints[*it])*(oldPoints[oldIndex] - newPoints[*it]);
+		else
+			dist = 100.0;
+		
+		std::pair<float, std::list<int> > rval = touchMatchClosestPoints(oldPoints, newPoints, count, oldIndex + 1, newPointsCopy,
+														  currentTotalDistance + dist);
+		
+		//if(verbose_ >= 4)
+		//	cout << "     from " << *it << " got " << rval.first << endl;
+		
+		if(rval.first < minVal) {
+			minVal = rval.first;
+			order = rval.second;
+			order.push_front(*it);
+		}
+		
+		newPointsCopy.insert(*it);
+	}
+	
+	/*if(verbose_ >= 4) {
+		cout << " -> [";
+		list<int>::iterator it2;
+		
+		for(it2 = order.begin(); it2 != order.end(); ++it2)
+			cout << *it2 << ", ";
+		cout << "] (" << minVal << ")\n";
+	}*/
+	
+	return std::pair<float, std::list<int> >(minVal, order);
+}
+
+// A new touch was added from the last frame to this one
+
+void PianoKey::touchAdd(const KeyTouchFrame& frame, int index, timestamp_type timestamp) {
+	KeyTouchEvent event = { kTouchEventAdd, timestamp, frame };
+	touchEvents_.insert(std::pair<int, KeyTouchEvent>(frame.ids[index], event));
+#ifdef TOUCHKEYS_LEGACY_OSC
+	keyboard_.sendMessage("/touchkeys/add", "iiifff", noteNumber_, frame.ids[index], frame.count,
+						  frame.locs[index], frame.sizes[index], frame.horizontal(index),
+						  LO_ARGS_END);
+#endif
+}
+
+// A touch was removed from the last frame.  The frame in this case is the last frame containing
+// the touch in question (so we can find its ending position later).
+
+void PianoKey::touchRemove(const KeyTouchFrame& frame, int idRemoved, int remainingCount, timestamp_type timestamp) {
+	KeyTouchEvent event = { kTouchEventRemove, timestamp, frame };
+	touchEvents_.insert(std::pair<int, KeyTouchEvent>(idRemoved, event));
+#ifdef TOUCHKEYS_LEGACY_OSC
+	keyboard_.sendMessage("/touchkeys/remove", "iii", noteNumber_, idRemoved,
+						  remainingCount, LO_ARGS_END);
+#endif
+}
+
+// Process multi-finger gestures (pinch and slide) based on previous and current frames
+
+void PianoKey::touchMultiFingerGestures(const KeyTouchFrame& lastFrame, const KeyTouchFrame& newFrame, timestamp_type timestamp) {
+#ifdef TOUCHKEYS_LEGACY_OSC
+	if(newFrame.count == 2 && lastFrame.count == 2) {
+		float previousCentroid = (lastFrame.locs[0] + lastFrame.locs[1]) / 2.0;
+		float newCentroid = (newFrame.locs[0] + newFrame.locs[1]) / 2.0;
+		float previousWidth = lastFrame.locs[1] - lastFrame.locs[0];
+		float newWidth = newFrame.locs[1] - newFrame.locs[0];
+		
+		if(fabsf(newWidth - previousWidth) >= 0 /*pinchThreshold_*/) {
+			keyboard_.sendMessage("/touchkeys/twofinger/pinch", "iiif",
+									noteNumber_, newFrame.ids[0], newFrame.ids[1], newWidth, LO_ARGS_END);
+		}
+		if(fabsf(newCentroid - previousCentroid) >= 0 /*slideThreshold_*/) {
+			keyboard_.sendMessage("/touchkeys/twofinger/slide", "iiif",
+									noteNumber_, newFrame.ids[0], newFrame.ids[1], newCentroid, LO_ARGS_END);
+		}
+	}
+	else if(newFrame.count == 3 && lastFrame.count == 3) {
+		float previousCentroid = (lastFrame.locs[0] + lastFrame.locs[1] + lastFrame.locs[2]) / 3.0;
+		float newCentroid = (newFrame.locs[0] + newFrame.locs[1] + newFrame.locs[2]) / 3.0;
+		float previousWidth = lastFrame.locs[2] - lastFrame.locs[0];
+		float newWidth = newFrame.locs[2] - newFrame.locs[0];
+		
+		if(fabsf(newWidth - previousWidth) >= 0 /*pinchThreshold_*/) {
+			keyboard_.sendMessage("/touchkeys/threefinger/pinch", "iiiif",
+								  noteNumber_, newFrame.ids[0], newFrame.ids[1], newFrame.ids[2], newWidth, LO_ARGS_END);
+		}
+		if(fabsf(newCentroid - previousCentroid) >= 0 /*slideThreshold_*/) {
+			keyboard_.sendMessage("/touchkeys/threefinger/slide", "iiiif",
+								  noteNumber_, newFrame.ids[0], newFrame.ids[1], newFrame.ids[2], newCentroid, LO_ARGS_END);
+		}
+	}
+#endif
+}
+
+/*
+ * State Machine:
+ *
+ * Disabled
+ *		--> Unknown: (by user)
+ *			enable triggers on comparator
+ * Unknown
+ *		--> Idle: activity <= X, pos < Y
+ * Idle
+ *		--> Active: activity > X 
+ *				start looking for maxes and mins
+ *				watch for key down
+ * Active
+ *		--> Idle: activity <= X, pos < Y
+ *				stop looking for maxes and mins
+ *		--> Max: found maximum position > Z
+ *				calculate features
+ * Max:
+ *		--> Max: found maximum position greater than before; time from start < T
+ *				recalculate features
+ *		--> Idle: activity <= X, pos < Y
+ * Down:
+ *		(just a special case of Max?)
+ * Release:
+ *		(means the user is no longer playing the key, ignore its motion)
+ *		--> Idle: activity <= X, pos < Y
+ *
+ */
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/TouchKeys/PianoKey.h	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,228 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+ 
+  PianoKey.h: handles the operation of a single key on the keyboard,
+  including fusing touch and MIDI data. Also has hooks for continuous
+  key angle.
+*/
+
+
+#ifndef KEYCONTROL_PIANOKEY_H
+#define KEYCONTROL_PIANOKEY_H
+
+#include <set>
+#include <map>
+#include <list>
+#include "../JuceLibraryCode/JuceHeader.h"
+#include "../Utility/Node.h"
+#include "PianoTypes.h"
+#include "KeyIdleDetector.h"
+#include "KeyPositionTracker.h"
+#include "KeyTouchFrame.h"
+#include "../Utility/Scheduler.h"
+#include "../Utility/IIRFilter.h"
+
+const unsigned int kPianoKeyStateBufferLength = 20;	// How many previous states to save
+const unsigned int kPianoKeyIdleBufferLength = 10;  // How many idle/active transitions to save
+const unsigned int kPianoKeyPositionTrackerBufferLength = 30; // How many state histories to save
+const key_position kPianoKeyDefaultIdleActivityThreshold = scale_key_position(.020);
+const key_position kPianoKeyDefaultIdlePositionThreshold = scale_key_position(.05);
+const int kPianoKeyDefaultIdleCounter = 20;
+const timestamp_diff_type kPianoKeyDefaultTouchTimeoutInterval = microseconds_to_timestamp(0); // was 20000
+const timestamp_diff_type kPianoKeyGuiUpdateInterval = microseconds_to_timestamp(15000); // How frequently to update the position display
+
+// Possible key states
+enum {
+	kKeyStateToBeInitialized = -1,
+	kKeyStateUnknown = 0,
+	kKeyStateDisabled,
+	kKeyStateIdle,
+	kKeyStateActive
+};
+
+// Possible touch events
+enum {
+	kTouchEventIdle = 0,
+	kTouchEventAdd,
+	kTouchEventRemove
+};
+
+typedef int key_state;
+
+class PianoKeyboard;
+class MidiKeyboardSegment;
+
+/*
+ * PianoKey
+ *
+ * This class holds the buffer and state information for a single piano key,
+ * with methods to manage its status.
+ */
+
+class PianoKey : public TriggerDestination {
+private:
+	// ***** Internal Classes *****
+	
+	// Data on key touch events: what, when and where
+	struct KeyTouchEvent {
+		int type;
+		timestamp_type timestamp;
+		KeyTouchFrame frame;
+	};
+	
+public:
+	// ***** Constructors *****
+	
+	PianoKey(PianoKeyboard& keyboard, int noteNumber, int bufferLength);
+	//PianoKey(PianoKey const& obj);
+    
+    // ***** Destructor *****
+    
+    ~PianoKey();
+    		
+	// ***** Access Methods *****
+	
+	Node<key_position>& buffer() { return positionBuffer_; }
+	
+	// ***** Control Methods *****
+	//
+	// Force changes in the key state (e.g. to resolve stuck notes)
+	
+	// Enable or disable a key from generating events
+	void disable();
+	void enable();
+	
+	// Force the key to the Idle state, provided it is enabled
+	void forceIdle();
+	
+	// Clear any previous state, go back to initial state
+	void reset();
+	
+	void insertSample(key_position pos, timestamp_type ts);
+	
+	// ***** Trigger Methods *****
+	//
+	// This will be called by positionBuffer_ on each new sample.  Examine each sample to see
+	// whether the key is idle or not.
+	
+	void triggerReceived(TriggerSource* who, timestamp_type timestamp);
+	
+	// ***** MIDI Methods *****
+	//
+	// If MIDI input is used to control this note, the controller should call these functions
+	
+	void midiNoteOn(MidiKeyboardSegment *who, int velocity, int channel, timestamp_type timestamp);
+	void midiNoteOff(MidiKeyboardSegment *who, timestamp_type timestamp);
+	void midiAftertouch(MidiKeyboardSegment *who, int value, timestamp_type timestamp);
+	
+	bool midiNoteIsOn() { return midiNoteIsOn_; }
+	int midiVelocity() { return midiVelocity_; }
+	int midiChannel() { return midiChannel_; }
+	void changeMidiChannel(int newChannel) { midiChannel_ = newChannel; }
+    int midiOutputPort() { return midiOutputPort_; }
+    void changeMidiOutputPort(int newPort) { midiOutputPort_ = newPort; }
+	
+	// ***** Touch Methods *****
+	//
+	// If touchkeys are used, the controller uses these functions to provide data
+	// touchInsertFrame() implies touch is active if not already.
+	
+	void touchInsertFrame(KeyTouchFrame& newFrame, timestamp_type timestamp);
+	void touchOff(timestamp_type timestamp);
+	bool touchIsActive() { return touchIsActive_; }
+    void setTouchSensorsPresent(bool present) { touchSensorsArePresent_ = present; }
+	
+	// This function is called on a timer when the key receives MIDI data before touch data
+	// and wants to wait to integrate the two.  If the touch data doesn't materialize, this function
+	// is called by the scheduler.
+	timestamp_type touchTimedOut();
+	
+private:
+	// ***** MIDI Methods (private) *****
+	
+	// This method does the real work of midiNoteOn(), and might be called from it directly
+	// or after a delay.
+	void midiNoteOnHelper(MidiKeyboardSegment *who);
+	
+	// ***** Touch Methods (private) *****
+	
+	std::pair<float, std::list<int> > touchMatchClosestPoints(const float* oldPoints, const float *newPoints, float count,
+														 int oldIndex, std::set<int>& availableNewPoints, float currentTotalDistance); 
+	void touchAdd(const KeyTouchFrame& frame, int index, timestamp_type timestamp);
+	void touchRemove(const KeyTouchFrame& frame, int idRemoved, int remainingCount, timestamp_type timestamp);
+	void touchMultiFingerGestures(const KeyTouchFrame& lastFrame, const KeyTouchFrame& newFrame, timestamp_type timestamp);
+	
+	// ***** State Machine Methods *****
+	//
+	// This class maintains a current state that determines its response to the incoming
+	// key position data.
+	
+	void changeState(key_state newState);
+	void changeState(key_state newState, timestamp_type timestamp);	
+	
+	void terminateActivity();
+	
+	// ***** Member Variables *****
+	
+	// Reference back to the keyboard which centralizes control
+	PianoKeyboard& keyboard_;
+	
+	// Identity of the key (MIDI note number)
+	int noteNumber_;
+	
+	// --- Data related to MIDI ---
+	
+	bool midiNoteIsOn_;					// Whether this note is currently active from MIDI
+	int midiChannel_;					// MIDI channel currently associated with this note
+    int midiOutputPort_;                // Which port MIDI output for this note goes to
+	int midiVelocity_;					// Velocity of last MIDI onset
+	Node<int> midiAftertouch_;			// Aftertouch history on this note, if any
+	
+	// Timestamps for the most recent MIDI note on and note off events
+	timestamp_type midiOnTimestamp_, midiOffTimestamp_;
+	
+	// --- Data related to continuous key position ---
+
+	Node<key_position> positionBuffer_;     // Buffer that holds the key positions
+	KeyIdleDetector idleDetector_;          // Detector for whether the key is still or moving
+    KeyPositionTracker positionTracker_;    // Object to track the various active states of the key
+    timestamp_type timeOfLastGuiUpdate_;    // How long it's been since the last key position GUI call
+    timestamp_type timeOfLastDebugPrint_;   // TESTING
+    
+	Node<key_state> stateBuffer_;		// State history
+	key_state state_;					// Current state of the key (see enum above)
+	CriticalSection stateMutex_;		// Use this to synchronize changes of state
+    
+    //IIRFilterNode<key_position> testFilter_;    // Filter the raw key position data, for testing
+	
+	// --- Data related to surface touches ---
+
+    bool touchSensorsArePresent_;                   // Whether touch sensitivity exists on this key
+	bool touchIsActive_;							// Whether the user is currently touching the key
+	Node<KeyTouchFrame> touchBuffer_;				// Buffer that holds touchkey frames
+	std::multimap<int, KeyTouchEvent> touchEvents_;	// Mapping from touch number to event
+	bool touchIsWaiting_;							// Whether we're waiting for a touch to occur
+    MidiKeyboardSegment *touchWaitingSource_;  // Who we're waiting from a touch for
+	timestamp_type touchWaitingTimestamp_;			// When the timeout will occur
+	timestamp_diff_type touchTimeoutInterval_;		// How long to wait for a touch before timing out
+    
+    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(PianoKey)
+};
+
+#endif /* KEYCONTROL_PIANOKEY_H */
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/TouchKeys/PianoKeyCalibrator.cpp	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,264 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+ 
+  PianoKeyCalibrator.cpp: handles calibration of continuous key position data
+*/
+
+#include "PianoKeyCalibrator.h"
+
+// Constructor
+PianoKeyCalibrator::PianoKeyCalibrator(bool pressValueGoesDown, key_position* warpTable)
+: status_(kPianoKeyNotCalibrated), prevStatus_(kPianoKeyNotCalibrated),
+  pressValueGoesDown_(pressValueGoesDown), history_(0), warpTable_(warpTable) {}
+
+// Destructor
+PianoKeyCalibrator::~PianoKeyCalibrator() {
+    if(history_ != 0)
+        delete history_;
+    
+	// warpTable_ is passed in externally-- don't delete it
+}
+
+// Produce the calibrated value for a raw sample
+key_position PianoKeyCalibrator::evaluate(int rawValue) {
+	key_position calibratedValue, calibratedValueDenominator;
+
+    ScopedLock sl(calibrationMutex_);
+	
+	switch(status_) {
+		case kPianoKeyCalibrated:
+			if(missing_value<int>::isMissing(quiescent_) ||
+			   missing_value<int>::isMissing(press_)) {
+				return missing_value<key_position>::missing();
+			}
+			
+			// Do the calculation either in integer or floating-point arithmetic
+			calibratedValueDenominator = (key_position)(press_ - quiescent_);
+			
+			// Prevent divide-by-0 errors
+			if(calibratedValueDenominator == 0)
+				calibratedValue = missing_value<key_position>::missing();
+			else {
+                // Scale the value and clip it to a sensible range (for badly calibrated sensors)
+				calibratedValue = (scale_key_position((rawValue - quiescent_))) / calibratedValueDenominator;
+                if(calibratedValue < -0.5)
+                    calibratedValue = -0.5;
+                if(calibratedValue > 1.2)
+                    calibratedValue = 1.2;
+            }
+			
+			if(warpTable_ != 0) {
+				// TODO: warping
+			}
+			return calibratedValue;
+		case kPianoKeyInCalibration:
+			historyMutex_.enter();
+
+			// Add the sample to the calibration buffer, and wait until we have enough samples to do anything
+			history_->push_back(rawValue);
+			if(history_->size() < kPianoKeyCalibrationPressLength) {
+				historyMutex_.exit();
+				return missing_value<key_position>::missing();
+			}
+            
+			if(pressValueGoesDown_) {      // Pressed keys have a lower value than quiescent keys
+				int currentAverage = averagePosition(kPianoKeyCalibrationPressLength);
+                
+				// Look for minimum overall value
+				if(currentAverage < newPress_ || missing_value<int>::isMissing(newPress_)) {
+					newPress_ = currentAverage;
+                }
+			}
+			else {                          // Pressed keys have a higher value than quiescent keys
+				int currentAverage = averagePosition(kPianoKeyCalibrationPressLength);
+				
+				// Look for maximum overall value
+				if(currentAverage > newPress_ || missing_value<int>::isMissing(newPress_)) {
+					newPress_ = currentAverage;
+                }
+			}
+			
+			// Don't return a value while calibrating
+			historyMutex_.exit();
+			return missing_value<key_position>::missing();
+		case kPianoKeyNotCalibrated:	// Don't do anything
+		default:
+			return missing_value<key_position>::missing();
+	}
+}
+
+// Begin the calibrating process.
+void PianoKeyCalibrator::calibrationStart() {
+	if(status_ == kPianoKeyInCalibration)	// Throw away the old results if we're already in progress
+		calibrationAbort();					// This will clear the slate
+	
+    historyMutex_.enter();
+    if(history_ != 0)
+        delete history_;
+    history_ = new boost::circular_buffer<int>(kPianoKeyCalibrationBufferSize);
+    historyMutex_.exit();
+    
+	calibrationMutex_.enter();
+    newPress_ = quiescent_ = missing_value<int>::missing();
+	changeStatus(kPianoKeyInCalibration);
+	calibrationMutex_.exit();
+}
+
+// Finish calibrating and accept the new results. Returns true if
+// calibration was successful; false if one or more values were missing
+// or if insufficient range is available.
+
+bool PianoKeyCalibrator::calibrationFinish() {
+    bool updatedCalibration = false;
+    int oldQuiescent = quiescent_;
+    
+	if(status_ != kPianoKeyInCalibration)
+		return false;
+    
+    ScopedLock sl(calibrationMutex_);
+    
+    // Check that we were successfully able to update the quiescent value
+    // (should always be the case but this is a sanity check)
+	bool updatedQuiescent = internalUpdateQuiescent();
+    
+    if(updatedQuiescent && abs(newPress_ - quiescent_) >= kPianoKeyCalibrationMinimumRange) {
+        press_ = newPress_;
+        changeStatus(kPianoKeyCalibrated);
+        updatedCalibration = true;
+    }
+    else {
+        quiescent_ = oldQuiescent;
+        
+        if(prevStatus_ == kPianoKeyCalibrated) {	// There may or may not have been valid data in press_ and quiescent_ before, depending on whether
+            changeStatus(kPianoKeyCalibrated);      // they were previously calibrated.
+        }
+        else {
+            changeStatus(kPianoKeyNotCalibrated);
+        }
+    }
+    
+    cleanup();
+    return updatedCalibration;
+}
+
+// Finish calibrating without saving results
+void PianoKeyCalibrator::calibrationAbort() {
+    ScopedLock sl(calibrationMutex_);
+	cleanup();
+	if(prevStatus_ == kPianoKeyCalibrated) {	// There may or may not have been valid data in press_ and quiescent_ before, depending on whether
+		changeStatus(kPianoKeyCalibrated);	// they were previously calibrated.
+	}
+	else {
+		changeStatus(kPianoKeyNotCalibrated);
+	}
+}
+
+// Clear the existing calibration, reverting to an uncalibrated state
+void PianoKeyCalibrator::calibrationClear() {
+	if(status_ == kPianoKeyInCalibration)
+		calibrationAbort();
+    ScopedLock sl(calibrationMutex_);
+	status_ = prevStatus_ = kPianoKeyNotCalibrated;
+}
+
+// Generate new quiescent values without changing the press values
+void PianoKeyCalibrator::calibrationUpdateQuiescent() {
+	calibrationStart();
+	usleep(250000);			// Wait 0.25 seconds for data to collect
+	internalUpdateQuiescent();
+	calibrationAbort();
+}
+
+// Load calibration data from an XML string
+void PianoKeyCalibrator::loadFromXml(const XmlElement& baseElement) {
+	// Abort any calibration in progress and reset to default values
+	if(status_ == kPianoKeyInCalibration)
+		calibrationAbort();
+	calibrationClear();
+	
+	XmlElement *calibrationElement = baseElement.getChildByName("Calibration");
+	
+	if(calibrationElement != 0) {
+        if(calibrationElement->hasAttribute("quiescent") &&
+           calibrationElement->hasAttribute("press")) {
+            quiescent_ = calibrationElement->getIntAttribute("quiescent");
+            press_ = calibrationElement->getIntAttribute("press");
+            changeStatus(kPianoKeyCalibrated);
+        }
+	}
+}
+
+// Saves calibration data within the provided XML Element.  Child elements
+// will be added for each sequence.  Returns true if valid data was saved.
+bool PianoKeyCalibrator::saveToXml(XmlElement& baseElement) {
+	if(status_ != kPianoKeyCalibrated)
+		return false;
+
+    XmlElement *newElement = baseElement.createNewChildElement("Calibration");
+    
+    if(newElement == 0)
+        return false;
+    
+    newElement->setAttribute("quiescent", quiescent_);
+    newElement->setAttribute("press", press_);
+
+	return true;
+}
+
+// ***** Internal Methods *****
+
+// Internal method to clean up after a calibration session.
+void PianoKeyCalibrator::cleanup() {
+    ScopedLock sl(historyMutex_);
+    if(history_ != 0)
+        delete history_;
+    history_ = 0;
+    newPress_ = missing_value<int>::missing();
+}
+
+// This internal method actually calculates the new quiescent values.  Used by calibrationUpdateQuiescent()
+// and calibrationFinish(). Returns true if successful.
+bool PianoKeyCalibrator::internalUpdateQuiescent() {
+    ScopedLock sl(historyMutex_);
+    if(history_ == 0) {
+        return false;
+    }
+    if(history_->size() < kPianoKeyCalibrationPressLength) {
+        return false;
+    }
+    quiescent_  = averagePosition(kPianoKeyCalibrationBufferSize);
+    return true;
+}
+
+// Get the average position of several samples in the buffer. 
+int PianoKeyCalibrator::averagePosition(int length) {
+	boost::circular_buffer<int>::reverse_iterator rit = history_->rbegin();
+	int count = 0, sum = 0;
+	
+	while(rit != history_->rend() && count < length) {
+		sum += *rit++;
+		count++;
+	}
+	
+	if(count == 0) {
+		return missing_value<int>::missing();
+    }
+    
+    return (int)(sum / count);
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/TouchKeys/PianoKeyCalibrator.h	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,134 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+ 
+  PianoKeyCalibrator.h: handles calibration of continuous key position data
+*/
+
+
+#ifndef KEYCONTROL_PIANO_KEY_CALIBRATOR_H
+#define KEYCONTROL_PIANO_KEY_CALIBRATOR_H
+
+#include <iostream>
+#include <boost/circular_buffer.hpp>
+#include "../JuceLibraryCode/JuceHeader.h"
+#include "../Utility/Types.h"
+#include "PianoKeyboard.h"
+
+// Calibration status of the Piano Bar.  This compensates for variations in mechanical position, light level, etc.
+enum {
+	kPianoKeyNotCalibrated = 0,
+	kPianoKeyCalibrated,
+	kPianoKeyInCalibration
+};
+
+// Size of the calibration history we keep
+const size_t kPianoKeyCalibrationBufferSize = 32; // 32
+const size_t kPianoKeyCalibrationPressLength = 10; // 10
+
+// Minimum amount of range between quiescent and press for a note to be calibrated
+const int kPianoKeyCalibrationMinimumRange = 64;
+
+/*
+ * PianoKeyboardCalibrator
+ *
+ * This class defines a calibration from raw value to normalized value, generically for
+ * any sensor which outputs a continuous value for key position. It allows the calibration 
+ * to be learned and applied. It allows a direction to be set where pressed keys are either
+ * greater or lower in value than unpressed keys, to accommodate different sensor topologies.
+ */
+
+class PianoKeyCalibrator {
+public:
+	// ***** Constructor *****
+	
+	PianoKeyCalibrator(bool pressValueGoesDown, key_position* warpTable);
+	
+	// ***** Destructor *****
+	
+	~PianoKeyCalibrator();
+	
+	// ***** Evaluator *****
+	//
+	// In normal (operational) mode, evaluate() returns the calibrated value for the raw input.
+	// In other modes, it returns a "missing" value.  Specifically in calibration mode, it updates
+	// the settings for calibration.
+	
+	key_position evaluate(int rawValue);
+	
+	// ***** Calibration Methods *****
+	//
+    // Return the current status
+    int calibrationStatus() { return status_; }
+    
+	// Manage the calibration state
+	
+	void calibrationStart();
+	bool calibrationFinish();   // Returns true on successful calibration
+	void calibrationAbort();
+	void calibrationClear();
+	
+	// Learn new quiescent values only.  This needs to be called from a thread other than the data source
+	// (audio or serial callback) thread, since it waits for the buffer to fill up before calculating the values.
+	
+	void calibrationUpdateQuiescent();
+	
+	// ***** XML I/O Methods *****
+	//
+	// These methods load and save calibration data from an XML string.  The PianoKeyCalibrator object handles
+	// the relevant file I/O.
+	
+	void loadFromXml(const XmlElement& baseElement);
+	bool saveToXml(XmlElement& baseElement);
+	
+private:
+	// ***** Helper Methods *****
+	
+	void changeStatus(int newStatus) {
+		prevStatus_ = status_;
+		status_ = newStatus;
+	}
+	
+	// Update quiescent values
+	bool internalUpdateQuiescent();
+	
+	// Average position over the history buffer, for finding minima and maxima
+	int averagePosition(int length);
+	
+	// Clean up after a calibration; called by finish() and abort()
+	void cleanup();
+	
+	// ***** Member Variables *****
+	
+	int status_, prevStatus_;		// Status of calibration (see enum above), and its previous value
+	bool pressValueGoesDown_;		// If true, the pressed key value is expected to be lower than the quiescent
+	int quiescent_;                 // Resting value for the sensor
+	int press_;                     // Fully pressed value for the sensor
+	int newPress_;                  // Value-in-training for press
+	
+	boost::circular_buffer<int>* history_;  // Buffer holds history of raw values for calibrating
+	
+	// Table of warping values to correct for sensor non-linearity
+	key_position* warpTable_;
+    
+	CriticalSection calibrationMutex_;	// This mutex protects access to the entire calibration structure
+	CriticalSection historyMutex_;		// This mutex is specifically tied to the history_ buffers
+};
+
+#endif /* KEYCONTROL_PIANO_KEY_CALIBRATOR_H */
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/TouchKeys/PianoKeyboard.cpp	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,314 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+ 
+  PianoKeyboard.cpp: main class that keeps track of each key (and pedal)
+  on the keyboard, while also providing hooks for mapping and scheduling
+  of events. One shared instance of this class is used widely throughout
+  the program.
+*/
+
+#include "PianoKeyboard.h"
+#include "TouchkeyDevice.h"
+#include "../Mappings/Mapping.h"
+#include "MidiOutputcontroller.h"
+#include "../Mappings/MappingFactory.h"
+#include "../Mappings/MappingScheduler.h"
+
+// Constructor
+PianoKeyboard::PianoKeyboard() 
+: gui_(0), graphGui_(0), midiOutputController_(0),
+  oscTransmitter_(0), touchkeyDevice_(0),
+  lowestMidiNote_(0), highestMidiNote_(0), numberOfPedals_(0),
+  isInitialized_(false), isRunning_(false), isCalibrated_(false), calibrationInProgress_(false)
+{
+	  // Start a thread by which we can schedule future events
+	  futureEventScheduler_.start(0);
+      
+      // Build the key list
+      for(int i = 0; i <= 127; i++)
+          keys_.push_back(new PianoKey(*this, i, kDefaultKeyHistoryLength));
+      
+      mappingScheduler_ = new MappingScheduler(*this);
+      mappingScheduler_->start();
+}
+
+// Reset all keys and pedals to their default state.
+void PianoKeyboard::reset() {
+	// Clear any history in the source buffers
+	std::vector<PianoKey*>::iterator itKey;
+	std::vector<PianoPedal*>::iterator itPed;
+	
+	for(itKey = keys_.begin(); itKey != keys_.end(); itKey++)
+		(*itKey)->reset();
+	for(itPed = pedals_.begin(); itPed != pedals_.end(); itPed++)
+		(*itPed)->clear();
+}
+
+// Provide a pointer to the graphical display class
+
+void PianoKeyboard::setGUI(KeyboardDisplay* gui) {
+	gui_ = gui;
+	if(gui_ != 0) {
+		gui_->setKeyboardRange(lowestMidiNote_, highestMidiNote_);
+	}
+}
+
+// Set the range of the keyboard in terms of MIDI notes.  A standard
+// 88-key keyboard has a range of 21-108, but other setups may differ.
+
+void PianoKeyboard::setKeyboardGUIRange(int lowest, int highest) {
+	lowestMidiNote_ = lowest;
+	highestMidiNote_ = highest;
+	
+	// Sanity checks: enforce 0-127 range, high >= low
+	if(lowestMidiNote_ < 0)
+		lowestMidiNote_ = 0;
+	if(highestMidiNote_ < 0)
+		highestMidiNote_ = 0;
+	if(lowestMidiNote_ > 127)
+		lowestMidiNote_ = 127;
+	if(highestMidiNote_ > 127)
+		highestMidiNote_ = 127;
+	if(lowestMidiNote_ > highestMidiNote_)
+		highestMidiNote_ = lowestMidiNote_;
+	
+    /*
+	// Free the existing PianoKey objects
+	for(std::vector<PianoKey*>::iterator it = keys_.begin(); it != keys_.end(); ++it)
+		delete (*it);
+	keys_.clear();
+	
+	// Rebuild the key list
+	for(int i = lowestMidiNote_; i <= highestMidiNote_; i++)
+		keys_.push_back(new PianoKey(*this, i, kDefaultKeyHistoryLength));
+	*/
+    
+	if(gui_ != 0)
+		gui_->setKeyboardRange(lowestMidiNote_, highestMidiNote_);
+}
+
+// Send a message by OSC (and potentially by other means depending on who's listening)
+
+void PianoKeyboard::sendMessage(const char * path, const char * type, ...) {
+	//ScopedReadLock sl(oscListenerMutex_);
+
+    //cout << "sendMessage: " << path << endl;
+    
+	// Initialize variable argument list for reading
+	va_list v;
+	va_start(v, type);	
+	
+	// Make a new OSC message which we will use both internally and externally
+	lo_message msg = lo_message_new();
+	lo_message_add_varargs(msg, type, v);
+	int argc = lo_message_get_argc(msg);
+	lo_arg **argv = lo_message_get_argv(msg);
+	
+	// Internal handler lookup first
+	// Lock the mutex so the list of listeners doesn't change midway through
+
+    updateListeners();
+    
+	oscListenerMutex_.enter();
+    //oscListenerMutex_.enterRead();
+
+	// Now remove the global prefix and compare the rest of the message to the registered handlers.
+	std::multimap<std::string, OscHandler*>::iterator it;
+	std::pair<std::multimap<std::string, OscHandler*>::iterator,std::multimap<std::string, OscHandler*>::iterator> ret;
+	ret = noteListeners_.equal_range((std::string)path);
+	
+    double timeInHandlers = 0;
+    int numHandlers = 0; // DEBUG
+	it = ret.first;
+	while(it != ret.second) {
+		OscHandler *object = (*it++).second;
+
+        double before = Time::getMillisecondCounterHiRes();
+		object->oscHandlerMethod(path, type, argc, argv, 0);
+        timeInHandlers += Time::getMillisecondCounterHiRes() - before;
+        numHandlers++; // DEBUG
+	}
+    //oscListenerMutex_.exitRead();
+    oscListenerMutex_.exit();
+	
+    //if(timeInHandlers > 1.0)
+    //    cout << "sendMessage(): timeInHandlers = " << timeInHandlers << " for " << numHandlers << " handlers (msg " << path << ")\n";
+
+	// Now send this message to any external OSC sources	
+	if(oscTransmitter_ != 0)
+		oscTransmitter_->sendMessage(path, type, msg);
+	
+	lo_message_free(msg);	
+	va_end(v);
+
+
+}
+
+// Change number of pedals
+
+void PianoKeyboard::setNumberOfPedals(int number) {
+	numberOfPedals_ = number;
+	if(numberOfPedals_ < 0)
+		numberOfPedals_ = 0;
+	if(numberOfPedals_ > 127)
+		numberOfPedals_ = 127;
+	
+	// Free the existing PianoPedal objects
+	for(std::vector<PianoPedal*>::iterator it = pedals_.begin(); it != pedals_.end(); ++it)
+		delete (*it);
+	pedals_.clear();
+	
+	// Rebuild the list of pedals
+	for(int i = 0; i < numberOfPedals_; i++)
+		pedals_.push_back(new PianoPedal(kDefaultPedalHistoryLength));
+}
+
+// Set color of RGB LED for a given key. note indicates the MIDI
+// note number of the key, and color can be specified in one of two
+// formats.
+void PianoKeyboard::setKeyLEDColorRGB(const int note, const float red, const float green, const float blue) {
+    if(touchkeyDevice_ != 0) {
+        touchkeyDevice_->rgbledSetColor(note, red, green, blue);
+    }
+}
+
+void PianoKeyboard::setKeyLEDColorHSV(const int note, const float hue, const float saturation, const float value) {
+    if(touchkeyDevice_ != 0) {
+        touchkeyDevice_->rgbledSetColorHSV(note, hue, saturation, value);
+    }
+}
+
+void PianoKeyboard::setAllKeyLEDsOff() {
+    if(touchkeyDevice_ != 0) {
+        touchkeyDevice_->rgbledAllOff();
+    }
+}
+
+// ***** Mapping Methods *****
+
+// Add a new mapping identified by a MIDI note and an owner
+void PianoKeyboard::addMapping(int noteNumber, Mapping* mapping) {
+    removeMapping(noteNumber);  // Free any mapping that's already present on this note
+    mappings_[noteNumber] = mapping;
+}
+
+// Remove an existing mapping identified by owner
+void PianoKeyboard::removeMapping(int noteNumber) {
+    if(mappings_.count(noteNumber) == 0)
+        return;
+    Mapping* mapping = mappings_[noteNumber];
+    delete mapping;
+    mappings_.erase(noteNumber);
+}
+
+// Return a specific mapping by owner and note number
+Mapping* PianoKeyboard::mapping(int noteNumber) {
+    if(mappings_.count(noteNumber) == 0)
+        return 0;
+    return mappings_[noteNumber];
+}
+
+// Return a list of all MIDI notes with active mappings. Some may have more than
+// one but we only want the list of active notes
+std::vector<int> PianoKeyboard::activeMappings() {
+    std::vector<int> keys;
+    std::map<int, Mapping*>::iterator it = mappings_.begin();
+    while(it != mappings_.end()) {
+        int nextKey = (it++)->first;
+        keys.push_back(nextKey);
+    }
+    return keys;
+}
+
+void PianoKeyboard::clearMappings() {
+    std::map<int, Mapping*>::iterator it = mappings_.begin();
+    
+    while(it != mappings_.end()) {
+        // Delete everybody in the container
+        Mapping *mapping = it->second;
+        delete mapping;
+    }
+    
+    // Now clear the container
+    mappings_.clear();
+}
+
+// Mapping factory methods: tell each registered factory about these events if it listens to this particular note
+void PianoKeyboard::tellAllMappingFactoriesTouchBegan(int noteNumber, bool midiNoteIsOn, bool keyMotionActive,
+                                                      Node<KeyTouchFrame>* touchBuffer,
+                                                      Node<key_position>* positionBuffer,
+                                                      KeyPositionTracker* positionTracker) {
+    ScopedReadLock sl(mappingFactoriesMutex_);
+    std::map<MidiKeyboardSegment*, MappingFactory*>::iterator it;
+    for(it = mappingFactories_.begin(); it != mappingFactories_.end(); it++) {
+        if(it->first->respondsToNote(noteNumber))
+            it->second->touchBegan(noteNumber, midiNoteIsOn, keyMotionActive, touchBuffer, positionBuffer, positionTracker);
+    }
+}
+
+void PianoKeyboard::tellAllMappingFactoriesTouchEnded(int noteNumber, bool midiNoteIsOn, bool keyMotionActive,
+                                                      Node<KeyTouchFrame>* touchBuffer,
+                                                      Node<key_position>* positionBuffer,
+                                                      KeyPositionTracker* positionTracker) {
+    ScopedReadLock sl(mappingFactoriesMutex_);
+    std::map<MidiKeyboardSegment*, MappingFactory*>::iterator it;
+    for(it = mappingFactories_.begin(); it != mappingFactories_.end(); it++) {
+        if(it->first->respondsToNote(noteNumber))
+            it->second->touchEnded(noteNumber, midiNoteIsOn, keyMotionActive, touchBuffer, positionBuffer, positionTracker);
+    }
+}
+
+void PianoKeyboard::tellAllMappingFactoriesKeyMotionActive(int noteNumber, bool midiNoteIsOn, bool touchIsOn,
+                                                           Node<KeyTouchFrame>* touchBuffer,
+                                                           Node<key_position>* positionBuffer,
+                                                           KeyPositionTracker* positionTracker) {
+    ScopedReadLock sl(mappingFactoriesMutex_);
+    std::map<MidiKeyboardSegment*, MappingFactory*>::iterator it;
+    for(it = mappingFactories_.begin(); it != mappingFactories_.end(); it++) {
+        if(it->first->respondsToNote(noteNumber))
+            it->second->keyMotionActive(noteNumber, midiNoteIsOn, touchIsOn, touchBuffer, positionBuffer, positionTracker);
+    }
+}
+
+void PianoKeyboard::tellAllMappingFactoriesKeyMotionIdle(int noteNumber, bool midiNoteIsOn, bool touchIsOn,
+                                                         Node<KeyTouchFrame>* touchBuffer,
+                                                         Node<key_position>* positionBuffer,
+                                                         KeyPositionTracker* positionTracker) {
+    ScopedReadLock sl(mappingFactoriesMutex_);
+    std::map<MidiKeyboardSegment*, MappingFactory*>::iterator it;
+    for(it = mappingFactories_.begin(); it != mappingFactories_.end(); it++) {
+        if(it->first->respondsToNote(noteNumber))
+            it->second->keyMotionIdle(noteNumber, midiNoteIsOn, touchIsOn, touchBuffer, positionBuffer, positionTracker);
+    }
+}
+
+// Destructor
+
+PianoKeyboard::~PianoKeyboard() {
+    // Remove all mappings
+    clearMappings();
+    
+	// Delete any keys and pedals we've allocated
+	for(std::vector<PianoKey*>::iterator it = keys_.begin(); it != keys_.end(); ++it)
+		delete (*it);
+	for(std::vector<PianoPedal*>::iterator it = pedals_.begin(); it != pedals_.end(); ++it)
+		delete (*it);
+    mappingScheduler_->stop();
+    delete mappingScheduler_;
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/TouchKeys/PianoKeyboard.h	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,287 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+ 
+  PianoKeyboard.h: main class that keeps track of each key (and pedal)
+  on the keyboard, while also providing hooks for mapping and scheduling
+  of events. One shared instance of this class is used widely throughout
+  the program.
+*/
+
+#ifndef KEYCONTROL_PIANOKEYBOARD_H
+#define KEYCONTROL_PIANOKEYBOARD_H
+
+#include <iostream>
+#include <fstream>
+#include <map>
+#include "../JuceLibraryCode/JuceHeader.h"
+#include "../Utility/Types.h"
+#include "../Utility/Node.h"
+#include "PianoKey.h"
+#include "PianoPedal.h"
+#include "../Display/KeyboardDisplay.h"
+#include "../Display/KeyPositionGraphDisplay.h"
+#include "Osc.h"
+#include "../Utility/Scheduler.h"
+
+
+#define NUM_KEYS 88
+#define NUM_PEDALS 3
+
+enum {							// Index of each pedal in the buffers
+	kPedalDamper = 0,
+	kPedalSostenuto = 1,
+	kPedalUnaCorda = 2,
+	kNumPedals
+};
+
+const int kDefaultKeyHistoryLength = 8192;
+const int kDefaultPedalHistoryLength = 1024;
+
+class TouchkeyDevice;
+class Mapping;
+class MidiOutputController;
+class MappingFactory;
+class MidiKeyboardSegment;
+class MappingScheduler;
+
+/*
+ * PianoKeyboard
+ *
+ * Base class that implements all the functionality needed to measure and process
+ * real-time piano key motion.  This class is abstract in that it doesn't define a particular
+ * source for the key motion data.  The data source depends on the hardware used: PianoBar,
+ * PnoScan II, CEUS, etc.
+ *
+ * PianoKeyboard is a source of OSC messages (generated by various key actions).  Hence,
+ * objects can register with it to implement custom behavior for different actions.
+ */
+
+class PianoKeyboard : public OscMessageSource {
+public:
+    ofstream testLog_;
+    
+	// ***** Constructors *****
+	
+	PianoKeyboard();
+	
+	// ***** Destructor *****
+	
+	~PianoKeyboard();
+	
+	// ***** Control Methods *****
+	//
+	// These methods start and stop the device and all associated processing.  The implementation
+	// is specific to the hardware used.  Methods return true on success.
+	
+	//virtual bool start() = 0;
+	//virtual bool stop() = 0;
+	
+	bool isInitialized() { return isInitialized_; }
+	bool isRunning() { return isRunning_; }
+	
+	void reset();
+	
+	std::pair<int, int> keyboardGUIRange() { return std::pair<int, int>(lowestMidiNote_, highestMidiNote_); }
+	void setKeyboardGUIRange(int lowest, int highest);
+	
+	int numberOfPedals() { return numberOfPedals_; }
+	void setNumberOfPedals(int number);
+	
+	// ***** Communication Links and Methods *****
+	
+    // Set/query the output controller
+	MidiOutputController* midiOutputController() { return midiOutputController_; }
+	void setMidiOutputController(MidiOutputController* ct) { midiOutputController_ = ct; }
+    
+	// Set reference to GUI displays
+	KeyboardDisplay* gui() { return gui_; }
+	void setGUI(KeyboardDisplay* gui);
+    KeyPositionGraphDisplay *graphGUI() { return graphGui_; }
+    void setGraphGUI(KeyPositionGraphDisplay* newGui) { graphGui_ = newGui; }
+	
+	// OSC transmitter handles the mechanics of sending messages to one or more targets
+	void setOscTransmitter(OscTransmitter* trans) { oscTransmitter_ = trans; }
+    
+    // TouchkeyDevice handles communication with the touch-sensor/piano-scanner hardware
+    void setTouchkeyDevice(TouchkeyDevice* device) { touchkeyDevice_ = device; }
+	
+	// Send a named message by OSC (and potentially by MIDI or other means if suitable listeners
+	// are enabled)
+	void sendMessage(const char * path, const char * type, ...);
+	
+	// ***** Scheduling Methods *****
+	
+	// Add or remove events from the scheduler queue
+	void scheduleEvent(void *who, Scheduler::action func, timestamp_type timestamp) {
+		futureEventScheduler_.schedule(who, func, timestamp);
+	}
+    void unscheduleEvent(void *who) {
+		futureEventScheduler_.unschedule(who);
+	}
+	void unscheduleEvent(void *who, timestamp_type timestamp) {
+		futureEventScheduler_.unschedule(who, timestamp);
+	}
+	
+	// Return the current timestamp associated with the scheduler
+	timestamp_type schedulerCurrentTimestamp() { return futureEventScheduler_.currentTimestamp(); }
+	
+	// ***** Individual Key/Pedal Methods *****
+	
+	// Access to individual keys and pedals
+	PianoKey* key(int note) {
+		//if(note < lowestMidiNote_ || note > highestMidiNote_)
+		//	return 0;
+		//return keys_[note - lowestMidiNote_];
+        if(note < 0 || note > 127)
+            return 0;
+        return keys_[note];
+	}
+	PianoPedal* pedal(int pedal) {
+		if(pedal < 0 || pedal >= numberOfPedals_)
+			return 0;
+		return pedals_[pedal];
+	}
+	
+	// Keys and pedals are enabled by default.  If one has been disabled, reenable it so it reads data
+	// and triggers notes, as normal.
+	void enableKey(int key);
+	void enablePedal(int pedal);
+	
+	// Disable a key or pedal from causing any activity to occur.
+	void disableKey(int key);
+	void disablePedal(int pedal);
+	
+	// Leave a key enabled, but terminate any activity it has initiated and return it to the idle state.
+	// If the key is active because of a hardware problem, this may be a short-term solution at best, requiring
+	// the key to be disabled until the problem can be properly resolved.
+	void forceKeyIdle(int key);
+    
+    // Set the color of an RGB LED for the given key, if relevant hardware is present
+    void setKeyLEDColorRGB(const int note, const float red, const float green, const float blue);
+    void setKeyLEDColorHSV(const int note, const float hue, const float saturation, const float value);
+    void setAllKeyLEDsOff();
+    
+    // ***** Mapping Methods *****
+    // Mappings are identified by the MIDI note they affect and by
+    // their owner object.
+    void addMapping(int noteNumber, Mapping* mapping);    // Add a new mapping to the container
+    void removeMapping(int noteNumber);                  // Remove a mapping from the container
+    Mapping* mapping(int noteNumber);                    // Look up a mapping with the given note and owner
+    std::vector<int> activeMappings();                   // Return a list of all active note mappings
+    void clearMappings();                                // Remove all mappings
+    
+    // Managing the mapping factories
+    MappingFactory *mappingFactory(MidiKeyboardSegment* segment) {
+        ScopedReadLock sl(mappingFactoriesMutex_);
+        if(mappingFactories_.count(segment) == 0)
+            return 0;
+        return mappingFactories_[segment];
+    }
+    void setMappingFactory(MidiKeyboardSegment* segment, MappingFactory *factory) {
+        ScopedWriteLock sl(mappingFactoriesMutex_);
+        mappingFactories_[segment] = factory;
+    }
+    void removeMappingFactory(MidiKeyboardSegment* segment) {
+        ScopedWriteLock sl(mappingFactoriesMutex_);
+        if(mappingFactories_.count(segment) > 0)
+            mappingFactories_.erase(segment);
+    }
+    
+    // Passing data to all mapping factories; these methods are not specific to a particular
+    // MIDI input segment so we need to check with each factory whether it wants this data.
+    void tellAllMappingFactoriesTouchBegan(int noteNumber, bool midiNoteIsOn, bool keyMotionActive,
+                                           Node<KeyTouchFrame>* touchBuffer,
+                                           Node<key_position>* positionBuffer,
+                                           KeyPositionTracker* positionTracker);
+    void tellAllMappingFactoriesTouchEnded(int noteNumber, bool midiNoteIsOn, bool keyMotionActive,
+                                           Node<KeyTouchFrame>* touchBuffer,
+                                           Node<key_position>* positionBuffer,
+                                           KeyPositionTracker* positionTracker);
+    void tellAllMappingFactoriesKeyMotionActive(int noteNumber, bool midiNoteIsOn, bool touchIsOn,
+                                                Node<KeyTouchFrame>* touchBuffer,
+                                                Node<key_position>* positionBuffer,
+                                                KeyPositionTracker* positionTracker);
+    void tellAllMappingFactoriesKeyMotionIdle(int noteNumber, bool midiNoteIsOn, bool touchIsOn,
+                                              Node<KeyTouchFrame>* touchBuffer,
+                                              Node<key_position>* positionBuffer,
+                                              KeyPositionTracker* positionTracker);
+    
+    MappingScheduler& mappingScheduler() { return *mappingScheduler_; }
+	
+	// ***** Member Variables *****
+public:
+    // This mutex is grabbed by any thread which is supplying performance
+    // data (MIDI or touch). By synchronizing access once centrally, we
+    // can avoid many other lock scenarios in individual objects. The object
+    // is declared public so it can be used in ScopedLocks.
+    CriticalSection performanceDataMutex_;
+    
+private:
+	// Individual key and pedal data structures
+	std::vector<PianoKey*> keys_;
+	std::vector<PianoPedal*> pedals_;	
+	
+	// Reference to GUI display (if present)
+	KeyboardDisplay* gui_;
+    KeyPositionGraphDisplay *graphGui_;
+    
+    // Reference to the MIDI output controller
+    MidiOutputController* midiOutputController_;
+	
+	// Reference to message transmitter class
+	OscTransmitter* oscTransmitter_;
+    
+    // Reference to TouchKey hardware controller class
+    TouchkeyDevice* touchkeyDevice_;
+	
+	// Keyboard range, expressed in MIDI note numbers
+	int lowestMidiNote_, highestMidiNote_;
+	int numberOfPedals_;
+	
+	bool isInitialized_;
+	bool isRunning_;
+	bool isCalibrated_;
+	bool calibrationInProgress_;
+	
+	// Mapping objects associated with particular messages
+	// When a sendMessage() command is received, it checks the message
+	// against this set of possible listeners to see if it matches the
+	// path.  If so, the handler function is called.
+	std::multimap<std::string, OscHandler*> messageListeners_;
+	
+	// This object can be used to schedule events to be executed at future timestamps,
+	// for example to handle timeouts.  This will often be called from within a particular
+	// key, but we should maintain one central repository for these events.
+	Scheduler futureEventScheduler_;
+    
+    // Data related to mappings for active notes
+    std::map<int, Mapping*> mappings_;            // Mappings from key motion to sound
+    
+    // Collection of mapping factories organised by segment of the keyboard. Different
+    // segments may have different mappings
+    std::map<MidiKeyboardSegment*, MappingFactory*> mappingFactories_;
+    ReadWriteLock mappingFactoriesMutex_;
+    
+    // Scheduler specifically used for coordinating mappings
+    MappingScheduler *mappingScheduler_;
+    
+    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(PianoKeyboard)
+};
+
+#endif /* KEYCONTROL_PIANOKEYBOARD_H */
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/TouchKeys/PianoPedal.cpp	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,26 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+ 
+  PianoPedal.cpp: class representing the action of a piano pedal (presently
+  not used; placeholder)
+*/
+
+
+#include "PianoPedal.h"
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/TouchKeys/PianoPedal.h	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,35 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+ 
+  PianoPedal.h: class representing the action of a piano pedal (presently
+  not used; placeholder)
+*/
+
+#ifndef PIANO_PEDAL_H
+#define PIANO_PEDAL_H
+
+#include "../Utility/Node.h"
+#include "PianoTypes.h"
+
+class PianoPedal : public Node<key_position> {
+public:
+	PianoPedal(int historyLength) : Node<key_position>(historyLength) {}
+};
+
+#endif /* PIANO_PEDAL_H */
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/TouchKeys/PianoTypes.h	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,50 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+ 
+  PianoTypes.h: some useful constants mainly relevant to continuous key angle
+*/
+
+#ifndef KEYCONTROL_PIANO_TYPES_H
+#define KEYCONTROL_PIANO_TYPES_H
+
+#include "../Utility/Types.h"
+
+#undef FIXED_POINT_PIANO_SAMPLES
+
+// Data types.  Allow for floating-point (more flexible) or fixed-point (faster) arithmetic
+// on piano key positions
+#ifdef FIXED_POINT_PIANO_SAMPLES
+typedef int key_position;
+typedef int key_velocity;
+#define scale_key_position(x) 4096*(key_position)(x)
+#define key_position_to_float(x) ((float)x/4096.0)
+#define key_abs(x) abs(x)
+#define calculate_key_velocity(dpos, dt) (key_velocity)((65536*dpos)/(key_position)dt)
+#define scale_key_velocity(x) (65536/4096)*(key_velocity)(x) // FIXME: TEST THIS!
+#else
+typedef float key_position;
+typedef float key_velocity;
+#define scale_key_position(x) (key_position)(x)
+#define key_position_to_float(x) (x)
+#define key_abs(x) fabsf(x)
+#define calculate_key_velocity(dpos, dt) (key_velocity)(dpos/(key_position)dt)
+#define scale_key_velocity(x) (key_velocity)(x)
+#endif /* FIXED_POINT_PIANO_SAMPLES */
+
+#endif /* KEYCONTROL_PIANO_TYPES_H */
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/TouchKeys/TouchkeyDevice.cpp	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,2078 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+ 
+  TouchkeyDevice.cpp: handles communication with the TouchKeys hardware
+*/
+
+#include <iomanip>
+#include <stdio.h>
+#include <stdlib.h>
+#include "TouchkeyDevice.h"
+
+
+const char* kKeyNames[13] = {"C ", "C#", "D ", "D#", "E ", "F ", "F#", "G ", "G#", "A ", "A#", "B ", "c "};
+
+// Constructor
+
+TouchkeyDevice::TouchkeyDevice(PianoKeyboard& keyboard) 
+: keyboard_(keyboard), device_(-1),
+ioThread_(boost::bind(&TouchkeyDevice::runLoop, this, _1), "TouchKeyDevice::ioThread"),
+rawDataThread_(boost::bind(&TouchkeyDevice::rawDataRunLoop, this, _1), "TouchKeyDevice::rawDataThread"),
+autoGathering_(false), shouldStop_(false), sendRawOscMessages_(false),
+verbose_(4), numOctaves_(0), lowestMidiNote_(12), lowestKeyPresentMidiNote_(12),
+updatedLowestMidiNote_(12), deviceSoftwareVersion_(-1), deviceHardwareVersion_(-1),
+expectedLengthWhite_(kTransmissionLengthWhiteNewHardware),
+expectedLengthBlack_(kTransmissionLengthBlackNewHardware), deviceHasRGBLEDs_(false),
+ledThread_(boost::bind(&TouchkeyDevice::ledUpdateLoop, this, _1), "TouchKeyDevice::ledThread"),
+isCalibrated_(false), calibrationInProgress_(false),
+keyCalibrators_(0), keyCalibratorsLength_(0), sensorDisplay_(0)
+{
+    // Tell the piano keyboard class how to call us back
+    keyboard_.setTouchkeyDevice(this);
+	
+	// Initialize the frame -> timestamp synchronization.  Frame interval is nominally 1ms,
+	// but this class helps us find the actual rate which might drift slightly, and it keeps
+	// the time stamps of each data point in sync with other streams.
+	timestampSynchronizer_.initialize(Time::getMillisecondCounterHiRes(), keyboard_.schedulerCurrentTimestamp());
+	timestampSynchronizer_.setNominalSampleInterval(.001);
+	timestampSynchronizer_.setFrameModulus(65536);
+    
+    for(int i = 0; i < 4; i++)
+        analogLastFrame_[i] = 0;
+    
+    logFileCreated_ = false;
+    loggingActive_ = false;
+}
+
+
+// ------------------------------------------------------
+// create a new MIDI log file, ready to have data written to it
+void TouchkeyDevice::createLogFiles(string keyTouchLogFilename, string analogLogFilename, string path)
+{
+    if (path.compare("") != 0)
+    {
+        path = path + "/";
+    }
+    
+    keyTouchLogFilename = path + keyTouchLogFilename;
+    analogLogFilename = path + analogLogFilename;
+    
+    char *fileName = (char*)keyTouchLogFilename.c_str();
+    
+    // create output file for key touch
+    keyTouchLog_.open (fileName, ios::out | ios::binary);
+    keyTouchLog_.seekp(0);
+
+    fileName = (char*)analogLogFilename.c_str();
+    
+    // create output file for analog data
+    analogLog_.open (fileName, ios::out | ios::binary);
+    analogLog_.seekp(0);
+    
+    // indicate that we have created a log file (so we can close it later)
+    logFileCreated_ = true;
+}
+
+// ------------------------------------------------------
+// close the log file
+void TouchkeyDevice::closeLogFile()
+{
+    if (logFileCreated_)
+    {
+        keyTouchLog_.close();
+        analogLog_.close();
+        logFileCreated_ = false;
+    }
+    loggingActive_ = false;
+}
+
+// ------------------------------------------------------
+// start logging midi data
+void TouchkeyDevice::startLogging()
+{
+    loggingActive_ = true;
+}
+
+// ------------------------------------------------------
+// stop logging midi data
+void TouchkeyDevice::stopLogging()
+{
+    loggingActive_ = false;
+}
+
+// Open the touchkey device (a USB CDC device).  Returns true on success.
+
+bool TouchkeyDevice::openDevice(const char * inputDevicePath) {
+	// If the device is already open, close it
+	if(device_ >= 0)
+		closeDevice();
+	
+	// Open the device
+	device_ = open(inputDevicePath, O_RDWR | O_NOCTTY | O_NDELAY);
+	
+	if(device_ < 0)
+		return false;
+	return true;
+}
+
+// Close the touchkey serial device
+void TouchkeyDevice::closeDevice() {
+	if(device_ < 0)
+		return;
+	
+	stopAutoGathering();
+	keysPresent_.clear();
+	close(device_);
+    device_ = -1;
+}
+
+// Check if the device is present and ready to respond.  If status is not null, store the current
+// controller status information.
+
+bool TouchkeyDevice::checkIfDevicePresent(int millisecondsToWait) {
+	//struct timeval startTime, currentTime;
+    double startTime, currentTime;
+	unsigned char ch;
+	bool controlSeq = false, startingFrame = false;
+    
+	if(device_ < 0)
+		return false;
+	tcflush(device_, TCIFLUSH);							// Flush device input
+	if(write(device_, (char*)kCommandStatus, 5) < 0) {	// Write status command
+		cout << "ERROR: unable to write status command.  errno = " << errno << endl;
+		return false;
+	}	
+	tcdrain(device_);									// Force output reach device
+	
+	// Wait the specified amount of time for a response before giving up
+    startTime = Time::getMillisecondCounterHiRes();
+    currentTime = startTime;
+
+	while(currentTime - startTime < (double)millisecondsToWait) {
+		long count = read(device_, (char *)&ch, 1);
+
+		if(count < 0) {				// Check if an error occurred on read
+			if(errno != EAGAIN) {
+				cout << "Unable to read from device (error " << errno << ").  Aborting.\n";
+				return false;
+			}
+		}
+		else if(count > 0) {		// Data received
+			// Wait for a frame back that is of type status.  We don't even care what the 
+			// status is at this point, just that we got something.
+			
+			if(controlSeq) {
+				controlSeq = false;
+				if(ch == kControlCharacterFrameBegin)
+					startingFrame = true;
+                else
+                    startingFrame = false;
+			}
+			else {
+				if(ch == ESCAPE_CHARACTER) {
+					controlSeq = true;
+                }
+				else if(startingFrame) {
+					if(ch == kFrameTypeStatus) {
+						ControllerStatus status;
+						unsigned char statusBuf[TOUCHKEY_MAX_FRAME_LENGTH];
+						int statusBufLength = 0;
+						bool frameError = false;
+						
+						// Gather and parse the status frame
+						
+						while(currentTime - startTime < millisecondsToWait) {
+							count = read(device_, (char *)&ch, 1);
+							
+							if(count == 0)
+								continue;
+							if(count < 0) {
+								if(errno != EAGAIN && verbose_ >= 1) {	// EAGAIN just means no data was available
+									cout << "Unable to read from device (error " << errno << ").  Aborting.\n";
+									return false;
+								}
+								
+								continue;
+							}
+							
+							if(controlSeq) {
+								controlSeq = false;
+								if(ch == kControlCharacterFrameEnd)	{		// frame finished?
+									break;
+                                }
+								else if(ch == kControlCharacterFrameError)	// device telling us about an internal comm error
+									frameError = true;
+								else if(ch == ESCAPE_CHARACTER) {			// double-escape means a literal escape character
+									statusBuf[statusBufLength++] = (unsigned char)ch;
+									if(statusBufLength >= TOUCHKEY_MAX_FRAME_LENGTH) {
+										frameError = true;
+										break;
+									}				
+								}
+								else if(ch == kControlCharacterNak && verbose_ >= 1) {
+									cout << "Warning: received NAK\n";
+								}			
+							}
+							else {
+								if(ch == ESCAPE_CHARACTER)
+									controlSeq = true;
+								else {
+									statusBuf[statusBufLength++] = (unsigned char)ch;
+									if(statusBufLength >= TOUCHKEY_MAX_FRAME_LENGTH) {
+										frameError = true;
+										break;
+									}
+								}
+							}
+							
+							currentTime = Time::getMillisecondCounterHiRes();
+						}
+						
+						if(frameError) {
+                            if(verbose_ >= 1)
+                                cout << "Warning: device present, but frame error received trying to get status.\n";
+						}
+						else if(processStatusFrame(statusBuf, statusBufLength, &status)) {
+							// Clear keys present in preparation to read new list of keys
+							keysPresent_.clear();
+							
+							numOctaves_ = status.octaves;
+                            deviceSoftwareVersion_ = status.softwareVersionMajor;
+                            deviceHardwareVersion_ = status.hardwareVersion;
+                            deviceHasRGBLEDs_ = status.hasRGBLEDs;
+                            lowestKeyPresentMidiNote_ = 127;
+							
+							if(verbose_ >= 1) {
+								cout << endl << "Found Device: Hardware Version " << status.hardwareVersion;
+								cout << " Software Version " << status.softwareVersionMajor << "." << status.softwareVersionMinor;
+								cout << endl << "  " << status.octaves << " octaves connected" << endl;
+							}
+							for(int i = 0; i < status.octaves; i++) {
+								bool foundKey = false;
+								
+								if(verbose_ >= 1) cout << "  Octave " << i << ": ";
+								for(int j = 0; j < 13; j++) {
+									if(status.connectedKeys[i] & (1<<j)) {
+										if(verbose_ >= 1) cout << kKeyNames[j] << " ";
+										keysPresent_.insert(octaveNoteToIndex(i, j));
+										foundKey = true;
+                                        if(octaveKeyToMidi(i, j) < lowestKeyPresentMidiNote_)
+                                            lowestKeyPresentMidiNote_ = octaveKeyToMidi(i, j);
+									}
+									else {
+										if(verbose_ >= 1) cout << "-  ";
+									}
+
+								}
+
+								cout << endl;
+							}
+                            
+                            // Hardware version determines whether all keys have XY or not
+                            if(status.hardwareVersion >= 2) {
+                                expectedLengthWhite_ = kTransmissionLengthWhiteNewHardware;
+                                expectedLengthBlack_ = kTransmissionLengthBlackNewHardware;
+                                whiteMaxX_ = kWhiteMaxXValueNewHardware;
+                                whiteMaxY_ = kWhiteMaxYValueNewHardware;
+                                blackMaxX_ = kBlackMaxXValueNewHardware;
+                                blackMaxY_ = kBlackMaxYValueNewHardware;
+                            }
+                            else {
+                                expectedLengthWhite_ = kTransmissionLengthWhiteOldHardware;
+                                expectedLengthBlack_ = kTransmissionLengthBlackOldHardware;
+                                whiteMaxX_ = kWhiteMaxXValueOldHardware;
+                                whiteMaxY_ = kWhiteMaxYValueOldHardware;
+                                blackMaxX_ = 1.0; // irrelevant -- no X data
+                                blackMaxY_ = kBlackMaxYValueOldHardware;
+                            }
+                            
+                            // Software version indicates what information is available. On version
+                            // 2 and greater, can indicate which is lowest sensor available. Might
+                            // be different from lowest connected key.
+                            if(status.softwareVersionMajor >= 2) {
+                                lowestKeyPresentMidiNote_ = octaveKeyToMidi(0, status.lowestHardwareNote);
+                            }
+                            else if(lowestKeyPresentMidiNote_ == 127) // No keys found and old device software
+                                lowestKeyPresentMidiNote_ = lowestMidiNote_;
+   
+                            keyboard_.setKeyboardGUIRange(lowestKeyPresentMidiNote_, lowestMidiNote_ + 12*numOctaves_);
+                            calibrationInit(12*numOctaves_ + 1); // One more for the top C
+						}
+						else {
+							if(verbose_ >= 1) cout << "Warning: device present, but received invalid status frame.\n";
+							tcflush(device_, TCIOFLUSH);	// Throw away anything else in the buffer
+							return false;					// Yes... found the device
+						}
+
+						tcflush(device_, TCIOFLUSH);	// Throw away anything else in the buffer
+						return true;					// Yes... found the device
+					}
+				}
+                
+                startingFrame = false;
+			}
+		}
+	
+		currentTime = Time::getMillisecondCounterHiRes();
+	}
+	
+	return false;
+}
+
+// Start a run loop thread to receive centroid data.  Returns true
+// on success.
+
+bool TouchkeyDevice::startAutoGathering() {
+    // Can only start if the device is open
+	if(!isOpen())
+		return false;
+    // Already running?
+	if(autoGathering_)
+		return true;
+	shouldStop_ = false;
+    ledShouldStop_ = false;
+	
+	if(verbose_ >= 1)
+		cout << "Starting auto centroid collection\n";
+	
+    // Start the data input and LED threads
+    ioThread_.startThread();
+    ledThread_.startThread();
+	autoGathering_ = true;
+    
+    // Tell the device to start scanning for new data
+	if(write(device_, (char*)kCommandStartScanning, 5) < 0) {
+		cout << "ERROR: unable to write startAutoGather command.  errno = " << errno << endl;
+	}
+	tcdrain(device_);
+	
+	keyboard_.sendMessage("/touchkeys/allnotesoff", "", LO_ARGS_END);
+	if(keyboard_.gui() != 0) {
+		// Update display: touch sensing enabled, which keys connected, no current touches
+		keyboard_.gui()->setTouchSensingEnabled(true);
+		for(set<int>::iterator it = keysPresent_.begin(); it != keysPresent_.end(); ++it) {
+			keyboard_.gui()->setTouchSensorPresentForKey(octaveKeyToMidi(indexToOctave(*it), indexToNote(*it)), true);
+		}
+		keyboard_.gui()->clearAllTouches();
+        keyboard_.gui()->clearAnalogData();
+	}
+
+	return true;
+}
+
+// Stop the run loop if applicable
+void TouchkeyDevice::stopAutoGathering() {
+    // Check if actually running
+	if(!autoGathering_ || !isOpen())
+		return;
+    // Stop any calibration in progress
+    calibrationAbort();	
+    
+    // Tell device to stop scanning
+	if(write(device_, (char*)kCommandStopScanning, 5) < 0) {
+		cout << "ERROR: unable to write stopAutoGather command.  errno = " << errno << endl;
+	}		
+	tcdrain(device_);
+	
+    // Setting this to true tells the run loop to exit what it's doing
+	shouldStop_ = true;
+    ledShouldStop_ = true;
+	
+	if(verbose_ >= 1)
+		cout << "Stopping auto centroid collection\n";
+	
+    // Wait for run loop thread to finish. Set a timeout in case there's
+    // some sort of device hangup
+    if(ioThread_.isThreadRunning())
+        ioThread_.stopThread(3000);
+    if(ledThread_.isThreadRunning())
+        ledThread_.stopThread(3000);
+    if(rawDataThread_.isThreadRunning())
+        rawDataThread_.stopThread(3000);
+	
+    // Stop any currently playing notes
+	keyboard_.sendMessage("/touchkeys/allnotesoff", "", LO_ARGS_END);
+	
+	// Clear touch for all keys
+	//std::pair<int, int> keyboardRange = keyboard_.keyboardRange();
+	//for(int i = keyboardRange.first; i <= keyboardRange.second; i++)
+    for(int i = 0; i <= 127; i++)
+        if(keyboard_.key(i) != 0)
+            keyboard_.key(i)->touchOff(lastTimestamp_);
+								   
+	if(keyboard_.gui() != 0) {
+		// Update display: touch sensing disabled
+		keyboard_.gui()->clearAllTouches();		
+		keyboard_.gui()->setTouchSensingEnabled(false);
+        keyboard_.gui()->clearAnalogData();
+	}
+	
+	if(verbose_ >= 2)
+		cout << "...done.\n";
+
+	autoGathering_ = false;
+}
+
+// Begin raw data collection from a given single key
+
+bool TouchkeyDevice::startRawDataCollection(int octave, int key, int mode, int scaler) {
+    cout << "startRawDataCollection()\n";
+    
+	if(!isOpen())
+		return false;
+	
+	stopAutoGathering();	// Stop the thread if it's running	
+	
+    cout << "preparing\n";
+    
+	//unsigned char command[] = {ESCAPE_CHARACTER, kControlCharacterFrameBegin,
+	//	kFrameTypeMonitorRawFromKey, (unsigned char)octave, (unsigned char)key,
+    //    (unsigned char)mode, (unsigned char)scaler,
+	//	ESCAPE_CHARACTER, kControlCharacterFrameEnd};
+    
+    usleep(10000);
+    
+    // Command to set the mode of the key
+    unsigned char commandSetMode[] = {ESCAPE_CHARACTER, kControlCharacterFrameBegin,
+        kFrameTypeSendI2CCommand, (unsigned char)octave, (unsigned char)key,
+        3 /* xmit */, 0 /* response */, 0 /* command offset */, 1 /* mode */, (unsigned char)mode,
+        ESCAPE_CHARACTER, kControlCharacterFrameEnd};
+	
+	if(write(device_, (char*)commandSetMode, 12) < 0) {
+		cout << "ERROR: unable to write setMode command.  errno = " << errno << endl;
+	}
+	tcdrain(device_);
+    
+    usleep(10000);
+    
+    // Command to set the scaler of the key
+    unsigned char commandSetScaler[] = {ESCAPE_CHARACTER, kControlCharacterFrameBegin,
+        kFrameTypeSendI2CCommand, (unsigned char)octave, (unsigned char)key,
+        3 /* xmit */, 0 /* response */, 0 /* command offset */, 3 /* raw scaler */, (unsigned char)scaler,
+        ESCAPE_CHARACTER, kControlCharacterFrameEnd};
+	
+	if(write(device_, (char*)commandSetScaler, 12) < 0) {
+		cout << "ERROR: unable to write setMode command.  errno = " << errno << endl;
+	}
+	tcdrain(device_);
+    
+    usleep(10000);
+    
+    unsigned char commandPrepareRead[] = {ESCAPE_CHARACTER, kControlCharacterFrameBegin,
+        kFrameTypeSendI2CCommand, (unsigned char)octave, (unsigned char)key,
+        1 /* xmit */, 0 /* response */, 6 /* data offset */,
+        ESCAPE_CHARACTER, kControlCharacterFrameEnd};
+    
+	if(write(device_, (char*)commandPrepareRead, 10) < 0) {
+		cout << "ERROR: unable to write prepareRead command.  errno = " << errno << endl;
+	}
+	tcdrain(device_);
+    
+    usleep(10000);
+    
+    rawDataCurrentOctave_ = octave;
+    rawDataCurrentKey_ = key;
+    
+	shouldStop_ = false;
+    rawDataThread_.startThread();
+    
+	if(verbose_ >= 1)
+		cout << "Starting raw data collection from octave " << octave << ", key " << key << endl;
+	
+	autoGathering_ = true;
+    
+    cout << "running\n";
+	/*if(write(device_, (char*)command, 9) < 0) {
+		cout << "ERROR: unable to write startRawDataCollection command.  errno = " << errno << endl;
+	}
+	tcdrain(device_);*/
+
+	//previousTouchData_.clear();
+	//previousTouchActive_.clear();
+	
+	return true;
+}
+
+// Set the scan interval in milliseconds.  Returns true on success.
+
+bool TouchkeyDevice::setScanInterval(int intervalMilliseconds) {
+	if(!isOpen())
+		return false;	
+	if(intervalMilliseconds <= 0 || intervalMilliseconds > 255)
+		return false;
+	
+	unsigned char command[] = {ESCAPE_CHARACTER, kControlCharacterFrameBegin,
+		kFrameTypeScanRate, (unsigned char)(intervalMilliseconds & 0xFF), ESCAPE_CHARACTER, kControlCharacterFrameEnd};
+	
+	// Send command
+	if(write(device_, (char*)command, 6) < 0) {
+		cout << "ERROR: unable to write startRawDataCollection command.  errno = " << errno << endl;
+	}
+	tcdrain(device_);
+	
+	if(verbose_ >= 2)
+		cout << "Setting scan interval to " << intervalMilliseconds << endl;
+	
+	// Return value depends on ACK or NAK received
+	return checkForAck(250);
+}
+
+// Key parameters.  Setting octave or key to -1 means all octaves or all keys, respectively.
+// This controls the sensitivity of the capacitive touch sensing system on each key.
+// It is a balance between achieving the best range of data and not saturating the sensors
+// for the largest touches.
+bool TouchkeyDevice::setKeySensitivity(int octave, int key, int value) {
+	unsigned char chOctave, chKey, chVal;
+	
+	if(!isOpen())
+		return false;
+	if(octave > 255)
+		return false;
+	if(key > 12)
+		return false;
+	if(value > 255 || value < 0)
+		return false;
+	if(octave < 0)
+		chOctave = 0xFF;
+	else 
+		chOctave = (unsigned char)(octave & 0xFF);
+	if(key < 0)
+		chKey = 0xFF;
+	else
+		chKey = (unsigned char)(key & 0xFF);
+	chVal = (unsigned char)value;
+	
+	unsigned char command[] = {ESCAPE_CHARACTER, kControlCharacterFrameBegin, kFrameTypeSensitivity,
+		chOctave, chKey, chVal, ESCAPE_CHARACTER, kControlCharacterFrameEnd};
+	
+	// Send command
+	if(write(device_, (char*)command, 8) < 0) {
+		cout << "ERROR: unable to write setKeySensitivity command.  errno = " << errno << endl;
+	}
+	tcdrain(device_);
+	
+	if(verbose_ >= 2)
+		cout << "Setting sensitivity to " << value << endl;
+	
+	// Return value depends on ACK or NAK received
+	return checkForAck(250);	
+}
+
+// Change how the calculated centroids are scaled to fit in a single byte. They
+// will be right-shifted by the indicated number of bits before being transmitted.
+bool TouchkeyDevice::setKeyCentroidScaler(int octave, int key, int value) {
+	unsigned char chOctave, chKey, chVal;
+	
+	if(!isOpen())
+		return false;	
+	if(octave > 255)
+		return false;
+	if(key > 12)
+		return false;
+	if(value > 7 || value < 0)
+		return false;
+	if(octave < 0)
+		chOctave = 0xFF;
+	else 
+		chOctave = (unsigned char)(octave & 0xFF);
+	if(key < 0)
+		chKey = 0xFF;
+	else
+		chKey = (unsigned char)(key & 0xFF);
+	chVal = (unsigned char)value;
+	
+	unsigned char command[] = {ESCAPE_CHARACTER, kControlCharacterFrameBegin, kFrameTypeSizeScaler,
+		chOctave, chKey, chVal, ESCAPE_CHARACTER, kControlCharacterFrameEnd};
+	
+	// Send command
+	if(write(device_, (char*)command, 8) < 0) {
+		cout << "ERROR: unable to write setKeyCentroidScaler command.  errno = " << errno << endl;
+	}
+	tcdrain(device_);
+	
+	if(verbose_ >= 2)
+		cout << "Setting size scaler to " << value << endl;
+	
+	// Return value depends on ACK or NAK received
+	return checkForAck(250);
+}
+
+// Set the minimum size of a centroid calculated on the key which is considered
+// "real" and not noise.
+bool TouchkeyDevice::setKeyMinimumCentroidSize(int octave, int key, int value) {
+	unsigned char chOctave, chKey, chValHi, chValLo;
+	
+	if(!isOpen())
+		return false;	
+	if(octave > 255)
+		return false;
+	if(key > 12)
+		return false;
+	if(value > 0xFFFF || value < 0)
+		return false;
+	if(octave < 0)
+		chOctave = 0xFF;
+	else 
+		chOctave = (unsigned char)(octave & 0xFF);
+	if(key < 0)
+		chKey = 0xFF;
+	else
+		chKey = (unsigned char)(key & 0xFF);
+	chValHi = (unsigned char)((value >> 8) & 0xFF);
+	chValLo = (unsigned char)(value & 0xFF);
+	
+	unsigned char command[] = {ESCAPE_CHARACTER, kControlCharacterFrameBegin, kFrameTypeMinimumSize,
+		chOctave, chKey, chValHi, chValLo, ESCAPE_CHARACTER, kControlCharacterFrameEnd};
+	
+	// Send command
+	if(write(device_, (char*)command, 9) < 0) {
+		cout << "ERROR: unable to write setKeyMinimumCentroidSize command.  errno = " << errno << endl;
+	}
+	tcdrain(device_);
+	
+	if(verbose_ >= 2)
+		cout << "Setting minimum centroid size to " << value << endl;
+	
+	// Return value depends on ACK or NAK received
+	return checkForAck(250);	
+}
+
+// Set the noise threshold for individual sensor pads: the reading must exceed
+// the background value by this amount to be considered an actual touch.
+bool TouchkeyDevice::setKeyNoiseThreshold(int octave, int key, int value) {
+	unsigned char chOctave, chKey, chVal;
+	
+	if(octave > 255)
+		return false;
+	if(key > 12)
+		return false;
+	if(value > 255 || value < 0)
+		return false;
+	if(octave < 0)
+		chOctave = 0xFF;
+	else 
+		chOctave = (unsigned char)(octave & 0xFF);
+	if(key < 0)
+		chKey = 0xFF;
+	else
+		chKey = (unsigned char)(key & 0xFF);
+	chVal = (unsigned char)value;
+	
+	unsigned char command[] = {ESCAPE_CHARACTER, kControlCharacterFrameBegin, kFrameTypeNoiseThreshold,
+		chOctave, chKey, chVal, ESCAPE_CHARACTER, kControlCharacterFrameEnd};
+	
+	// Send command
+	if(write(device_, (char*)command, 8) < 0) {
+		cout << "ERROR: unable to write setKeyNoiseThreshold command.  errno = " << errno << endl;
+	}
+	tcdrain(device_);
+	
+	if(verbose_ >= 2)
+		cout << "Setting noise threshold to " << value << endl;
+	
+	// Return value depends on ACK or NAK received
+	return checkForAck(250);	
+}
+
+// Jump to the built-in bootloader of the TouchKeys device
+void TouchkeyDevice::jumpToBootloader() {
+	unsigned char command[] = {ESCAPE_CHARACTER, kControlCharacterFrameBegin, kFrameTypeEnterSelfProgramMode,
+		ESCAPE_CHARACTER, kControlCharacterFrameEnd};
+	
+	// Send command
+	if(write(device_, (char*)command, 5) < 0) {
+		cout << "ERROR: unable to write jumpToBootloader command.  errno = " << errno << endl;
+	}
+	tcdrain(device_);
+}
+
+// Set the LED color for the given MIDI note (if RGB LEDs are present). This method
+// does not directly communicate with the device, but it schedules an update to take
+// place in the relevant thread.
+void TouchkeyDevice::rgbledSetColor(const int midiNote, const float red, const float green, const float blue) {
+    RGBLEDUpdate updateStructure;
+    
+    updateStructure.allLedsOff = false;
+    updateStructure.midiNote = midiNote;
+    
+    // Convert 0-1 floating point range to 0-255
+    updateStructure.red = (int)(red * 4095.0);
+    updateStructure.green = (int)(green * 4095.0);
+    updateStructure.blue = (int)(blue * 4095.0);
+    
+    ledUpdateQueue_.push_front(updateStructure);
+}
+
+// Same as rgbledSetColor() but uses HSV format color instead of RGB
+void TouchkeyDevice::rgbledSetColorHSV(const int midiNote, const float hue, const float saturation, const float value) {
+    float red = 0, green = 0, blue = 0;
+    float chroma = value * saturation;
+    
+    // Hue will lie on one of 6 segments from 0 to 1; convert this from 0 to 6.
+    float hueSegment = hue * 6.0;
+    float x = chroma * (1.0 - fabsf(fmodf(hueSegment, 2.0) - 1.0));
+    
+    if(hueSegment < 1.0) {
+        red = chroma;
+        green = x;
+        blue = 0;
+    }
+    else if(hueSegment < 2.0) {
+        red = x;
+        green = chroma;
+        blue = 0;
+    }
+    else if(hueSegment < 3.0) {
+        red = 0;
+        green = chroma;
+        blue = x;
+    }
+    else if(hueSegment < 4.0) {
+        red = 0;
+        green = x;
+        blue = chroma;
+    }
+    else if(hueSegment < 5.0) {
+        red = x;
+        green = 0;
+        blue = chroma;
+    }
+    else {
+        red = chroma;
+        green = 0;
+        blue = x;
+    }
+
+    rgbledSetColor(midiNote, red, green, blue);
+}
+
+// Set all RGB LEDs off (if RGB LEDs are present). This method does not
+// directly communicate with the device, but it schedules an update to take
+// place in the relevant thread.
+void TouchkeyDevice::rgbledAllOff() {
+    RGBLEDUpdate updateStructure;
+    
+    updateStructure.allLedsOff = true;
+    updateStructure.midiNote = 0;
+    updateStructure.red = 0;
+    updateStructure.green = 0;
+    updateStructure.blue = 0;
+    
+    ledUpdateQueue_.push_front(updateStructure);
+}
+
+// Set the color of a given RGB LED (piano scanner boards only). LEDs are numbered from 0-24
+// starting at left. Boards are numbered 0-3 starting at left.
+bool TouchkeyDevice::internalRGBLEDSetColor(const int device, const int led, const int red, const int green, const int blue) {
+	if(!isOpen())
+		return false;
+    if(!deviceHasRGBLEDs_)
+        return false;
+    if(device < 0 || device > 3)
+        return false;
+    if(led < 0 || led > 24)
+        return false;
+    if(red < 0 || red > 4095)
+        return false;
+    if(green < 0 || green > 4095)
+        return false;
+    if(blue < 0 || blue > 4095)
+        return false;
+    
+    unsigned char command[17]; // 11 bytes + possibly 6 doubled characters
+    
+    // There's a chance that one of the bytes will come out to ESCAPE_CHARACTER (0xFE) depending
+    // on LED color. We need to double up any bytes that come in that way.
+    
+    command[0] = ESCAPE_CHARACTER;
+    command[1] = kControlCharacterFrameBegin;
+    command[2] = kFrameTypeRGBLEDSetColors;
+    
+    int byte, location = 3;
+    
+    byte = (((unsigned char)device & 0xFF) << 6) | (unsigned char)led;
+    command[location++] = byte;
+    if(byte == ESCAPE_CHARACTER)
+        command[location++] = byte;
+    byte = (red >> 4) & 0xFF;
+    command[location++] = byte;
+    if(byte == ESCAPE_CHARACTER)
+        command[location++] = byte;
+    byte = ((red << 4) & 0xF0) | ((green >> 8) & 0x0F);
+    command[location++] = byte;
+    if(byte == ESCAPE_CHARACTER)
+        command[location++] = byte;
+    byte = (green & 0xFF);
+    command[location++] = byte;
+    if(byte == ESCAPE_CHARACTER)
+        command[location++] = byte;
+    byte = (blue >> 4) & 0xFF;
+    command[location++] = byte;
+    if(byte == ESCAPE_CHARACTER)
+        command[location++] = byte;
+    byte = (blue << 4) & 0xF0;
+    command[location++] = byte;
+    if(byte == ESCAPE_CHARACTER)
+        command[location++] = byte;
+    command[location++] = ESCAPE_CHARACTER;
+    command[location++] = kControlCharacterFrameEnd;
+    
+	// Send command
+	if(write(device_, (char*)command, location) < 0) {
+		cout << "ERROR: unable to write setRGBLEDColor command.  errno = " << errno << endl;
+	}
+	tcdrain(device_);
+	
+	if(verbose_ >= 3)
+		cout << "Setting RGB LED color for device " << device << ", led " << led << endl;
+        
+	// Return value depends on ACK or NAK received
+	return true; //checkForAck(20);
+}
+
+// Turn off all RGB LEDs on a given board
+bool TouchkeyDevice::internalRGBLEDAllOff() {
+	if(!isOpen())
+		return false;
+    if(!deviceHasRGBLEDs_)
+        return false;
+    
+    unsigned char command[5];
+    
+    command[0] = ESCAPE_CHARACTER;
+    command[1] = kControlCharacterFrameBegin;
+    command[2] = kFrameTypeRGBLEDAllOff;
+    command[3] = ESCAPE_CHARACTER;
+    command[4] = kControlCharacterFrameEnd;
+	
+	// Send command
+	if(write(device_, (char*)command, 5) < 0) {
+		cout << "ERROR: unable to write setRGBLEDAllOff command.  errno = " << errno << endl;
+	}
+	tcdrain(device_);
+	
+	if(verbose_ >= 3)
+		cout << "Turning off all RGB LEDs" << endl;
+    
+	// Return value depends on ACK or NAK received
+	return true; //checkForAck(20);
+}
+
+// Get board number for MIDI note
+int TouchkeyDevice::internalRGBLEDMIDIToBoardNumber(const int midiNote) {
+    // lowestMidiNote_ holds the very bottom LED on the bottom board. The boards
+    // go up by two-octave sets from there. The top board has one extra LED (high C).
+    
+    if(midiNote > lowestMidiNote_ + 96)
+        return -1;
+    if(midiNote >= lowestMidiNote_ + 72)
+        return 3;
+    else if(midiNote >= lowestMidiNote_ + 48)
+        return 2;
+    else if(midiNote >= lowestMidiNote_ + 24)
+        return 1;
+    else if(midiNote >= lowestMidiNote_)
+        return 0;
+    return -1;
+}
+
+// Get LED number for MIDI note (within a board)
+int TouchkeyDevice::internalRGBLEDMIDIToLEDNumber(const int midiNote) {
+    // Take the note number relative to the lowest note of the whole device.
+    // Once it's located within a board (2-octaves each), the offset gives us the LED number.
+    // However, the lowest board works differently as it only has 15 LEDs which start at A,
+    // not at C.
+    const int midiNoteOffset = midiNote - lowestMidiNote_;
+    
+    if(midiNoteOffset < 9) // Below the bottom A, hence invalid
+        return -1;
+    if(midiNoteOffset < 24) {
+        // Within 2 octaves of bottom --> lowest board --> adjust for 15 LEDs on board
+        return midiNoteOffset - 9;
+    }
+    else if(midiNoteOffset < 48) {
+        // Board 1
+        return midiNoteOffset - 24;
+    }
+    else if(midiNoteOffset < 72) {
+        // Board 2
+        return midiNoteOffset - 48;
+    }
+    else if(midiNoteOffset < 97) {
+        // Board 3 includes a top C (index 24)
+        return midiNoteOffset - 72;
+    }
+    return -1;
+}
+
+// ***** Calibration Methods *****
+
+// Start calibrating selected keys and pedals. If argument is NULL, assume it applies to all keys.
+void TouchkeyDevice::calibrationStart(std::vector<int>* keysToCalibrate) {
+	if(keysToCalibrate == 0) {
+		for(int i = 0; i < keyCalibratorsLength_; i++)
+			keyCalibrators_[i]->calibrationStart();
+	}
+	else {
+		std::vector<int>::iterator it;
+		for(it = keysToCalibrate->begin(); it != keysToCalibrate->end(); it++) {
+			if(*it >= 0 && *it < keyCalibratorsLength_)
+				keyCalibrators_[*it]->calibrationStart();
+		}
+	}
+	
+	calibrationInProgress_ = true;
+}
+
+// Finish the current calibration in progress.  Pass it on to all Calibrators, and the ones that weren't
+// calibrating will just ignore it.
+void TouchkeyDevice::calibrationFinish() {
+    bool calibratedAtLeastOneKey = false;
+    
+	for(int i = 0; i < keyCalibratorsLength_; i++) {
+        // Check if calibration was successful
+		if(keyCalibrators_[i]->calibrationFinish()) {
+            calibratedAtLeastOneKey = true;
+            // Update the display if available
+            if(keyboard_.gui() != 0) {
+                keyboard_.gui()->setAnalogCalibrationStatusForKey(i + lowestMidiNote_, true);
+            }
+        }
+    }
+	
+	calibrationInProgress_ = false;
+	isCalibrated_ = calibratedAtLeastOneKey;
+}
+
+// Abort a calibration in progress, without saving its results. Pass it on to all Calibrators.
+void TouchkeyDevice::calibrationAbort() {
+	for(int i = 0; i < keyCalibratorsLength_; i++)
+		keyCalibrators_[i]->calibrationAbort();
+	
+	calibrationInProgress_ = false;
+}
+
+// Clear the existing calibration, reverting to an uncalibrated state.
+void TouchkeyDevice::calibrationClear() {
+	for(int i = 0; i < keyCalibratorsLength_; i++) {
+		keyCalibrators_[i]->calibrationClear();
+        if(keyboard_.gui() != 0) {
+            keyboard_.gui()->setAnalogCalibrationStatusForKey(i + lowestMidiNote_, false);
+        }
+    }
+
+	calibrationInProgress_ = false;
+	isCalibrated_ = false;
+}
+
+// Save calibration data to a file
+bool TouchkeyDevice::calibrationSaveToFile(std::string const& filename) {
+	int i;
+	
+	if(!isCalibrated()) {
+		std::cerr << "TouchKeys not calibrated, so can't save calibration data.\n";
+		return false;
+	}
+	
+	// Create an XML structure and save it to file.
+	try {
+		XmlElement baseElement("TouchkeyDeviceCalibration");
+		bool savedValidData = false;
+		
+		for(i = 0; i < keyCalibratorsLength_; i++) {
+            XmlElement *calibrationElement = baseElement.createNewChildElement("Key");
+            if(calibrationElement == 0)
+                continue;
+            
+			calibrationElement->setAttribute("id", i);
+			
+			// Tell each individual calibrator to add its data to the XML tree
+			if(keyCalibrators_[i]->saveToXml(*calibrationElement)) {
+				savedValidData = true;
+			}
+		}
+		
+		if(!savedValidData) {
+			std::cerr << "TouchkeyDevice: unable to find valid calibration data to save.\n";
+			throw 1;
+		}
+		
+		// Now save the generated tree to a file
+        
+		if(!baseElement.writeToFile(File(filename.c_str()), "")) {
+			std::cerr << "TouchkeyDevice: could not write calibration file " << filename << "\n";
+			throw 1;
+		}
+		
+		//lastCalibrationFile_ = filename;
+	}
+	catch(...) {
+		return false;
+	}
+	
+	return true;
+}
+
+// Load calibration from a file
+bool TouchkeyDevice::calibrationLoadFromFile(std::string const& filename) {
+	//int i, j;
+    
+	calibrationClear();
+	
+	// Open the file and read the new values
+	try {
+        XmlDocument doc(File(filename.c_str()));
+		XmlElement *baseElement = doc.getDocumentElement();
+        XmlElement *deviceCalibrationElement, *calibratorElement;
+		
+		if(baseElement == 0) {
+			std::cerr << "TouchkeyDevice: unable to load patch table file: \"" << filename << "\". Error was:\n";
+			std::cerr << doc.getLastParseError() << std::endl;
+			throw 1;
+		}
+		
+		// All calibration data is encapsulated within the root element <PianoBarCalibration>
+		deviceCalibrationElement = baseElement->getChildByName("TouchkeyDeviceCalibration");
+		if(deviceCalibrationElement == 0) {
+			std::cerr << "TouchkeyDevice: malformed calibration file, aborting.\n";
+            delete baseElement;
+			throw 1;
+		}
+		
+		// Go through and find each key's calibration information
+		calibratorElement = deviceCalibrationElement->getChildByName("Key");
+		if(calibratorElement == 0) {
+			std::cerr << "TouchkeyDevice: warning: no keys found\n";
+		}
+		else {
+			while(calibratorElement != 0) {
+				int keyId;
+                
+                if(calibratorElement->hasAttribute("id")) {
+                    keyId = calibratorElement->getIntAttribute("id");
+					if(keyId >= 0 && keyId < keyCalibratorsLength_)
+						keyCalibrators_[keyId]->loadFromXml(*calibratorElement);
+                }
+
+				calibratorElement = calibratorElement->getNextElementWithTagName("Key");
+			}
+		}
+        
+        calibrationInProgress_ = false;
+        isCalibrated_ = true;
+        if(keyboard_.gui() != 0) {
+            for(int i = lowestMidiNote_; i <  lowestMidiNote_ + 12*numOctaves_; i++) {
+                keyboard_.gui()->setAnalogCalibrationStatusForKey(i, true);
+            }
+        }
+		//lastCalibrationFile_ = filename;
+        
+        delete baseElement;
+	}
+	catch(...) {
+		return false;
+	}
+	
+	// TODO: reset key states?
+	
+	return true;
+}
+
+// Initialize the calibrators
+void TouchkeyDevice::calibrationInit(int numberOfCalibrators) {
+    if(keyCalibrators_ != 0)
+        calibrationDeinit();
+    if(numberOfCalibrators <= 0)
+        return;
+    keyCalibratorsLength_ = numberOfCalibrators;
+    
+    // Initialize the calibrator array
+    keyCalibrators_ = (PianoKeyCalibrator **)malloc(keyCalibratorsLength_ * sizeof(PianoKeyCalibrator*));
+    
+    for(int i = 0; i < keyCalibratorsLength_; i++) {
+		keyCalibrators_[i] = new PianoKeyCalibrator(true, 0);
+	}
+    
+    calibrationClear();
+}
+
+// Free the initialized calibrators
+void TouchkeyDevice::calibrationDeinit() {
+    if(keyCalibrators_ == 0)
+        return;
+    
+	for(int i = 0; i < keyCalibratorsLength_; i++) {
+        if(keyCalibrators_[i] != 0)
+            delete keyCalibrators_[i];
+        keyCalibrators_[i] = 0;
+    }
+    free(keyCalibrators_);
+    
+    keyCalibratorsLength_ = 0;
+    isCalibrated_ = calibrationInProgress_ = false;
+}
+
+// Update the lowest MIDI note of the TouchKeys device
+void TouchkeyDevice::setLowestMidiNote(int note) {
+    // If running, save the value in a temporary holding place until
+    // the data gathering thread makes the update. Otherwise update right away.
+    // This avoids things changing during data processing and other threading problems.
+    if(isAutoGathering())
+        updatedLowestMidiNote_ = note;
+    else {
+        lowestKeyPresentMidiNote_ += (note - lowestMidiNote_);
+        lowestMidiNote_ = updatedLowestMidiNote_ = note;
+        if(isOpen())
+            keyboard_.setKeyboardGUIRange(lowestMidiNote_, lowestMidiNote_ + 12*numOctaves_);
+    }
+}
+
+// Loop for sending LED updates to the device, which must happen
+// in a separate thread from data collection so the device's capacity
+// to process incoming data doesn't gate its transmission of sensor data
+void TouchkeyDevice::ledUpdateLoop(DeviceThread *thread) {
+    
+    // Run until told to stop, looking for updates to send to the board
+    while(!shouldStop_ && !ledShouldStop_ && !thread->threadShouldExit()) {
+        while(!ledUpdateQueue_.empty()) {
+            // Get the update
+            RGBLEDUpdate& updateStructure = ledUpdateQueue_.back();
+            
+            if(updateStructure.allLedsOff) {
+                internalRGBLEDAllOff();
+            }
+            else {
+                // Convert MIDI note number to board/LED pair. If valid, send to device.
+                int board = internalRGBLEDMIDIToBoardNumber(updateStructure.midiNote);
+                int led = internalRGBLEDMIDIToLEDNumber(updateStructure.midiNote);
+                
+                if(board >= 0 && board <= 3 && led >= 0)
+                    internalRGBLEDSetColor(board, led, updateStructure.red, updateStructure.green, updateStructure.blue);
+            }
+            
+            // Remove the update we just transmitted
+            ledUpdateQueue_.pop_back();
+        }
+        
+        usleep(20000);  // Wait 20ms to check again
+    }
+}
+
+// Main run loop, which runs in its own thread
+void TouchkeyDevice::runLoop(DeviceThread *thread) {
+	unsigned char buffer[1024];							// Raw data from device
+	unsigned char frame[TOUCHKEY_MAX_FRAME_LENGTH];		// Accumulated frame of data
+	int frameLength;
+	bool controlSeq = false, inFrame = false, frameError = false;
+
+   /* struct timeval currentTime;
+    unsigned long long currentTicks = 0, lastTicks = 0;
+    int currentNote = 21;*/
+
+	// Continuously read from the input device.  Read as much data as is available, up to
+	// 1024 bytes at a time.  If no data is available, wait 0.5ms before trying again.  USB
+	// data comes in every 1ms, so this guarantees no more than a 1ms wait for data, and often less.
+	
+	while(!shouldStop_ && !thread->threadShouldExit()) {
+        
+/*
+            // This code for RGBLED testing
+            gettimeofday(&currentTime, 0);
+            
+            currentTicks = currentTime.tv_sec * 1000000ULL + currentTime.tv_usec;
+            if(currentTicks - lastTicks > 50000ULL) {
+                lastTicks = currentTicks;
+                rgbledSetColor(currentNote, 0, 0, 0);
+                currentNote++;
+                if(currentNote > highestMidiNote()) {
+                    rgbledAllOff();
+                    currentNote = 21;
+                }
+                rgbledSetColorHSV(currentNote, (float)(currentNote - 21)/(float)(highestMidiNote() - 21), 1.0, 1.0);
+            }
+*/        
+ 		long count = read(device_, (char *)buffer, 1024);
+		
+		if(count == 0) {
+			usleep(500);
+			continue;
+		}
+		if(count < 0) {
+			if(errno != EAGAIN) {	// EAGAIN just means no data was available
+				cout << "Unable to read from device (error " << errno << ").  Aborting.\n";
+				shouldStop_ = true;
+			}
+			
+			usleep(500);
+			continue;
+		}	
+		
+		// Process the received data
+		
+		for(int i = 0; i < count; i++) {
+			unsigned char ch = buffer[i];
+		
+			if(inFrame) {
+				// Receiving a frame
+				
+				if(controlSeq) {
+					controlSeq = false;
+					if(ch == kControlCharacterFrameEnd)	{		// frame finished?
+						inFrame = false;
+						processFrame(frame, frameLength);
+					}
+					else if(ch == kControlCharacterFrameError) { // device telling us about an internal comm error
+						if(verbose_ >= 1)
+							cout << "Warning: received frame error, continuing anyway.\n";
+						frameError = true;
+					}
+					else if(ch == ESCAPE_CHARACTER) {			// double-escape means a literal escape character
+						frame[frameLength++] = ch;
+						if(frameLength >= TOUCHKEY_MAX_FRAME_LENGTH) {
+							inFrame = false;
+							if(verbose_ >= 1)
+								cout << "Warning: ignoring frame exceeding length limit " << (int)TOUCHKEY_MAX_FRAME_LENGTH << endl;
+						}				
+					}
+					else if(ch == kControlCharacterNak && verbose_ >= 1) {
+                        // TODO: pass this on to a checkForAck() call
+						cout << "Warning: received NAK (while receiving frame)\n";
+					}			
+				}
+				else {
+					if(ch == ESCAPE_CHARACTER)
+						controlSeq = true;
+					else {
+						frame[frameLength++] = ch;
+						if(frameLength >= TOUCHKEY_MAX_FRAME_LENGTH) {
+							inFrame = false;
+							if(verbose_ >= 1)
+								cout << "Warning: ignoring frame exceeding length limit " << (int)TOUCHKEY_MAX_FRAME_LENGTH << endl;
+						}
+					}
+				}				
+			}
+			else {
+				// Waiting for a frame beginning control sequence
+				
+				if(controlSeq) {
+					controlSeq = false;
+					if(ch == kControlCharacterFrameBegin) {
+						inFrame = true;
+						frameLength = 0;
+						frameError = false;
+					}
+					else if(ch == kControlCharacterNak && verbose_ >= 1) {
+                        // TODO: pass this on to a checkForAck() call
+						cout << "Warning: received NAK (while waiting for frame)\n";
+					}
+				}
+				else {
+					if(ch == ESCAPE_CHARACTER)
+						controlSeq = true;
+				}
+			}
+		}
+	}
+}
+
+// Main run loop for gathering raw data from a particular key, used for debugging
+// and testing purposes
+void TouchkeyDevice::rawDataRunLoop(DeviceThread *thread) {
+	unsigned char buffer[1024];							// Raw data from device
+	unsigned char frame[TOUCHKEY_MAX_FRAME_LENGTH];		// Accumulated frame of data
+	int frameLength;
+	bool controlSeq = false, inFrame = false, frameError = false;
+    
+    unsigned char gatherDataCommand[] = {ESCAPE_CHARACTER, kControlCharacterFrameBegin,
+        kFrameTypeSendI2CCommand, (unsigned char)rawDataCurrentOctave_, (unsigned char)rawDataCurrentKey_,
+        0 /* xmit */, 26 /* response */, 
+        ESCAPE_CHARACTER, kControlCharacterFrameEnd};
+    
+    //struct timeval currentTime;
+    double currentTime = 0, lastTime = 0;
+    //unsigned long long currentTicks = 0, lastTicks = 0;
+
+	// Continuously read from the input device.  Read as much data as is available, up to
+	// 1024 bytes at a time.  If no data is available, wait 0.5ms before trying again.  USB
+	// data comes in every 1ms, so this guarantees no more than a 1ms wait for data, and often less.
+	
+	while(!shouldStop_ && !thread->threadShouldExit()) {
+        // Every 100ms, request raw data from the active key
+        currentTime = Time::getMillisecondCounterHiRes();
+        
+        if(currentTime - lastTime > 0.1) {
+            lastTime = currentTime;
+            // Request data
+            if(write(device_, (char*)gatherDataCommand, 9) < 0) {
+                cout << "ERROR: unable to write setMode command.  errno = " << errno << endl;
+            }
+            tcdrain(device_);
+            cout << "wrote to device\n";
+        }
+        
+ 		long count = read(device_, (char *)buffer, 1024);
+		
+		if(count == 0) {
+			usleep(500);
+			continue;
+		}
+		if(count < 0) {
+			if(errno != EAGAIN) {	// EAGAIN just means no data was available
+				cout << "Unable to read from device (error " << errno << ").  Aborting.\n";
+				shouldStop_ = true;
+			}
+			
+			usleep(500);
+			continue;
+		}
+		
+		// Process the received data
+		
+		for(int i = 0; i < count; i++) {
+			unsigned char ch = buffer[i];
+            
+			if(inFrame) {
+				// Receiving a frame
+				
+				if(controlSeq) {
+					controlSeq = false;
+					if(ch == kControlCharacterFrameEnd)	{		// frame finished?
+						inFrame = false;
+						processFrame(frame, frameLength);
+					}
+					else if(ch == kControlCharacterFrameError) { // device telling us about an internal comm error
+						if(verbose_ >= 1)
+							cout << "Warning: received frame error, continuing anyway.\n";
+						frameError = true;
+					}
+					else if(ch == ESCAPE_CHARACTER) {			// double-escape means a literal escape character
+						frame[frameLength++] = ch;
+						if(frameLength >= TOUCHKEY_MAX_FRAME_LENGTH) {
+							inFrame = false;
+							if(verbose_ >= 1)
+								cout << "Warning: ignoring frame exceeding length limit " << (int)TOUCHKEY_MAX_FRAME_LENGTH << endl;
+						}
+					}
+					else if(ch == kControlCharacterNak && verbose_ >= 1) {
+                        // TODO: pass this on to a checkForAck() call
+						cout << "Warning: received NAK (while receiving frame)\n";
+					}
+				}
+				else {
+					if(ch == ESCAPE_CHARACTER)
+						controlSeq = true;
+					else {
+						frame[frameLength++] = ch;
+						if(frameLength >= TOUCHKEY_MAX_FRAME_LENGTH) {
+							inFrame = false;
+							if(verbose_ >= 1)
+								cout << "Warning: ignoring frame exceeding length limit " << (int)TOUCHKEY_MAX_FRAME_LENGTH << endl;
+						}
+					}
+				}
+			}
+			else {
+				// Waiting for a frame beginning control sequence
+				
+				if(controlSeq) {
+					controlSeq = false;
+					if(ch == kControlCharacterFrameBegin) {
+						inFrame = true;
+						frameLength = 0;
+						frameError = false;
+					}
+					else if(ch == kControlCharacterNak && verbose_ >= 1) {
+                        // TODO: pass this on to a checkForAck() call
+						cout << "Warning: received NAK (while waiting for frame)\n";
+					}
+				}
+				else {
+					if(ch == ESCAPE_CHARACTER)
+						controlSeq = true;
+				}
+			}
+		}
+	}
+}
+
+// Process the contents of a frame that has been received from the device
+void TouchkeyDevice::processFrame(unsigned char * const frame, int length) {
+	if(length == 0)	// Empty frame --> nothing to do here
+		return;
+	
+	switch(frame[0]) { // First character gives frame type
+		case kFrameTypeCentroid:
+			if(verbose_ >= 3)
+				cout << "Received centroid data\n";
+			processCentroidFrame(&frame[1], length - 1);
+			break;
+		case kFrameTypeRawKeyData:
+			if(verbose_ >= 3)
+				cout << "Received raw key data\n";
+			processRawDataFrame(&frame[1], length - 1);
+			break;
+        case kFrameTypeAnalog:
+			if(verbose_ >= 3)
+				cout << "Received analog data\n";
+            processAnalogFrame(&frame[1], length - 1);
+            break;
+        case kFrameTypeErrorMessage:
+            if(verbose_ >= 3)
+				cout << "Received error data\n";
+            processErrorMessageFrame(&frame[1], length-1);
+            break;
+        case kFrameTypeI2CResponse:
+            if(verbose_ >= 3)
+                cout << "Received I2C response\n";
+            processI2CResponseFrame(&frame[1], length - 1);
+            break;
+		case kFrameTypeStatus:
+		default:
+			if(verbose_ >= 3)
+				cout << "Received frame type " << (int)frame[0] << endl;			
+			break;
+	}	
+}
+
+// Process a frame of data containing centroid values (the default mode of scanning)
+void TouchkeyDevice::processCentroidFrame(unsigned char * const buffer, const int bufferLength) {
+    int frame, octave, bufferIndex;
+    
+    // Old and new generation devices structure the frame differently. 
+	if((deviceSoftwareVersion_ <= 0 && bufferLength < 3) || (deviceSoftwareVersion_ > 0 && bufferLength < 5)) {
+		if(verbose_ >= 1)
+			cout << "Warning: ignoring malformed centroid frame of " << bufferLength << " bytes, less than minimum 3\n";
+		if(verbose_ >= 2) {
+			cout << "  Contents: ";
+			hexDump(cout, buffer, bufferLength);
+			cout << endl;
+		}
+		return;
+	}
+	
+	if(verbose_ >= 4) {
+		cout << "Centroid frame contents:  ";
+		hexDump(cout, buffer, bufferLength);
+		cout << endl;		
+	}
+	
+    // Parse the octave and timestamp differently depending on hardware version
+    if(deviceSoftwareVersion_ > 0) {
+        octave = buffer[0]; // First byte is octave
+        
+        // Frame is stored as 32-bit little endian value
+        frame = buffer[1] + ((int)buffer[2] << 8) + ((int)buffer[3] << 16) + ((int)buffer[4] << 24);
+        bufferIndex = 5;
+        
+        if(verbose_ >= 3)
+            cout << "Centroid frame octave " << octave << " timestamp " << frame << endl;
+    }
+    else {
+        frame = (buffer[0] << 8) + buffer[1];	// First two bytes give us the timestamp in milliseconds (mod 2^16)
+        octave = buffer[2];	// Third byte tells us which octave of keys is being addressed
+        bufferIndex = 3;
+	}
+    
+	// Convert from device frame number (expressed in USB 1ms SOF intervals) to a system
+	// timestamp that can be synchronized with other data streams
+	lastTimestamp_ = timestampSynchronizer_.synchronizedTimestamp(frame);
+	
+	//ioMutex_.enter();
+	
+	while(bufferIndex < bufferLength) {
+		// First byte tells us the number of the key (0-12); next bytes hold the data frame
+		int key = (int)buffer[bufferIndex++];
+		int bytesParsed = processKeyCentroid(frame,octave, key, lastTimestamp_, &buffer[bufferIndex], bufferLength - bufferIndex);
+		
+		if(bytesParsed < 0)  {
+			if(verbose_ >= 1)
+				cout << "Warning: malformed data frame (parsing key " << key << " at byte " << bufferIndex << ")\n";
+			
+			if(verbose_ >= 2) {
+				cout << "--> Data: ";
+				hexDump(cout, buffer, bufferLength);
+				cout << endl;
+			}
+			
+			break;
+		}
+		
+		bufferIndex += bytesParsed;
+	}
+    
+    if(updatedLowestMidiNote_ != lowestMidiNote_) {
+        int keyPresentDifference = (lowestKeyPresentMidiNote_ - lowestMidiNote_);
+        
+		lowestMidiNote_ = updatedLowestMidiNote_;
+        lowestKeyPresentMidiNote_ = lowestMidiNote_ + keyPresentDifference;
+
+        // Turn off all existing touches before changing the octave
+        // so we don't end up with orphan touches when the "off" message is
+        // sent to a different octave than the "on"
+        for(int i = 0; i <= 127; i++)
+            if(keyboard_.key(i) != 0)
+                if(keyboard_.key(i)->touchIsActive())
+                    keyboard_.key(i)->touchOff(lastTimestamp_);
+        
+        keyboard_.setKeyboardGUIRange(lowestMidiNote_, lowestMidiNote_ + 12*numOctaves_);
+    }
+	
+	//ioMutex_.exit();
+}
+
+// Process a frame containing raw key data, whose configuration was set with startRawDataCollection()
+// First byte holds the octave that the data came from.
+
+void TouchkeyDevice::processRawDataFrame(unsigned char * const buffer, const int bufferLength) {
+	int octave = buffer[0];
+	
+	if(verbose_ >= 3)
+		cout << "Raw data frame from octave " << octave << " contains " << bufferLength - 1 << " samples\n";
+	
+	if(verbose_ >= 4) {
+		cout << "  ";
+		hexDump(cout, &buffer[1], bufferLength - 1);
+		cout << endl;		
+	}
+
+	// Send raw data as an OSC blob
+	lo_blob b = lo_blob_new(bufferLength, buffer);
+	keyboard_.sendMessage("/touchkeys/rawbytes", "b", b, LO_ARGS_END);
+	lo_blob_free(b);	
+}
+
+// Extract the floating-point centroid data for a key from packed character input.
+// Send OSC features as appropriate
+
+int TouchkeyDevice::processKeyCentroid(int frame,int octave, int key, timestamp_type timestamp, unsigned char * buffer, int maxLength) {
+	int touchCount = 0;
+	
+	float sliderPosition[3];
+	float sliderPositionH;
+	float sliderSize[3];
+	
+	int bytesParsed;
+	
+	if(key < 0 || key > 12 || maxLength < 1)
+		return -1;
+	
+	int white = (kKeyColor[key] == kKeyColorWhite);
+	int midiNote = octaveKeyToMidi(octave, key);
+	
+	// Check that the received data is actually valid and not left over from a previous scan (which
+	// can happen when the scan rate is too high).  0x88 is a special "warning" marker for this case
+	// since it will never be part of a valid centroid.
+	
+	if(buffer[0] == 0x88) {
+		cout << "Warning: octave " << octave << " key " << key << " data is not ready.  Check scan rate.\n";
+        if(deviceSoftwareVersion_ >= 1)
+            return white ? expectedLengthWhite_ : expectedLengthBlack_;
+        else
+            return 1;
+	}
+	
+	// A value of 0xFF means that no touch is active, and no further data will be present on this key.
+	
+	if(buffer[0] == 0xFF && deviceSoftwareVersion_ <= 0) {
+		bytesParsed = 1;
+		sliderPosition[0] = sliderPosition[1] = sliderPosition[2] = -1.0;
+		sliderSize[0] = sliderSize[1] = sliderSize[2] = 0.0;
+		sliderPositionH = -1.0;
+		
+		if(verbose_ >= 4) {
+			cout << "Octave " << octave << " Key " << key << " (TS " << timestamp << "): ff\n";
+		}			
+	}
+	else {		
+		bytesParsed = white ? expectedLengthWhite_ : expectedLengthBlack_;
+		
+		if(bytesParsed > maxLength)	// Make sure there's enough buffer left to process this key
+			return -1;
+		
+		int rawSliderPosition[3];
+		int rawSliderPositionH;		
+		
+		rawSliderPosition[0] = (((buffer[0] & 0xF0) << 4) + buffer[1]);
+		rawSliderPosition[1] = (((buffer[0] & 0x0F) << 8) + buffer[2]);
+		rawSliderPosition[2] = (((buffer[3] & 0xF0) << 4) + buffer[4]);
+		
+        if(deviceHardwareVersion_ >= 2)
+        {
+            // Always an H value with version 2 sensor hardware
+            rawSliderPositionH = (((buffer[3] & 0x0F) << 8) + buffer[5]);
+            
+            if(white) {
+                for(int i = 0; i < 3; i++) {
+                    if(rawSliderPosition[i] != 0x0FFF) {	// 0x0FFF means no touch
+                        sliderPosition[i] = (float)rawSliderPosition[i] / whiteMaxY_;
+                        sliderSize[i] = (float)buffer[i + 6] / kSizeMaxValue;
+                        touchCount++;
+                    }
+                    else {
+                        sliderPosition[i] = -1.0;
+                        sliderSize[i] = 0.0;
+                    }
+                }
+            }
+            else {
+                for(int i = 0; i < 3; i++) {
+                    if(rawSliderPosition[i] != 0x0FFF) {	// 0x0FFF means no touch
+                        sliderPosition[i] = (float)rawSliderPosition[i] / blackMaxY_;
+                        sliderSize[i] = (float)buffer[i + 6] / kSizeMaxValue;
+                        touchCount++;
+                    }
+                    else {
+                        sliderPosition[i] = -1.0;
+                        sliderSize[i] = 0.0;
+                    }
+                }
+            }
+        }
+        else
+        {
+            // H value only on white keys with version 0-1 sensor hardware
+            
+            if(white) {
+                rawSliderPositionH = (((buffer[3] & 0x0F) << 8) + buffer[5]);
+                
+                for(int i = 0; i < 3; i++) {
+                    if(rawSliderPosition[i] != 0x0FFF) {	// 0x0FFF means no touch
+                        sliderPosition[i] = (float)rawSliderPosition[i] / whiteMaxY_;
+                        sliderSize[i] = (float)buffer[i + 6] / kSizeMaxValue;
+                        touchCount++;
+                    }
+                    else {
+                        sliderPosition[i] = -1.0;
+                        sliderSize[i] = 0.0;
+                    }
+                }
+            }
+            else {
+                rawSliderPositionH = 0x0FFF;
+                
+                for(int i = 0; i < 3; i++) {
+                    if(rawSliderPosition[i] != 0x0FFF) {	// 0x0FFF means no touch
+                        sliderPosition[i] = (float)rawSliderPosition[i] / blackMaxY_;
+                        sliderSize[i] = (float)buffer[i + 5] / kSizeMaxValue;
+                        touchCount++;
+                    }
+                    else {
+                        sliderPosition[i] = -1.0;
+                        sliderSize[i] = 0.0;
+                    }
+                }		
+            }
+        }
+		
+		if(rawSliderPositionH != 0x0FFF) {
+			sliderPositionH = (float)rawSliderPositionH / whiteMaxX_ ;
+		}
+		else
+			sliderPositionH = -1.0;
+		
+		if(verbose_ >= 4) {
+			cout << "Octave " << octave << " Key " << key << ": ";
+			hexDump(cout, buffer, white ? expectedLengthWhite_ : expectedLengthBlack_);
+			cout << endl;
+		}	
+	}
+	
+	// Sanity check: do we have the PianoKey structure available to receive this data?
+	// If not, no need to proceed further.
+	if(keyboard_.key(midiNote) == 0) {
+		cout << "Warning: No PianoKey available for touchkey MIDI note " << midiNote << endl;
+		return bytesParsed;
+	}
+    
+    // From here on out, grab the performance data mutex so no MIDI events can show up in the middle
+    ScopedLock ksl(keyboard_.performanceDataMutex_);
+
+	// Turn off touch activity on this key if there's no active touches
+	if(touchCount == 0) {
+		if(keyboard_.key(midiNote)->touchIsActive())
+        {
+			keyboard_.key(midiNote)->touchOff(timestamp);
+            KeyTouchFrame newFrame(0, sliderPosition, sliderSize, sliderPositionH, white);
+            
+            if (loggingActive_)
+            {
+                ////////////////////////////////////////////////////////
+                ////////////////////////////////////////////////////////
+                //////////////////// BEGIN LOGGING /////////////////////
+                
+                keyTouchLog_.write((char*)&timestamp, sizeof(timestamp_type));
+                keyTouchLog_.write((char*)&frame, sizeof(int));
+                keyTouchLog_.write((char*)&midiNote, sizeof(int));
+                keyTouchLog_.write((char*)&newFrame, sizeof(KeyTouchFrame));
+                
+                ///////////////////// END LOGGING //////////////////////
+                ////////////////////////////////////////////////////////
+                ////////////////////////////////////////////////////////
+            }
+            
+            // Send raw OSC message if enabled
+            if(sendRawOscMessages_) {
+                keyboard_.sendMessage("/touchkeys/raw-off", "iii",
+                                      octave, key, frame,
+                                      LO_ARGS_END );
+            }
+            
+        }
+        
+        return bytesParsed;
+	}
+	
+	// At this point, construct a new frame with this data and pass it to the PianoKey for
+	// further processing.  Leave the ID fields empty; these are state-dependent and will
+	// be worked out based on the previous frames.
+	
+	KeyTouchFrame newFrame(touchCount, sliderPosition, sliderSize, sliderPositionH, white);
+	
+	keyboard_.key(midiNote)->touchInsertFrame(newFrame, timestamp);
+    
+    
+    if (loggingActive_)
+    {
+        ////////////////////////////////////////////////////////
+        ////////////////////////////////////////////////////////
+        //////////////////// BEGIN LOGGING /////////////////////
+        
+        keyTouchLog_.write((char*)&timestamp, sizeof(timestamp_type));
+        keyTouchLog_.write((char*)&frame, sizeof(int));
+        keyTouchLog_.write((char*)&midiNote, sizeof(int));
+        keyTouchLog_.write((char*)&newFrame, sizeof(KeyTouchFrame));
+        
+        ///////////////////// END LOGGING //////////////////////
+        ////////////////////////////////////////////////////////
+        ////////////////////////////////////////////////////////
+    }
+	
+	// Send raw OSC message if enabled
+	if(sendRawOscMessages_) {
+		keyboard_.sendMessage("/touchkeys/raw", "iiifffffff",
+									 octave, key, frame,
+									 sliderPosition[0],
+									 sliderSize[0],
+									 sliderPosition[1],
+									 sliderSize[1],									 
+									 sliderPosition[2],
+									 sliderSize[2],
+									 sliderPositionH,
+									 LO_ARGS_END );
+	}
+	
+	// Verbose logging of key info
+	if(verbose_ >= 3) {
+		cout << "Octave " << octave << " Key " << key << " (TS " << timestamp << "): ";
+		cout << sliderPositionH << " ";
+		cout << sliderPosition[0] << " " << sliderPosition[1] << " " << sliderPosition[2] << " ";
+		cout << sliderSize[0] << " " << sliderSize[1] << " " << sliderSize[2] << endl;
+	}	
+	
+	return bytesParsed;
+}
+
+// Process a frame of data containing analog values (i.e. key angle, Z-axis). These
+// always come as a group for a whole board, and should be parsed apart into individual keys
+void TouchkeyDevice::processAnalogFrame(unsigned char * const buffer, const int bufferLength) {
+    // Format: [Octave] [TS0] [TS1] [TS2] [TS3] [Key0L] [Key0H] [Key1L] [Key1H] ... [Key24L] [Key24H]
+    //                   ... (more frames)
+    //                  [TS0] [TS1] [TS2] [TS3] [Key0L] [Key0H] [Key1L] [Key1H] ... [Key24L] [Key24H]
+    
+    if(bufferLength < 1) {
+		if(verbose_ >= 1)
+			cout << "Warning: ignoring malformed analog frame of " << bufferLength << " bytes, less than minimum 1\n";
+        return;
+    }
+    
+    int octave = buffer[0];
+    int board = octave / 2;
+    int frame;
+    int bufferIndex = 1;
+    int midiNote, value;
+    
+    // Parse the buffer one frame at a time
+    while(bufferIndex < bufferLength) {
+        if(bufferLength - bufferIndex < 54) {
+            // This condition indicates a malformed analog frame (not enough data)
+            if(verbose_ >= 1)
+                cout << "Warning: ignoring extra analog data of " << bufferLength - bufferIndex << " bytes, less than full frame 54 (total " << bufferLength << ")\n";
+            break;
+        }
+        
+        // Find the timestamp (i.e. frame ID generated by the device). 32-bit little-endian.
+        frame = buffer[bufferIndex] + ((int)buffer[bufferIndex+1] << 8) +
+                ((int)buffer[bufferIndex+2] << 16) + ((int)buffer[bufferIndex+3] << 24);
+        
+        // Check the timestamp against the last frame from this board to see if any frames have been dropped
+        if(frame > analogLastFrame_[board] + 1) {
+            cout << "WARNING: dropped frame(s) on board " << board << " at " << frame << " (last was " << analogLastFrame_[board] << ")" << endl;
+        }
+        else if(frame < analogLastFrame_[board] + 1) {
+            cout << "WARNING: repeat frame(s) on board " << board << " at " << frame << " (last was " << analogLastFrame_[board] << ")" << endl;            
+        }
+        analogLastFrame_[board] = frame;
+        
+        // TESTING
+        /*if(verbose_ >= 3 || (frame % 500 == 0))
+            cout << "Analog frame octave " << octave << " timestamp " << frame << endl;
+        if(verbose_ >= 4 || (frame % 500 == 0)) {
+            cout << "Values: ";
+            for(int i = 0; i < 25; i++) {
+                cout << std::setw(5) << (((signed char)buffer[i*2 + 6])*256 + buffer[i*2 + 5]) << " ";
+            }
+            cout << endl;
+        }*/
+        
+        // Process key values individually and add them to the keyboard data structure
+        for(int key = 0; key < 25; key++) {
+            // Every analog frame contains 25 values, however only the top board actually uses all 25
+            // sensors. There are several "high C" values in the lower boards (i.e. key == 24) which
+            // do not correspond to real sensors. These should be ignored.
+            if(key == 24 && octave != numberOfOctaves() - 2)
+                continue;
+            
+            midiNote = octaveKeyToMidi(octave, key);
+            
+            // Check that this note is in range to the available calibrators and keys.
+            if(keyboard_.key(midiNote) == 0 || (octave*12 + key) >= keyCalibratorsLength_ || midiNote < 21)
+                continue;
+            
+            // Pull the value out from the packed buffer (little endian 16 bit)
+            value = (((signed char)buffer[key*2 + 6])*256 + buffer[key*2 + 5]);
+            
+            // Calibrate the value, assuming the calibrator is ready and running
+            key_position calibratedPosition = keyCalibrators_[octave*12 + key]->evaluate(value);
+            if(!missing_value<key_position>::isMissing(calibratedPosition)) {
+                timestamp_type timestamp = timestampSynchronizer_.synchronizedTimestamp(frame);
+                keyboard_.key(midiNote)->insertSample(calibratedPosition, timestamp);
+            }
+            else if(keyboard_.gui() != 0){
+                
+                //keyboard_.key(midiNote)->insertSample((float)value / 4096.0, timestampSynchronizer_.synchronizedTimestamp(frame));
+                
+                // Update the GUI but don't actually save the value since it's uncalibrated
+                keyboard_.gui()->setAnalogValueForKey(midiNote, (float)value / kTouchkeyAnalogValueMax);
+                
+                if(keyCalibrators_[octave*12 + key]->calibrationStatus() == kPianoKeyCalibrated)
+                    cout << "key " << midiNote << " calibrated but missing (raw value " << value << ")\n";
+            }
+        }
+        
+        if(loggingActive_) {
+            analogLog_.write((char*)&buffer[0], 1); // Octave number
+            analogLog_.write((char*)&buffer[bufferIndex], 54);
+        }
+        
+        // Skip to next frame
+        bufferIndex += 54;
+    }
+}
+
+// Process a frame containing a human-readable (and machine-coded) error message generated
+// internally by the device
+void TouchkeyDevice::processErrorMessageFrame(unsigned char * const buffer, const int bufferLength) {
+    char msg[256];
+    int len = bufferLength - 5;
+    
+    // Error on error message frame!
+    if(bufferLength < 5) {
+        cout << "Warning: received error message frame of " << bufferLength << " bytes, less than minimum 5\n";
+        return;
+    }
+    
+    // Limit length of string for safety reasons
+    if(len > 256)
+        len = 256;
+    memcpy(msg, &buffer[5], len * sizeof(char));
+    msg[len - 1] = '\0';
+    
+    // Print the error
+    cout << "Error frame received: " << msg << endl;
+    
+    // Dump the buffer containing error coding information
+    if(verbose_ >= 2) {
+        cout << "Contents: ";
+        hexDump(cout, buffer, 5);
+        cout << endl;
+    }
+}
+
+// Process a frame containing a response to an I2C command. We can use this to gather
+// raw information from the key.
+void TouchkeyDevice::processI2CResponseFrame(unsigned char * const buffer, const int bufferLength) {
+    // Format: [octave] [key] [length] <data>
+    
+    if(bufferLength < 3) {
+        cout << "Warning: received I2C response frame of " << bufferLength << " bytes, less than minimum 3\n";
+        return;
+    }
+    
+    int octave = buffer[0];
+    int key = buffer[1];
+    int responseLength = buffer[2];
+    
+    if(bufferLength < responseLength + 3) {
+        cout << "Warning: received malformed I2C response (octave " << octave << ", key " << key << ", length " << responseLength;
+        cout << ") but only " << bufferLength - 3 << " bytes of data\n";
+        
+        responseLength = bufferLength - 3;
+    }
+    else {
+        if(verbose_ >= 3) {
+            cout << "I2C response from octave " << octave << ", key " << key << ", length " << responseLength << endl;
+        }
+    }
+    
+    if(sensorDisplay_ != 0) {
+        // Copy response data to display
+        vector<int> data;
+        
+        for(int i = 3; i < responseLength + 3; i++) {
+            data.push_back(buffer[i]);
+        }
+        sensorDisplay_->setDisplayData(data);
+    }
+}
+
+// Parse raw data from a status request.  Buffer should start immediately after the
+// frame type byte, and processing will finish either at the end of the expected buffer,
+// or at the given length, whichever comes first.  Returns true if a status buffer was
+// successfully received.
+
+bool TouchkeyDevice::processStatusFrame(unsigned char * buffer, int maxLength, TouchkeyDevice::ControllerStatus *status) {
+	if((status == 0 || maxLength < 5) && verbose_ >= 1) {
+		cout << "Invalid status frame: ";
+		hexDump(cout, buffer, maxLength);
+		cout << endl;
+		return false;
+	}
+	
+	status->hardwareVersion = buffer[0];
+	status->softwareVersionMajor = buffer[1];
+	status->softwareVersionMinor = buffer[2];
+	status->running = ((buffer[3] & kStatusFlagRunning) != 0);
+	status->octaves = buffer[4];
+	status->connectedKeys = (unsigned int *)malloc(2*status->octaves*sizeof(unsigned int));
+	
+	int i, oct = 0;				// Get connected key information
+    if(status->softwareVersionMajor >= 2) {
+        // One extra byte holds lowest physical sensor
+        status->lowestHardwareNote = buffer[5];
+        status->hasTouchSensors = ((buffer[3] & kStatusFlagHasI2C) != 0);
+        status->hasAnalogSensors = ((buffer[3] & kStatusFlagHasAnalog) != 0);
+        status->hasRGBLEDs = ((buffer[3] & kStatusFlagHasRGBLED) != 0);
+        i = 6;
+    }
+    else {
+        status->lowestHardwareNote = 0;
+        status->hasTouchSensors = true;
+        status->hasAnalogSensors = true;
+        status->hasRGBLEDs = true;
+        i = 5;
+    }
+
+	while(i+1 < maxLength) {
+		status->connectedKeys[oct] = 256*buffer[i] + buffer[i+1];
+		i += 2;
+		oct++;
+	}
+	
+	if(oct < status->octaves && verbose_ >= 1) {
+		cout << "Invalid status frame: ";
+		hexDump(cout, buffer, maxLength);	
+		cout << endl;
+		return false;
+	}
+	
+	return true;
+}
+
+// Check for an ACK response from the device.  Returns true if found.  Returns
+// false if NAK received, or if a timeout occurs.
+// TODO: this implementation needs to change to not chew up other data coming in.
+
+bool TouchkeyDevice::checkForAck(int timeoutMilliseconds) {
+	//struct timeval startTime, currentTime;
+	bool controlSeq = false;
+	unsigned char ch;
+	
+    double startTime = Time::getMillisecondCounterHiRes();
+    double currentTime = startTime;
+    
+	//gettimeofday(&startTime, 0);
+	//gettimeofday(&currentTime, 0);
+	
+	while(currentTime - startTime < (double)timeoutMilliseconds) {
+		long count = read(device_, (char *)&ch, 1);
+		
+		if(count < 0) {				// Check if an error occurred on read
+			if(errno != EAGAIN) {
+				cout << "Unable to read from device while waiting for ACK (error " << errno << ").  Aborting.\n";
+				return false;
+			}
+		}
+		else if(count > 0) {		// Data received
+			// Wait for a sequence {ESCAPE_CHARACTER, ACK} or {ESCAPE_CHARACTER, NAK}
+			if(controlSeq) {
+				controlSeq = false;
+				if(ch == kControlCharacterAck) {
+					if(verbose_ >= 2)
+						cout << "Received ACK\n";
+					return true;
+				}
+				else if(ch == kControlCharacterNak) {
+					if(verbose_ >= 1)
+						cout << "Warning: received NAK\n";
+					return false;
+				}
+			}
+			else if(ch == ESCAPE_CHARACTER)
+				controlSeq = true;
+		}
+		
+		currentTime = Time::getMillisecondCounterHiRes();
+	}
+	
+	cout << "Error: timeout waiting for ACK\n";
+	return false;
+}
+
+// Convenience method to dump hexadecimal output
+void TouchkeyDevice::hexDump(ostream& str, unsigned char * buffer, int length) {
+	if(length <= 0)
+		return;
+	str << std::hex << (int)buffer[0];
+	for(int i = 1; i < length; i++) {
+		str << " " << (int)buffer[i];
+	}
+	str << std::dec;
+}
+
+TouchkeyDevice::~TouchkeyDevice() {
+    if (logFileCreated_)
+    {
+        keyTouchLog_.close();
+        analogLog_.close();
+    }
+    
+	closeDevice();
+    calibrationDeinit();
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/TouchKeys/TouchkeyDevice.h	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,407 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+ 
+  TouchkeyDevice.h: handles communication with the TouchKeys hardware
+*/
+
+#ifndef TOUCHKEY_DEVICE_H
+#define TOUCHKEY_DEVICE_H
+
+#include <iostream>
+#include <fstream>
+#include <cstdio>
+#include <cmath>
+#include <map>
+#include <set>
+#include <deque>
+#include <errno.h>
+#include <fcntl.h>
+#include <termios.h>
+#include <limits>
+#include <list>
+#include <boost/bind.hpp>
+#include <boost/function.hpp>
+#include "../JuceLibraryCode/JuceHeader.h"
+#include "PianoKeyboard.h"
+#include "Osc.h"
+#include "../Utility/TimestampSynchronizer.h"
+#include "PianoKeyCalibrator.h"
+#include "../Display/RawSensorDisplay.h"
+
+using namespace std;
+
+#define TOUCHKEY_MAX_FRAME_LENGTH 256	// Maximum data length in a single frame
+#define ESCAPE_CHARACTER 0xFE			// Indicates control sequence
+
+//#define TRANSMISSION_LENGTH_WHITE 9
+//#define TRANSMISSION_LENGTH_BLACK 8
+//#define TRANSMISSION_LENGTH_TOTAL (8*TRANSMISSION_LENGTH_WHITE + 5*TRANSMISSION_LENGTH_BLACK)
+
+const int kTransmissionLengthWhiteOldHardware = 9;
+const int kTransmissionLengthBlackOldHardware = 8;
+const int kTransmissionLengthWhiteNewHardware = 9;
+const int kTransmissionLengthBlackNewHardware = 9;
+const int kTransmissionLengthTotalOldHardware = (8 * kTransmissionLengthWhiteOldHardware + 5 * kTransmissionLengthBlackOldHardware);
+const int kTransmissionLengthTotalNewHardware = (8 * kTransmissionLengthWhiteNewHardware + 5 * kTransmissionLengthBlackNewHardware);
+
+// Maximum integer values for different types of sliders
+
+//#define WHITE_MAX_VALUE 1280.0		// White keys, vertical	(64 * 20)
+//#define WHITE_MAX_H_VALUE 255.0		// Whtie keys, horizontal
+//#define BLACK_MAX_VALUE 1024.0		// Black keys, vertical (64 * 16)
+//#define SIZE_MAX_VALUE 255.0		// Max touch size for either key type
+
+const float kWhiteMaxYValueOldHardware = 1280.0;    // White keys, vertical	(64 * 20)
+const float kWhiteMaxXValueOldHardware = 255.0;     // White keys, horizontal (1 byte)
+const float kBlackMaxYValueOldHardware = 1024.0;    // Black keys, vertical (64 * 16)
+const float kWhiteMaxYValueNewHardware = 2432.0;    // White keys, vertical (128 * 19)
+const float kWhiteMaxXValueNewHardware = 256.0;     // White keys, horizontal (1 byte + 1 bit)
+const float kBlackMaxYValueNewHardware = 1536.0;    // Black keys, vertical (128 * 12)
+const float kBlackMaxXValueNewHardware = 256.0;     // Black keys, horizontal (1 byte + 1 bit)
+
+const float kSizeMaxValue = 255.0;
+
+enum {
+	kControlCharacterFrameBegin = 0x00,
+	kControlCharacterAck = 0x01,
+	kControlCharacterNak = 0x02,
+	kControlCharacterFrameError = 0xFD,
+	kControlCharacterFrameEnd = 0xFF
+};
+
+// Frame types for data sent over USB.  The first byte following a frame start control sequence gives the type.
+
+enum {
+	kFrameTypeStatus = 0,		// Status info: connected keys, current operating modes
+	kFrameTypeCentroid = 16,	// Centroid data (default mode of operation)
+	kFrameTypeI2CResponse = 17,	// Response from a specific I2C command
+	kFrameTypeRawKeyData = 18,	// Raw data from the selected key	
+    kFrameTypeAnalog = 19,		// Analog data from Z-axis optical sensors
+	
+    kFrameTypeErrorMessage = 127, // Error message from controller
+	// These types are for incoming (computer -> us) data
+	kFrameTypeStartScanning = 128,	// Start auto-scan
+	kFrameTypeStopScanning = 129,	// Stop auto-scan
+	kFrameTypeSendI2CCommand = 130,	// Send a specific I2C command
+	kFrameTypeResetDevices = 131,	// Physically reset the system
+	kFrameTypeScanRate = 132,		// Set the scan rate (in milliseconds)	
+	kFrameTypeNoiseThreshold = 133,
+	kFrameTypeSensitivity = 134,
+	kFrameTypeSizeScaler = 135,
+	kFrameTypeMinimumSize = 136,
+	kFrameTypeSetEnabledKeys = 137,
+	kFrameTypeMonitorRawFromKey = 138,
+	kFrameTypeUpdateBaselines = 139,	// Reinitialize baseline values
+	kFrameTypeRescanKeyboard = 140,	// Rescan what keys are connected
+    kFrameTypeRGBLEDSetColors = 168, // Set RGBLEDs of given index to specific values
+	kFrameTypeRGBLEDAllOff = 169,    // All LEDs off
+	kFrameTypeEnterISPMode = 192,
+    kFrameTypeEnterSelfProgramMode = 193
+};
+
+enum {
+	kKeyColorWhite = 0,
+	kKeyColorBlack
+};
+
+enum {
+	kStatusFlagRunning = 0x01,
+	kStatusFlagRawMode = 0x02,
+	kStatusFlagHasI2C = 0x04,
+	kStatusFlagHasAnalog = 0x08,
+	kStatusFlagHasRGBLED = 0x10,
+	kStatusFlagComError = 0x80
+};
+
+
+const int kKeyColor[13] = { kKeyColorWhite, kKeyColorBlack, kKeyColorWhite,
+	kKeyColorBlack, kKeyColorWhite, kKeyColorWhite, kKeyColorBlack,
+	kKeyColorWhite, kKeyColorBlack, kKeyColorWhite, kKeyColorBlack,
+	kKeyColorWhite, kKeyColorWhite };
+
+const int kWhiteKeyIndices[13] = { 0, -1, 1, -1, 2, 3, -1, 4, -1, 5, -1, 6, 7};
+
+const unsigned char kCommandStatus[] = { ESCAPE_CHARACTER, kControlCharacterFrameBegin, kFrameTypeStatus,
+	ESCAPE_CHARACTER, kControlCharacterFrameEnd };
+const unsigned char kCommandStartScanning[] = { ESCAPE_CHARACTER, kControlCharacterFrameBegin, kFrameTypeStartScanning,
+	ESCAPE_CHARACTER, kControlCharacterFrameEnd };
+const unsigned char kCommandStopScanning[] = { ESCAPE_CHARACTER, kControlCharacterFrameBegin, kFrameTypeStopScanning,
+	ESCAPE_CHARACTER, kControlCharacterFrameEnd };
+
+#define octaveNoteToIndex(octave, note) (100*octave + note)	// Generate indices for containers
+#define indexToOctave(index) (int)(index / 100)
+#define indexToNote(index) (index % 100)
+
+const float kTouchkeyAnalogValueMax = 4095.0; // Maximum value any analog sample can take
+
+// This class implements device access to the touchkey hardware.
+
+class TouchkeyDevice /*: public OscHandler*/
+{
+    // ***** Class to implement the Juce thread *****
+private:
+    class DeviceThread : public Thread {
+    public:
+        DeviceThread(boost::function<void (DeviceThread*)> action, String name = "DeviceThread")
+        : Thread(name), actionFunction_(action) {}
+        
+        ~DeviceThread() {}
+        
+        void run() {
+            actionFunction_(this);
+        }
+        
+    private:
+        boost::function<void (DeviceThread*)> actionFunction_;
+    };
+    
+public:
+	class ControllerStatus {
+	public:
+		ControllerStatus() : connectedKeys(0) {}
+		~ControllerStatus() {
+			if(connectedKeys != 0)
+				free(connectedKeys);
+		}
+		
+		int hardwareVersion;		// Hardware version
+		int softwareVersionMajor;	// Controller firmware major version
+		int softwareVersionMinor;	// Controller firmware minor version
+		bool running;				// Is the system currently gathering centroid data?
+        bool hasTouchSensors;       // Whether the device has I2C touch sensors
+        bool hasAnalogSensors;      // Whether the device has analog optical position sensors
+        bool hasRGBLEDs;            // Whether the device has RGB LEDs for display
+		int octaves;				// Number of octaves connected [two octaves per board]
+        int lowestHardwareNote;     // Note number (0-12) of lowest connector or sensor on lowest board
+		unsigned int *connectedKeys;// Which keys are connected to each octave
+	};
+	
+	class MultiKeySweep {
+	public:
+		
+		int sweepId;
+		int sweepOctave;
+		float sweepNote;
+		int keyCount;
+		int keyOctave[2];
+		int keyNote[2];
+		int keyTouchId[2];
+		float keyPosition[2];
+	};
+    
+    // Structure to hold changes to RGB LEDs on relevant hardware
+    class RGBLEDUpdate {
+    public:
+        bool allLedsOff;        // Set to true if all LEDs should turn off on indicated board
+        int midiNote;           // MIDI note number to change
+        int red;                // RGB color
+        int green;
+        int blue;
+    };
+	
+public:
+	// ***** Constructor *****
+	TouchkeyDevice(PianoKeyboard& keyboard);
+    
+    // ***** Destructor *****
+	~TouchkeyDevice();
+    
+    // ***** Device Management *****
+	// Open a new device.  Returns true on success
+	bool openDevice(const char * inputDevicePath);
+	void closeDevice();
+	
+	// Start or stop the processing.  startAutoGathering() returns
+	// true on success.
+	bool startAutoGathering();
+	void stopAutoGathering();	
+	
+	// Status query methods
+	bool isOpen() { return device_ >= 0; }
+	bool isAutoGathering() { return autoGathering_; }
+	int numberOfOctaves() { return numOctaves_; }
+    
+	// Ping the device, to see if it is ready to respond
+	bool checkIfDevicePresent(int millisecondsToWait);
+	
+	// Start collecting raw data from a given key
+	bool startRawDataCollection(int octave, int key, int mode, int scaler);
+    
+    // ***** RGB LED updates *****
+    void rgbledSetColor(const int midiNote, const float red, const float green, const float blue);
+    void rgbledSetColorHSV(const int midiNote, const float hue, const float saturation, const float value);
+    void rgbledAllOff();
+    
+    // ***** Device Parameters *****
+    
+	// Set the scan interval in milliseconds
+	bool setScanInterval(int intervalMilliseconds);
+	
+	// Key parameters.  Setting octave or key to -1 means all octaves or all keys, respectively.
+	bool setKeySensitivity(int octave, int key, int value);
+	bool setKeyCentroidScaler(int octave, int key, int value);
+	bool setKeyMinimumCentroidSize(int octave, int key, int value);
+	bool setKeyNoiseThreshold(int octave, int key, int value);
+    
+    // Jump to device internal bootloader
+    void jumpToBootloader();
+    
+    // ***** Calibration Methods *****
+    
+	// Return whether or not the controller has been calibrated, and whether it's currently calibrating
+	bool isCalibrated() { return isCalibrated_; }
+	bool calibrationInProgress() { return calibrationInProgress_; }
+	
+	// Start: begin calibrating; finish: end and save results; abort: end and discard results
+	void calibrationStart(std::vector<int>* keysToCalibrate);
+	void calibrationFinish();
+	void calibrationAbort();
+	void calibrationClear();
+	
+	bool calibrationSaveToFile(std::string const& filename);
+	bool calibrationLoadFromFile(std::string const& filename);
+    
+    // ***** Data Logging *****
+    void createLogFiles(string keyTouchLogFilename, string analogLogFilename, string path);
+    void closeLogFile();
+    void startLogging();
+    void stopLogging();
+
+    // ***** Debugging and Utility *****
+    
+	// Set logging level
+	void setVerboseLevel(int v) { verbose_ = v; }
+	void setTransmitRawData(bool raw) { sendRawOscMessages_	= raw; }
+    bool transmitRawDataEnabled() { return sendRawOscMessages_; }
+    
+	// Conversion between touchkey # and MIDI note
+	int lowestMidiNote() { return lowestMidiNote_; }
+    int highestMidiNote() { return lowestMidiNote_ + 12*numOctaves_; }
+    int lowestKeyPresentMidiNote() { return lowestKeyPresentMidiNote_; } // What is the lowest key actually connected?
+	void setLowestMidiNote(int note);
+	int octaveKeyToMidi(int octave, int key) { return lowestMidiNote_ + octave*12 + key; }
+    
+    // Sensor data display
+    void setSensorDisplay(RawSensorDisplay *display) { sensorDisplay_ = display; }
+    
+	// ***** Run Loop Functions *****
+    void ledUpdateLoop(DeviceThread *thread);
+	void runLoop(DeviceThread *thread);
+    void rawDataRunLoop(DeviceThread *thread);
+    
+    // for debugging
+    void testStopLeds() { ledShouldStop_ = true; }
+	
+private:
+	// Read and parse new data from the device, splitting out by frame type
+	void processFrame(unsigned char * const frame, int length);
+
+	// Specific data type parsing
+	void processCentroidFrame(unsigned char * const buffer, const int bufferLength);
+	int processKeyCentroid(int frame,int octave, int key, timestamp_type timestamp, unsigned char * buffer, int maxLength);
+    void processAnalogFrame(unsigned char * const buffer, const int bufferLength);
+	void processRawDataFrame(unsigned char * const buffer, const int bufferLength);
+	bool processStatusFrame(unsigned char * buffer, int maxLength, ControllerStatus *status);
+    void processI2CResponseFrame(unsigned char * const buffer, const int bufferLength);
+    void processErrorMessageFrame(unsigned char * const buffer, const int bufferLength);
+
+	// Helper methods for centroid processing
+	//pair<float, list<int> > matchClosestPoints(float* oldPoints, float *newPoints, float count,
+	//										   int oldIndex, set<int>& availableNewPoints, float currentTotalDistance);
+	void processTwoFingerGestures(int octave, int key, KeyTouchFrame& previousPosition, KeyTouchFrame& newPosition);
+	void processThreeFingerGestures(int octave, int key, KeyTouchFrame& previousPosition, KeyTouchFrame& newPosition);
+	
+	// Utility method for parsing multi-key gestures
+	pair<int, int> whiteKeyAbove(int octave, int note);
+	
+	// After writing a command, check whether it was acknolwedged by the controller
+	bool checkForAck(int timeoutMilliseconds);
+	
+	// Utility method for debugging
+	void hexDump(ostream& str, unsigned char * buffer, int length);
+
+    // Internal calibration methods
+    void calibrationInit(int numberOfCalibrators);
+    void calibrationDeinit();
+    
+    // Set RGB LED color (for piano scanner boards)
+    bool internalRGBLEDSetColor(const int device, const int led, const int red, const int green, const int blue);
+    bool internalRGBLEDAllOff();                        // RGB LEDs off
+    int  internalRGBLEDMIDIToBoardNumber(const int midiNote);   // Get board number for MIDI note
+    int  internalRGBLEDMIDIToLEDNumber(const int midiNote);     // Get LED number for MIDI note
+	
+private:
+	PianoKeyboard& keyboard_;	// Main keyboard controller
+
+	int device_;				// File descriptor
+	DeviceThread ioThread_;		// Thread that handles the communication from the device
+    DeviceThread rawDataThread_;// Thread that handles raw data collection
+	//CriticalSection ioMutex_;	// Mutex synchronizing access between internal and external threads
+	bool autoGathering_;		// Whether auto-scanning is enabled
+	volatile bool shouldStop_;	// Communication variable between threads
+	bool sendRawOscMessages_;	// Whether we should transmit the raw frame data by OSC
+	int verbose_;				// Logging level
+	int numOctaves_;			// Number of connected octaves (determined from device)
+	int lowestMidiNote_;		// MIDI note number for the lowest C on the lowest octave
+    int lowestKeyPresentMidiNote_; // MIDI note number for the lowest key actually attached
+    int updatedLowestMidiNote_; // Lowest MIDI note if changed; held separately for thread sync
+	set<int> keysPresent_;		// Which keys (octave and note) are present on this device?
+    int deviceSoftwareVersion_; // Which version of the device we're talking to
+    int deviceHardwareVersion_; // Which version of the device hardware is running
+    int expectedLengthWhite_;   // How long the white key data blocks are
+    int expectedLengthBlack_;   // How long the black key data blocks are
+    float whiteMaxX_, whiteMaxY_;   // Maximum sensor values for white keys
+    float blackMaxX_, blackMaxY_;   // Maximum sensor values for black keys
+    
+    // Frame counter for analog data, to detect dropped frames
+    unsigned int analogLastFrame_[4];    // Max 4 boards
+	
+	// Synchronization between frame time and system timestamp, allowing interaction
+	// with other simultaneous streams using different clocks.  Also save the last timestamp
+	// we've processed to other functions can access it.
+	TimestampSynchronizer timestampSynchronizer_;	
+	timestamp_type lastTimestamp_;
+    
+    // For raw data collection, this information keeps track of which key we're reading
+    int rawDataCurrentOctave_, rawDataCurrentKey_;
+    
+    // ***** RGB LED management *****
+    bool deviceHasRGBLEDs_;                 // Whether the device has RGB LEDs
+    DeviceThread ledThread_;                   // Thread that handles LED updates (communication to the device)
+    volatile bool ledShouldStop_;           // testing
+    deque<RGBLEDUpdate> ledUpdateQueue_;    // Queue that holds new LED messages to be sent to device
+    
+    // ***** Calibration *****
+    bool isCalibrated_;
+	bool calibrationInProgress_;
+    
+    PianoKeyCalibrator** keyCalibrators_;	// Calibration information for each key
+    int keyCalibratorsLength_;              // How many calibrators
+    
+    // ***** Logging *****
+    ofstream keyTouchLog_;
+    ofstream analogLog_;
+    bool logFileCreated_;
+    bool loggingActive_;
+    
+    // ***** Sensor Data Display (for debugging) *****
+    RawSensorDisplay *sensorDisplay_;
+};
+
+#endif /* TOUCHKEY_DEVICE_H */
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/Utility/Accumulator.h	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,135 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+
+  Accumulator.h: template class that accumulates (adds) samples coming into
+  a given Node.
+*/
+
+#ifndef KEYCONTROL_ACCUMULATOR_H
+#define KEYCONTROL_ACCUMULATOR_H
+
+#include <iostream>
+#include <exception>
+#include "Node.h"
+
+/*
+ * Accumulator
+ *
+ * Calculate the running sum of the last N points of a signal.  Unlike Integral, it does not
+ * take into account the timestamps, and so is signficantly faster but less flexible.
+ *
+ * The output type of this class is a pair, the first of which indicates how many samples are
+ * included in the accumulated result, and the second of which is the result itself.  This handles
+ * transient startup conditions where all N samples are not yet available.
+ *
+ */
+
+template<typename DataType, int N>
+class Accumulator : public Node<std::pair<int, DataType> > {
+public:
+	typedef typename std::pair<int, DataType> return_type;
+	typedef typename Node<return_type>::capacity_type capacity_type;
+	//typedef typename Node<return_type>::size_type size_type;
+	
+	// ***** Constructors *****
+		
+	Accumulator(capacity_type capacity, Node<DataType>& input) : Node<return_type>(capacity), input_(input), samples_(N+1) {
+		if(capacity <= N)			// Need to have at least N points in history to accumulate
+			throw new std::bad_alloc();
+		//std::cout << "Registering Accumulator\n";
+		this->registerForTrigger(&input_);
+		//std::cout << "Accumulator: this_source = " << (TriggerSource*)this << "this_dest = " << (TriggerDestination*)this << " input_ = " << &input_ << std::endl;
+	}
+				
+	// Copy constructor
+	Accumulator(Accumulator<DataType,N> const& obj) : Node<return_type>(obj), input_(obj.input_), samples_(obj.samples_) {
+		this->registerForTrigger(&input_);
+	}
+	
+	// ***** Modifiers *****
+	//
+	// Override this method to clear the samples_ buffer
+	
+	void clear() {
+		Node<std::pair<int, DataType> >::clear();
+		samples_.clear();
+	}
+	
+	// ***** Evaluator *****
+	//
+	// This is called when the input gets a new data point.  Accumulate its value and store it in our buffer.
+	// Storing it will also cause a trigger to be sent to anyone who's listening.
+	
+	void triggerReceived(TriggerSource* who, timestamp_type timestamp) {
+		//std::cout << "Accumulator::triggerReceived\n";
+		
+		if(who != &input_)
+			return;
+		
+		//std::cout << "Accumulator::triggerReceived2\n";		
+		
+		DataType newSample = input_.latest();
+		samples_.push_back(newSample);		
+		
+		if(this->empty()) {
+			this->insert(return_type(1, newSample), timestamp);
+		}
+		else {
+			// Get the last point (both sample count and its accumulated value)
+			return_type previousAccum = this->latest();
+			
+			// Add the current sample
+			DataType accumulatedValue = newSample + previousAccum.second;
+			int numPoints = previousAccum.first;
+			
+			// If necessary, subtract off the oldest sample, which by the size of samples_
+			// is guaranteed to be its first point.
+			if(samples_.full())
+				accumulatedValue -= samples_.front();
+			else
+				numPoints++;
+			
+			this->insert(return_type(numPoints, accumulatedValue), timestamp);
+		}
+	}
+	
+	// Reset the integral to a given value at a given sample.  All samples
+	// after this one are marked "missing" to force a recalculation of the integral next time
+	// the value is requested.
+
+	/*void reset(size_type index) {
+		if(index < this->beginIndex() || index >= this->endIndex())
+			return;
+		this->rawValueAt(index) = std::pair<int, DataType>(0,DataType());
+		size_type i = index + 1;
+		while(i < this->endIndex())
+			this->rawValueAt(index) = missing_value<std::pair<int, DataType> >::missing();
+	}*/
+	
+private:
+	Node<DataType>& input_;
+	
+	// Buffer holding the individual samples.  We need to be able to drop the last sample out of the
+	// accumulated buffer, and including our own sample buffer means we don't need to rely on the
+	// length of the input to store old samples.
+	boost::circular_buffer<DataType> samples_;
+};
+
+
+#endif /* KEYCONTROL_ACCUMULATOR_H */
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/Utility/IIRFilter.cpp	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,100 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+
+  IIRFilter.cpp: template class handling an Nth-order IIR filter on data
+  in a given Node.
+*/
+
+#include "IIRFilter.h"
+#include <cmath>
+
+// These are static functions to design IIR filters specifically for floating-point datatypes.
+// vector<double> and be converted to another type at the end if needed.
+
+void designFirstOrderLowpass(std::vector<double>& bCoeffs, std::vector<double>& aCoeffs,
+                                    double cutoffFrequency, double sampleFrequency) {
+    bCoeffs.clear();
+    aCoeffs.clear();
+    
+    double omega = tan(M_PI * cutoffFrequency / sampleFrequency);
+    double n = 1.0 / (1.0 + omega);
+    
+    bCoeffs.push_back(omega * n);       // B0
+    bCoeffs.push_back(omega * n);       // B1
+    aCoeffs.push_back((omega - 1) * n); // A1
+}
+
+void designFirstOrderHighpass(std::vector<double>& bCoeffs, std::vector<double>& aCoeffs,
+                             double cutoffFrequency, double sampleFrequency) {
+    bCoeffs.clear();
+    aCoeffs.clear();
+    
+    double omega = tan(M_PI * cutoffFrequency / sampleFrequency);
+    double n = 1.0 / (1.0 + omega);
+    
+    bCoeffs.push_back(n);               // B0
+    bCoeffs.push_back(-n);              // B1
+    aCoeffs.push_back((omega - 1) * n); // A1
+}
+
+void designSecondOrderLowpass(std::vector<double>& bCoeffs, std::vector<double>& aCoeffs,
+                             double cutoffFrequency, double q, double sampleFrequency) {
+    bCoeffs.clear();
+    aCoeffs.clear();
+    
+    double omega = tan(M_PI * cutoffFrequency / sampleFrequency);
+    double n = 1.0 / (omega*omega + omega/q + 1.0);
+    double b0 = n * omega * omega;
+    
+    bCoeffs.push_back(b0);       // B0
+    bCoeffs.push_back(2.0 * b0); // B1
+    bCoeffs.push_back(b0);       // B2
+    aCoeffs.push_back(2.0 * n * (omega * omega - 1.0));   // A1
+    aCoeffs.push_back(n * (omega * omega - omega / q + 1.0));
+}
+
+void designSecondOrderHighpass(std::vector<double>& bCoeffs, std::vector<double>& aCoeffs,
+                              double cutoffFrequency, double q, double sampleFrequency) {
+    bCoeffs.clear();
+    aCoeffs.clear();
+    
+    double omega = tan(M_PI * cutoffFrequency / sampleFrequency);
+    double n = 1.0 / (omega*omega + omega/q + 1.0);
+    
+    bCoeffs.push_back(n);        // B0
+    bCoeffs.push_back(-2.0 * n); // B1
+    bCoeffs.push_back(n);        // B2
+    aCoeffs.push_back(2.0 * n * (omega * omega - 1.0));   // A1
+    aCoeffs.push_back(n * (omega * omega - omega / q + 1.0));
+}
+
+void designSecondOrderBandpass(std::vector<double>& bCoeffs, std::vector<double>& aCoeffs,
+                               double cutoffFrequency, double q, double sampleFrequency) {
+    bCoeffs.clear();
+    aCoeffs.clear();
+    
+    double omega = tan(M_PI * cutoffFrequency / sampleFrequency);
+    double n = 1.0 / (omega*omega + omega/q + 1.0);
+    double b0 = n * omega / q;
+    bCoeffs.push_back(b0);       // B0
+    bCoeffs.push_back(0.0);      // B1
+    bCoeffs.push_back(-b0);      // B2
+    aCoeffs.push_back(2.0 * n * (omega * omega - 1.0));   // A1
+    aCoeffs.push_back(n * (omega * omega - omega / q + 1.0));
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/Utility/IIRFilter.h	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,271 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+
+  IIRFilter.h: template class handling an Nth-order IIR filter on data
+  in a given Node.
+*/
+
+#ifndef touchkeys_IIRFilter_h
+#define touchkeys_IIRFilter_h
+
+
+#include <iostream>
+#include <exception>
+#include <vector>
+#include "Node.h"
+
+/*
+ * IIRFilterNode
+ *
+ * This template class performs IIR (infinite impulse response) filtering on incoming Node data.
+ * It does not take into account timestamps so assumes the data is regularly sampled. The filter
+ * coefficients can be specified and changed, and the operation is selectable between always
+ * filtering on each new sample or only filtering on request. In the latter case, it will go back
+ * and filter from the most recent available sample, assuming the signal starts from 0 if there is
+ * any break in data between what was already calculated and what input data is now available.
+ */
+
+template<typename DataType>
+class IIRFilterNode : public Node<DataType> {
+public:
+	typedef typename Node<DataType>::capacity_type capacity_type;
+	//typedef typename Node<return_type>::size_type size_type;
+	
+	// ***** Constructors *****
+    
+	IIRFilterNode(capacity_type capacity, Node<DataType>& input) : Node<DataType>(capacity), input_(input),
+      autoCalculate_(false), inputHistory_(0), outputHistory_(0), lastInputIndex_(0) {
+	}
+    
+	// Copy constructor
+	IIRFilterNode(IIRFilterNode<DataType> const& obj) : Node<DataType>(obj), input_(obj.input_), autoCalculate_(obj.autoCalculate_),
+     aCoefficients_(obj.aCoefficients_), bCoefficients_(obj.bCoefficients_), lastInputIndex_(obj.lastInputIndex_) {
+         if(obj.inputHistory_ != 0)
+             inputHistory_ = new boost::circular_buffer<DataType>(*obj.inputHistory_);
+         else
+             inputHistory_ = 0;
+         if(obj.outputHistory_ != 0)
+             outputHistory_ = new boost::circular_buffer<DataType>(*obj.outputHistory_);
+         else
+             outputHistory_ = 0;
+         if(autoCalculate_) {
+             // Bring up to date and register for further updates
+             calculate();
+             this->registerForTrigger(&input_);
+         }
+	}
+    
+    // ***** Destructor *****
+    ~IIRFilterNode() {
+        if(inputHistory_ != 0)
+            delete inputHistory_;
+        if(outputHistory_ != 0)
+            delete outputHistory_;
+    }
+	
+	// ***** Modifiers *****
+	//
+	// Override this method to clear the input sample buffer
+	
+	void clear() {
+		Node<DataType>::clear();
+        clearInputOutputHistory();
+	}
+    
+    // Switch whether calculations happen automatically or only upon request
+    // If switching on, optionally specify how far back to calculate in bringing
+    // the filter up to date.
+    void setAutoCalculate(bool newAutoCalculate, int maximumLookback = -1) {
+        if(autoCalculate_ && !newAutoCalculate)
+            this->unregisterForTrigger(&input_);
+        else if(!autoCalculate_ && newAutoCalculate) {
+            // Bring the buffer up to date
+            calculate(maximumLookback);
+            this->registerForTrigger(&input_);
+        }
+        autoCalculate_ = newAutoCalculate;
+    }
+    
+    // Set the coefficients of the filter and allocate the proper buffer
+    // to hold past inputs. Optional last argument specifies whether to
+    // clear the past sample history or not (defaults to clearing it).
+    // If filter lengths are different, the buffer is always cleared.
+    void setCoefficients(std::vector<DataType> const& bCoeffs,
+                         std::vector<DataType> const& aCoeffs,
+                         bool clearBuffer = true) {
+        if(bCoeffs.empty()) // Can't have an empty feedforward coefficient set
+            return;
+        bool shouldClear = clearBuffer;
+        aCoefficients_ = aCoeffs;
+        bCoefficients_ = bCoeffs;
+        
+        if(inputHistory_ == 0) {
+            inputHistory_ = new boost::circular_buffer<DataType>(bCoeffs.size());
+            shouldClear = true;
+        }
+        else if(bCoeffs.size() != inputHistory_->capacity()) {
+            inputHistory_->set_capacity(bCoeffs.size());
+            shouldClear = true;
+        }
+        
+        if(outputHistory_ == 0) {
+            outputHistory_ = new boost::circular_buffer<DataType>(aCoeffs.size());
+            shouldClear = true;
+        }
+        else if(aCoeffs.size() != outputHistory_->capacity()) {
+            outputHistory_->set_capacity(aCoeffs.size());
+            shouldClear = true;
+        }
+        
+        if(shouldClear)
+            clearInputOutputHistory();
+    }
+    
+    // If not automatically calculating, bring the samples up to date by
+    // processing any available input that we have not yet seen. Returns
+    // the most recent output. Optional argument specifies how far back
+    // to look before starting fresh (if more samples have elapsed since
+    // last calculation).
+    typename Node<DataType>::return_value_type calculate(int maximumLookback = -1) {
+        typename Node<DataType>::size_type index = lastInputIndex_;
+        
+        if(maximumLookback >= 0 && index < input_.endIndex() - 1 - maximumLookback) {
+            //std::cout << "IIRFilterNode: clearing history at index " << index << std::endl;
+            // More samples gone by than we want to calculate... clear input
+            clearInputOutputHistory();
+            index = input_.endIndex() - 1 - maximumLookback;
+            if(index < input_.beginIndex())
+                index = input_.beginIndex();
+        }
+        else if(index < input_.beginIndex()) {
+            // More samples gone by than are now available... clear input
+            //std::cout << "IIRFilterNode: clearing history at index " << index << std::endl;
+            clearInputOutputHistory();
+            index = input_.beginIndex();
+        }
+        while(index < input_.endIndex()) {
+            processOneSample(input_[index], input_.timestampAt(index));
+            index++;
+        }
+        
+        lastInputIndex_ = index;
+        if(!this->empty())
+            return this->latest();
+        //std::cout << "IIRFilterNode: empty\n";
+        return missing_value<DataType>::missing();
+    }
+
+	// ***** Evaluator *****
+	//
+	// This is called when the input gets a new data point.  Accumulate its value and store it in our buffer.
+	// Storing it will also cause a trigger to be sent to anyone who's listening.
+	
+	void triggerReceived(TriggerSource* who, timestamp_type timestamp) {
+		if(who != &input_ || !autoCalculate_)
+			return;
+        
+        processOneSample(input_.latest(), timestamp);
+	}
+	
+private:
+    // ***** Internal Methods *****
+    // Run the filter once with a new sample. Put the result into the
+    // end of the buffer.
+    void processOneSample(DataType const& sample, timestamp_type timestamp) {
+        if(!bCoefficients_.empty()) {
+            // Always need at least one feedforward coefficient
+            DataType result = bCoefficients_[0] * sample;
+            typename boost::circular_buffer<DataType>::reverse_iterator rit = inputHistory_->rbegin();
+            
+            // Feedforward part
+            for(int i = 1; i < bCoefficients_.size() && rit != inputHistory_->rend(); i++) {
+                result += *rit * bCoefficients_[i];
+                rit++;
+            }
+            // Feedback part
+            rit = outputHistory_->rbegin();
+            for(int i = 0; i < aCoefficients_.size() && rit != outputHistory_->rend(); i++) {
+                result -= *rit * aCoefficients_[i];
+                rit++;
+            }
+            
+            // Update input history and put output in our buffer
+            inputHistory_->push_back(sample);
+            outputHistory_->push_back(result);
+            this->insert(result, timestamp);
+        }
+        else {
+            // Pass through when no coefficients present
+            this->insert(sample, timestamp);
+        }
+    }
+    
+    // Clear the recent history of input/output data and fill it with zeros
+    void clearInputOutputHistory() {
+        if(inputHistory_ != 0) {
+            inputHistory_->clear();
+            while(!inputHistory_->full())
+                inputHistory_->push_back(DataType());
+        }
+        if(outputHistory_ != 0) {
+            outputHistory_->clear();
+            while(!outputHistory_->full())
+                outputHistory_->push_back(DataType());
+        }
+    }
+    
+    
+    // ***** Member Variables *****
+    
+	Node<DataType>& input_;
+	bool autoCalculate_;        // Whether we're automatically calculating new output values
+
+    // Variables below are for filter calculation. We need to hold the past input samples
+    // ourselves because we can't consistently count on enough samples in the source buffer.
+    // Likewise, we need to hold past output samples, even though we have our own buffer, because
+    // when we clear the buffer for new calculations we don't want to lose what we've previously
+    // calculated.
+    boost::circular_buffer<DataType>* inputHistory_;
+    boost::circular_buffer<DataType>* outputHistory_;
+    std::vector<DataType> aCoefficients_, bCoefficients_;
+    typename Node<DataType>::size_type lastInputIndex_;              // Where in the input buffer we had the last sample
+};
+
+// ***** Static Filter Design Methods *****
+
+// These methods calculate specific coefficients and store them in the provided
+// A and B vectors. These will only work for double/float datatypes or others that
+// can be multiplied with them. Details: http://freeverb3.sourceforge.net/iir_filter.shtml
+
+void designFirstOrderLowpass(std::vector<double>& bCoeffs, std::vector<double>& aCoeffs,
+                                    double cutoffFrequency, double sampleFrequency);
+
+void designFirstOrderHighpass(std::vector<double>& bCoeffs, std::vector<double>& aCoeffs,
+                              double cutoffFrequency, double sampleFrequency);
+
+void designSecondOrderLowpass(std::vector<double>& bCoeffs, std::vector<double>& aCoeffs,
+                              double cutoffFrequency, double q, double sampleFrequency);
+
+void designSecondOrderHighpass(std::vector<double>& bCoeffs, std::vector<double>& aCoeffs,
+                               double cutoffFrequency, double q, double sampleFrequency);
+
+void designSecondOrderBandpass(std::vector<double>& bCoeffs, std::vector<double>& aCoeffs,
+                               double cutoffFrequency, double q, double sampleFrequency);
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/Utility/LineSegment.h	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,185 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+
+  LineSegment.h: template class implementing a line segment over time.
+*/
+
+#ifndef touchkeys_LineSegment_h
+#define touchkeys_LineSegment_h
+
+#include <iostream>
+#include <list>
+#include <boost/function.hpp>
+#include "Types.h"
+
+/*
+ * LineSegment
+ *
+ * This template class implements a line (or curve) segment. Given start and end times and
+ * start and end values, this class calculates the current value based on the given timestamp.
+ * Options are available to freeze at the final value or to extrapolate from it.
+ * 
+ * One assumption that is made with this class is that calls are always made in monotonically
+ * non-decreasing order of timestamp. Evaluating the value of the segment may implicitly update 
+ * its state so out-of-order evaluations could return unexpected results.
+ */
+
+
+template<typename DataType>
+class LineSegment {
+public:
+    typedef boost::function<DataType const& (DataType const&)> warp_function;
+    
+    struct {
+        timestamp_type time;        // Time to achieve a given value
+        DataType       value;       // Value that should be achieved
+        warp_function  warp;        // How to warp the data getting there. FIXME: null?
+        bool           jump;        // Whether to do an immediate jump
+    } Endpoint;
+    
+public:
+	// ***** Constructors *****
+
+    // Default constructor with default starting value
+    LineSegment() : lastEvaluatedTimestamp_(0), lastEvaluatedValue_() {
+        
+    }
+    
+    // Constructor with arbitrary starting value
+    LineSegment(DataType const& startingValue) : lastEvaluatedTimestamp_(0),
+      lastEvaluatedValue_(startingValue) {
+        
+    }
+    
+	// Copy constructor
+    LineSegment(LineSegment<DataType> const& obj) : 
+      lastEvaluatedTimestamp_(obj.lastEvaluatedTimestamp_),
+      lastEvaluatedValue_(obj.lastEvaluatedValue_) {
+        
+    }
+        
+    // ***** Destructor *****
+    ~LineSegment() {
+
+    }
+	
+	// ***** Modifiers *****
+    // Add a new segment from a given time to a given time. The "from" value is implicitly
+    // calculated based on the current state of the segment.
+    void addSegment(timestamp_type const fromTimestamp, timestamp_type const toTimestamp, DataType const& toValue) {
+        
+    }
+    
+    // As above, but use a non-linear (warped) transfer function to get from one end to the other.
+    // The function should take in one value between 0 and 1 (or their DataType equivalents) and return
+    // a value between 0 and 1 (or their DataType equivalents). Scaling will be done internally.
+    void addSegment(timestamp_type const fromTimestamp, timestamp_type const toTimestamp, DataType const& toValue,
+                    warp_function& warp) {
+        
+    }
+    
+    // Freeze the value starting at the indicated timestamp (immediately if no argument is given)
+    void hold(timestamp_type const timestamp = 0) {
+        if(timestamp == 0) {
+            segments_.clear();          // Hold at whatever last evaluation returned
+        }
+        else {
+            evaluate(timestamp);        // Recalculate value for this time, then hold
+            segments_.clear();
+        }
+    }
+    
+    // Jump to a given value at the indicated timestamp (immediately if no argument is given)
+    void jump(timestamp_type const timestamp = 0, DataType const& toValue) {
+        if(timestamp == 0) {
+            lastEvaluatedValue_ = toValue;
+            segments_.clear();
+        }
+        else {
+            // Jump to a value at a given time
+            Endpoint newEndpoint;
+            
+            newEndpoint.time = timestamp;
+            newEndpoint.value = toValue;
+            newEndpoint.jump = true;
+            
+            if(segments_.empty())
+                segments_.push_back(newEndpoint);
+            else {
+                std::list<Endpoint>::iterator it = segments_.begin();
+                
+                // Look for any elements in the list that have a later timestamp.
+                // Remove them and make this jump the last element.
+                while(it != segments_.end()) {
+                    if(it->time >= timestamp) {
+                        segments_.erase(it, segments_.end());
+                        break;
+                    }
+                    it++;
+                }
+                
+                segments_.push_back(newEndpoint);
+            }
+        }
+    }
+    
+    // Reset state to defaults, including times
+    void reset() {
+        lastEvaluatedValue_ = DataType();
+        lastEvaluatedTimestamp_ = 0;
+        segments_.clear();
+    }
+    
+	// ***** Evaluator *****
+    
+    // Return the current value of the segment based on current timestamp
+    DataType const& evaluate(timestamp_type const timestamp) {
+        // Check that the current timestamp is later than the previous one. If
+        // earlier, print a warning. If identical, return the last value identically.
+        // If there are no further changes planned, likewise return the last value.
+        if(timestamp < lastEvaluatedTimestamp_) {
+            std::cout << "Warning: LineSegment::evaluate() called with timestamp " << timestamp << " earlier than previous " << lastEvaluatedTimestamp_ << std::endl;
+            return lastEvaluatedValue_;
+        }
+        if(timestamp == lastEvaluatedValue_ || !changesRemaining_)
+            return lastEvaluatedValue_;
+        
+        // TODO: evaluate
+    }
+    
+    // Return whether all in-progress segments have finished. Call this after evaluate().
+    bool finished() {
+        return segments_.empty();
+    }
+
+private:
+    // ***** Internal Methods *****
+
+    
+    // ***** Member Variables *****
+    timestamp_type  lastEvaluatedTimestamp_;    // When evaluate() was last called
+    DataType        lastEvaluatedValue_;        // What evaluate() last returned
+    std::list<Endpoint> segments_;              // List of segment times and values
+};
+
+
+
+
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/Utility/Node.h	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,931 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+
+  Node.h: template class which wraps a circular buffer, providing a collection
+  of methods for inserting, accessing and iterating over data (including
+  interpolation). Also provides trigger functionality to notify listeners when
+  new objects are inserted. Unlike conventional containers, indices always increment
+  over time, even though only the last N objects will be held in the buffer. The
+  beginning index will therefore be greater than 0 in many cases, but the index of
+  a particular item will never change, even if the item itself eventually is dropped
+  from the buffer.
+*/
+
+#ifndef KEYCONTROL_NODE_H
+#define KEYCONTROL_NODE_H
+
+#include <iostream>
+#include <set>
+#include <boost/circular_buffer.hpp>
+#include <boost/lambda/lambda.hpp>
+#include <cmath>
+#include "../JuceLibraryCode/JuceHeader.h"
+#include "Types.h"
+#include "Trigger.h"
+
+template<typename OutputType> class NodeNonInterpolating;
+template<typename OutputType> class Node;
+
+/*
+ * NodeIterator
+ *
+ * Abstract class that implements common functionality for all types of Sources and Filters
+ * Modeled on (and contains) boost::cb_details::iterator
+ * See boost/circular_buffer/details.hpp for more information
+ *
+ */
+
+// Custom iterator type to move through the Node buffer
+template <class OutputType, class Traits, class NonConstTraits>
+struct NodeIterator :
+	public boost::iterator<
+	std::random_access_iterator_tag,
+	typename Traits::value_type,
+	typename Traits::difference_type,
+	typename Traits::pointer,
+	typename Traits::reference>
+{
+    typedef boost::iterator<
+		std::random_access_iterator_tag,
+		typename Traits::value_type,
+		typename Traits::difference_type,
+		typename Traits::pointer,
+		typename Traits::reference> base_iterator;
+	
+	typedef NodeNonInterpolating<OutputType> Buff;
+	
+	typedef NodeIterator<Buff, typename Traits::nonconst_self, NonConstTraits> nonconst_self;
+	typedef typename boost::cb_details::iterator<boost::circular_buffer<OutputType>, NonConstTraits> cb_iterator;
+	
+	typedef typename base_iterator::value_type value_type;
+	typedef typename base_iterator::pointer pointer;
+	typedef typename base_iterator::reference reference;
+	typedef typename Traits::size_type size_type;
+	typedef typename base_iterator::difference_type difference_type;
+	
+	// ***** Member Variables *****
+    
+    // Pointer to the Node object
+	Buff* m_buff;
+    
+	// Base (non-const) iterator to the circular buffer
+	cb_iterator m_cb_it;
+	
+	// ***** Constructors *****
+	
+	// Default constructor
+	NodeIterator() : m_buff(0), m_cb_it(cb_iterator()) {}
+	
+	// Copy constructor
+	NodeIterator(const nonconst_self& it) : m_cb_it(it.m_cb_it) {}
+	
+	// Constructor based on a circular_buffer iterator
+	NodeIterator(Buff* cb, const cb_iterator cb_it) : m_buff(cb), m_cb_it(cb_it) {}
+	
+	// ***** Operators *****
+	//
+	// Modeled on boost::cb_details::iterator (boost/circular_buffer/details.hpp)
+	
+    NodeIterator& operator = (const NodeIterator& it) {
+        if (this == &it)
+            return *this;
+        m_buff = it.m_buff;
+        m_cb_it = it.m_cb_it;
+        return *this;
+    }	
+	
+	// Dereferencing operator.  We change the behavior here to evaluate missing values as needed.
+	// Note that this requires m_cb_it to be a non_const type, even if we are a const iterator.
+	
+    reference operator * () const {
+		return *m_cb_it;
+		//reference val = *m_cb_it;
+		//if(!missing_value<OutputType>::isMissing(val))
+		//	return val;
+		//return (*m_cb_it = m_buff->evaluate(index()));
+    }	
+	
+	pointer operator -> () const { return &(operator*()); }
+	
+    template <class Traits0, class Traits1>
+    difference_type operator - (const NodeIterator<OutputType, Traits0, Traits1>& it) const { return m_cb_it - it.m_cb_it; }
+	
+    NodeIterator& operator ++ () {			// ++it
+		++m_cb_it;
+		return *this;
+	}
+	NodeIterator operator ++ (int) {		// it++
+		NodeIterator<OutputType, Traits, NonConstTraits> tmp = *this;
+		++m_cb_it;
+		return tmp;
+	}
+	NodeIterator& operator -- () {			// --it
+		--m_cb_it; 
+		return *this;
+	}
+	NodeIterator operator -- (int) {		// it--
+		NodeIterator<OutputType, Traits, NonConstTraits> tmp = *this;
+		m_cb_it--; 
+		return tmp;
+	}
+    NodeIterator& operator += (difference_type n) {		// it += n
+		m_cb_it += n;
+        return *this;
+    }	
+    NodeIterator& operator -= (difference_type n) {		// it -= n
+		m_cb_it -= n;
+        return *this;
+    }		
+	
+	NodeIterator operator + (difference_type n) const { return NodeIterator<OutputType, Traits, NonConstTraits>(*this) += n; }
+	NodeIterator operator - (difference_type n) const { return NodeIterator<OutputType, Traits, NonConstTraits>(*this) -= n; }
+	
+	reference operator [] (difference_type n) const { return *(*this + n); }
+	
+	// ***** Comparisons *****
+	//
+	// This iterator class implements some unusual comparison behavior: two iterators are evaluated by their offset within
+	// their respective buffers, even if they point to separate buffers.  When used on synchronized buffers, this allows
+	// us to evaluate which of two iterators points to the earlier event.
+	
+    template <class OutputType0, class Traits0, class Traits1>
+    bool operator == (const NodeIterator<OutputType0, Traits0, Traits1>& it) const { 
+		return index() == it.index(); 
+	}
+	
+    template <class OutputType0, class Traits0, class Traits1>
+    bool operator != (const NodeIterator<OutputType0, Traits0, Traits1>& it) const { 
+		return index() != it.index(); 
+	}	
+	
+    template <class OutputType0, class Traits0, class Traits1>
+    bool operator < (const NodeIterator<OutputType0, Traits0, Traits1>& it) const { 
+		return index() < it.index(); 
+	}	
+	
+    template <class OutputType0, class Traits0, class Traits1>
+    bool operator > (const NodeIterator<OutputType0, Traits0, Traits1>& it) const { return it < *this; }
+	
+    template <class OutputType0, class Traits0, class Traits1>
+    bool operator <= (const NodeIterator<OutputType0, Traits0, Traits1>& it) const { return !(it < *this); }
+	
+    template <class OutputType0, class Traits0, class Traits1>
+    bool operator >= (const NodeIterator<OutputType0, Traits0, Traits1>& it) const { return !(*this < it); }	
+	
+	// ***** Special Methods *****
+	
+	// Return the offset within the buffer for this iterator's current location
+	// Can be used with at() or operator[], and can be used to compare relative locations
+	// of two iterators, even if they don't refer to the same buffer
+	
+	size_type index() const {
+		return (size_type)(*this - m_buff->begin() + m_buff->firstSampleIndex_);
+	}
+	
+	// Return the timestamp associated with the sample this iterator points to
+	
+	timestamp_type timestamp() const { return m_buff->timestampAt(index()); }
+	
+	// Tells us whether the index points to a valid place in the buffer.
+	// TODO: make sure this is right
+	// bool isValid() const { return (index() >= m_buff->beginIndex() && index() <= (m_buff->endIndex() - 1)); }	
+};
+
+/*
+ * NodeReverseIterator
+ *
+ * Adapter for reverse iterators on Node classes, preserving some of the
+ * special behavior the iterators implement.
+ */
+
+template <typename T>
+struct NodeReverseIterator : public boost::reverse_iterator<T> {
+	NodeReverseIterator() {}
+	explicit NodeReverseIterator(T baseIt) : boost::reverse_iterator<T>(baseIt) {}
+	
+	typedef typename boost::reverse_iterator<T>::base_type::size_type size_type;
+
+	size_type index() const { return boost::prior(this->base_reference()).index(); }
+	timestamp_type timestamp() const { return boost::prior(this->base_reference()).timestamp(); }
+};
+
+/*
+ * NodeInterpolatedIterator
+ *
+ * Extends the iterator concept to fractional indices, using linear interpolation to return
+ * values and timestamps.  This is always a const iterator class.
+ */
+
+template<typename OutputType, typename Traits>
+struct NodeInterpolatedIterator :
+	public boost::iterator<
+	std::random_access_iterator_tag,
+	typename Traits::value_type,
+	typename Traits::difference_type,
+	typename Traits::pointer,
+	typename Traits::reference>
+{
+	typedef NodeInterpolatedIterator<OutputType,Traits> self_type;
+	
+    typedef boost::iterator<
+		std::random_access_iterator_tag,
+		typename Traits::value_type,
+		typename Traits::difference_type,
+		typename Traits::pointer,
+		typename Traits::reference> base_iterator;
+	
+	typedef typename Traits::size_type size_type;
+	typedef typename base_iterator::value_type value_type;
+	typedef typename base_iterator::pointer pointer;
+	typedef typename base_iterator::reference reference;	
+	
+	// ***** Member Variables *****
+	
+	// Reference to the buffer this iterator indexes
+	Node<OutputType>* m_buff;
+	
+	// Index location within the buffer
+	double m_index;
+	
+	// Step size for ++ and similar operators
+	double m_step;
+	
+	// ***** Constructors *****
+	
+	// Default (empty) constructor
+	NodeInterpolatedIterator() : m_buff(0), m_index(0.0), m_step(1.0) {}
+	
+	// Constructor that should be used primarily by the Node class itself
+	NodeInterpolatedIterator(Node<OutputType>* buff, double index, double stepSize) 
+	: m_buff(buff), m_index(index), m_step(stepSize) {}
+	
+	// Copy constructor
+	NodeInterpolatedIterator(const self_type& obj) : m_buff(obj.m_buff), m_index(obj.m_index), m_step(obj.m_step) {}
+	
+	// ***** Operators *****
+	//
+	// Modeled on STL iterators, but using fractional indices.  This class should be considered a sort of random access,
+	// bidirectional iterator.  
+
+    NodeInterpolatedIterator& operator = (const self_type& it) {
+        if (this == &it)
+            return *this;
+        m_buff = it.m_buff;
+        m_index = it.m_index;
+		m_step = it.m_step;
+        return *this;
+    }	
+	
+	// Dereferencing operators.  Like the non-interpolated version of this iterator, it will calculate the values as needed.
+	// This happens within the operator[] method of Node, rather than in this class itself.
+	
+    value_type operator * () const { return m_buff->interpolate(m_index); }
+	pointer operator -> () const { return &(operator*()); }
+	
+	// Difference of two iterators (return the difference in indices)
+    double operator - (const self_type& it) const { return m_index - it.m_index; }
+	
+	// Increment and decrement are typically integer operators.  We define their behavior here to change the index by a predetermined
+	// step size, set at construction but changeable.
+	
+    self_type& operator ++ () {			// ++it
+		m_index += m_step;
+		return *this;
+	}
+	self_type operator ++ (int) {		// it++
+		self_type tmp = *this;
+		m_index += m_step;
+		return tmp;
+	}
+	self_type& operator -- () {			// --it
+		m_index -= m_step;
+		return *this;
+	}
+	self_type operator -- (int) {		// it--
+		self_type tmp = *this;
+		m_index -= m_step;
+		return tmp;
+	}
+	
+	// These methods change the iterator location by a fractional amount.  Notice that this is NOT scaled by m_step.
+    self_type& operator += (double n) {		// it += n
+		m_index += n;
+        return *this;
+    }	
+    self_type& operator -= (double n) {		// it -= n
+		m_index -= n;
+        return *this;
+    }		
+	
+	self_type operator + (double n) const { return NodeInterpolatedIterator<OutputType,Traits>(*this) += n; }
+	self_type operator - (double n) const { return NodeInterpolatedIterator<OutputType,Traits>(*this) -= n; }
+	
+	reference operator [] (double n) const { return *(*this + n); }	
+	
+	
+	// ***** Comparisons *****
+	//
+	// The comparison behavior is the same as for NodeIterator: even if two iterators point to different buffers,
+	// they can be compared on the basis of the indices.  Of course, this is only meaningful if the two buffers are synchronized
+	// in time.
+	
+	template<class OutputType0, class Traits0>
+    bool operator == (const NodeInterpolatedIterator<OutputType0,Traits0>& it) const { return m_index == it.m_index; }
+	
+	template<class OutputType0, class Traits0>
+    bool operator != (const NodeInterpolatedIterator<OutputType0, Traits0>& it) const { return m_index != it.m_index; }
+
+	template<class OutputType0, class Traits0>	
+	bool operator < (const NodeInterpolatedIterator<OutputType0, Traits0>& it) const { return m_index < it.m_index; }
+	
+	template<class OutputType0, class Traits0>	
+	bool operator > (const NodeInterpolatedIterator<OutputType0, Traits0>& it) const { return m_index > it.m_index; }
+	
+	template<class OutputType0, class Traits0>	
+	bool operator <= (const NodeInterpolatedIterator<OutputType0, Traits0>& it) const { return !(it < *this); }
+	
+	template<class OutputType0, class Traits0>	
+	bool operator >= (const NodeInterpolatedIterator<OutputType0, Traits0>& it) const { return !(*this < it); }
+	
+	// We can also compare interpolated and non-interpolated iterators.
+	
+    template <class OutputType0, class Traits0, class Traits1>
+    bool operator == (const NodeIterator<OutputType0, Traits0, Traits1>& it) const { return m_index == (double)it.index(); }	
+	
+    template <class OutputType0, class Traits0, class Traits1>
+    bool operator != (const NodeIterator<OutputType0, Traits0, Traits1>& it) const { return m_index != (double)it.index(); }	
+	
+    template <class OutputType0, class Traits0, class Traits1>
+    bool operator < (const NodeIterator<OutputType0, Traits0, Traits1>& it) const { return m_index < (double)it.index(); }	
+	
+    template <class OutputType0, class Traits0, class Traits1>
+    bool operator > (const NodeIterator<OutputType0, Traits0, Traits1>& it) const { return m_index > (double)it.index(); }	
+	
+	template <class OutputType0, class Traits0, class Traits1>
+    bool operator <= (const NodeIterator<OutputType0, Traits0, Traits1>& it) const { return m_index <= (double)it.index(); }	
+	
+    template <class OutputType0, class Traits0, class Traits1>
+    bool operator >= (const NodeIterator<OutputType0, Traits0, Traits1>& it) const { return m_index >= (double)it.index(); }	
+	
+	// ***** Special Methods *****
+	
+	// Round a fractional index up, down, or to the nearest integer
+	
+    self_type& roundDown() {
+		m_index = floor(m_index);	
+		return *this;
+	}	
+    self_type& roundUp() {
+		m_index = ceil(m_index);	
+		return *this;
+	}	
+    self_type& round() {
+		m_index = floor(m_index + 0.5);	
+		return *this;
+	}	
+	
+	// Increment the iterator by a difference in time rather than a difference in index.  This is less efficient to
+	// compute, but can be useful for buffers whose samples are not regularly spaced in time.
+	
+	self_type& incrementTime(timestamp_diff_type ts) {
+		if(ts > 0) {
+			size_type afterIndex = (size_type)ceil(m_index);
+			if(afterIndex >= m_buff->endIndex()) {
+				m_index = (double)m_buff->endIndex();
+				return *this;
+			}
+			timestamp_type target = timestamp() + ts;
+			timestamp_type after = m_buff->timestampAt(afterIndex);
+			
+			// First of all, find the first integer index with a timestamp greater than our new target time.
+			while(after < target) {
+				afterIndex++;
+				if(afterIndex >= m_buff->endIndex()) {
+					m_index = (double)m_buff->endIndex();
+					return *this;
+				}				
+				after = m_buff->timestampAt(afterIndex);
+			}
+			
+			// Then find the timestamp immediately before that.  We'll interpolate between these two to get
+			// the adjusted index.
+			timestamp_type before = m_buff->timestampAt(afterIndex-1);
+			m_index = ((target - before) / (after - before)) + (double)(afterIndex - 1);
+		}
+		else if(ts < 0) {
+			size_type beforeIndex = (size_type)floor(m_index);
+			if(beforeIndex < m_buff->beginIndex()) {
+				m_index = (double)m_buff->beginIndex()-1.0;
+				return *this;
+			}
+			timestamp_type target = timestamp() + ts;
+			timestamp_type before = m_buff->timestampAt(beforeIndex);
+			
+			// Find the first integer index with a timestamp less than our new target time.
+			while(before > target) {
+				beforeIndex--;
+				if(beforeIndex < m_buff->beginIndex()) {
+					m_index = (double)m_buff->beginIndex()-1.0;
+					return *this;
+				}
+				before = m_buff->timestampAt(beforeIndex);
+			}
+			
+			// Now find the timestamp immediately after that.  Interpolated to get the adjusted index.
+			timestamp_type after = m_buff->timestampAt(beforeIndex+1);
+			m_index = ((target - before)/(after - before)) + (double)beforeIndex;
+		}
+		// if(ts == 0), do nothing
+		return *this;
+	}
+	
+	
+	// Return (or change) the step size
+	double& step() { return m_step; }
+	
+	// Return the index within the buffer.  The index is an always-increasing sample number (which means that as the
+	// buffer fills, an index will continue to point to the same piece of data until that data is overwritten, at which
+	// point that index is no longer valid for accessing the buffer at all.)
+	double index() const { return m_index; }
+	
+	// Return the timestamp for this particular location in the buffer (using linear interpolation).
+	timestamp_type timestamp() const { return m_buff->interpolatedTimestampAt(m_index); }
+	
+	// Tells us whether the index points to a valid place in the buffer.
+	bool isValid() const { return (m_index >= m_buff->beginIndex() && m_index <= (m_buff->endIndex() - 1)); }
+};
+
+/*
+ * NodeBase
+ *
+ * Abstract class that implements common functionality for all Node data types.
+ *
+ */
+ 
+class NodeBase : public TriggerSource, public TriggerDestination {	
+public:
+	typedef uint32_t size_type;
+	
+	// ***** Constructors *****
+	
+	// Default constructor
+	NodeBase() {}
+	
+	// Copy constructor: can't copy the mutexes
+	NodeBase(NodeBase const& obj) {}
+	
+	NodeBase& operator= (NodeBase const& obj) {
+		//listeners_ = obj.listeners_;
+		return *this;
+	}
+	
+	// ***** Destructor *****
+	
+	virtual ~NodeBase() {
+		//clearTriggerSources();
+	}
+	
+	// ***** Modifiers *****
+	
+	virtual void clear() = 0;									
+	//virtual void insertMissing(timestamp_type timestamp) = 0;	
+	
+	// ***** Listener Methods *****
+	//
+	// We need to keep the buffers of all connected units in sync.  That means any Source or Filter needs to know what its output
+	// connects to.  That functionality is accomplished by "listeners": any time a new sample is added to the buffer, the listeners
+	// are notified with a corresponding timestamp.  The latter is to ensure that a buffer does not respond to notifications from multiple
+	// inputs for the same data point.
+	
+	/*void registerListener(NodeBase* listener) {
+		if(listener != 0) {
+			listenerAccessMutex_.lock();
+			listeners_.insert(listener);
+			listenerAccessMutex_.unlock();
+		}
+	}
+	void unregisterListener(NodeBase* listener) {
+		listenerAccessMutex_.lock();
+		listeners_.erase(listener);
+		listenerAccessMutex_.unlock();
+	}
+	void clearListeners() {
+		listenerAccessMutex_.lock();
+		listeners_.clear();
+		listenerAccessMutex_.unlock();
+	}
+protected:
+	// Tell all our listeners about a new data point with the given timestamp
+	void notifyListenersOfInsert(timestamp_type timestamp) {
+		std::set<NodeBase*>::iterator it;
+		
+		listenerAccessMutex_.lock();
+		for(it = listeners_.begin(); it != listeners_.end(); it++)
+			(*it)->insertMissing(timestamp);
+		listenerAccessMutex_.unlock();
+	}
+	// Tell all our listeners that the buffer has cleared
+	void notifyListenersOfClear() {
+		std::set<NodeBase*>::iterator it;
+		
+		listenerAccessMutex_.lock();
+		for(it = listeners_.begin(); it != listeners_.end(); it++)
+			(*it)->clear();
+		listenerAccessMutex_.unlock();
+	}*/
+	
+public:	
+	// ***** Tree-Parsing Methods *****
+	//
+	// Sometimes we want to find out properties of the initial data source, regardless of what filters it's passed through.  These virtual
+	// methods should be implemented differently by Source and Filter classes.
+	
+	//virtual SourceBase& source() = 0;		// Return the source at the top of the tree for this unit
+	
+	// ***** Mutex Methods *****
+	//
+	// These methods should be used to acquire a lock whenever a process wants to read values from the buffer.  This would, for example,
+	// allow iteration over the contents of the buffer without worrying that the contents will change in the course of the iteration.
+	
+	void lock_mutex() { bufferAccessMutex_.enter(); }
+	bool try_lock_mutex() { return bufferAccessMutex_.tryEnter(); }
+	void unlock_mutex() { bufferAccessMutex_.exit(); }
+	
+	// ***** Circular Buffer (STL) Methods *****
+	//
+	// Certain STL methods (and related queries) that do not depend on the specific data type of the buffer should
+	// be available to any object with a NodeBase reference.
+	
+	virtual size_type size() = 0;				// Size: how many elements are currently in the buffer
+	virtual bool empty() = 0;
+	virtual bool full() = 0;
+	virtual size_type reserve() = 0;			// Reserve: how many elements are left before the buffer is full
+	virtual size_type capacity() const = 0;			// Capacity: how many elements could be in the buffer
+	
+	virtual size_type beginIndex() = 0;			// Index of the first sample we still have in the buffer
+	virtual size_type endIndex() = 0;			// Index just past the end of the buffer	
+	
+	// ***** Timestamp Methods *****
+	//
+	// Every sample is tagged with a timestamp.  We don't necessarily need to know the type of the sample to retrieve its
+	// associated timestamp.  However, we DO need to know the type in order to return an iterator.
+	
+	virtual timestamp_type timestampAt(size_type index) = 0;
+	virtual timestamp_type latestTimestamp() = 0;
+	virtual timestamp_type earliestTimestamp() = 0;
+	
+	virtual size_type indexNearestTo(timestamp_type t) = 0;
+	virtual size_type indexNearestBefore(timestamp_type t) = 0;
+	virtual size_type indexNearestAfter(timestamp_type t) = 0;	
+	
+	// ***** Member Variables *****
+protected:
+	// A collection of the units that are listening for updates on this unit.
+	//std::set<NodeBase*> listeners_;
+
+	// This mutex protects access to the underlying buffer.  It is locked every time a sample is written to the buffer,
+	// and external systems reading values from the buffer should also acquire at least a shared lock.
+	CriticalSection bufferAccessMutex_;
+	
+	// This mutex protects the list of listeners.  It prevents a listener from being added or removed while a notification
+	// is in progress.
+	//boost::mutex listenerAccessMutex_;
+	
+	// Keep an internal registry of who we've asked to send us triggers.  It's important to keep
+	// a list of these so that when this object is destroyed, all triggers are automatically unregistered.
+	//std::set<NodeBase*> triggerSources_;	
+	
+	// This list tracks the destinations we are *sending* triggers to (as opposed to the sources we're receiving from above)
+	//std::set<NodeBase*> triggerDestinations_;	
+};
+
+/*
+ * NodeNonInterpolating
+ *
+ * This class handles all functionality for a Node of a specific data type EXCEPT:
+ *   -- Interpolating accessors (for data types that support interpolation, use the more common Node subclass)
+ *   -- triggerReceived() which should be implemented by a specific subclass.
+ */
+
+template<typename OutputType>
+class NodeNonInterpolating : public NodeBase {
+public:	
+	// Useful type shorthands.  See <boost/circular_buffer.hpp> for details.
+	typedef typename std::allocator<OutputType> Alloc;
+	
+	typedef typename boost::circular_buffer<OutputType,Alloc>::value_type value_type;
+	typedef typename boost::circular_buffer<OutputType,Alloc>::pointer pointer;
+	typedef typename boost::circular_buffer<OutputType,Alloc>::const_pointer const_pointer;
+	typedef typename boost::circular_buffer<OutputType,Alloc>::reference reference;
+	typedef typename boost::circular_buffer<OutputType,Alloc>::const_reference const_reference;
+	typedef typename boost::circular_buffer<OutputType,Alloc>::difference_type difference_type;
+	//typedef typename boost::circular_buffer<OutputType,Alloc>::size_type size_type;
+	typedef typename boost::circular_buffer<OutputType,Alloc>::capacity_type capacity_type;
+	typedef typename boost::circular_buffer<OutputType,Alloc>::array_range array_range;
+	typedef typename boost::circular_buffer<OutputType,Alloc>::const_array_range const_array_range;
+	typedef typename boost::circular_buffer<OutputType,Alloc>::return_value_type return_value_type;
+	
+	// We only support const iterators.  (Modifying data in the buffer is restricted to only a few specialized instances.)
+	
+	typedef NodeIterator<OutputType, boost::cb_details::const_traits<Alloc>,
+											 boost::cb_details::nonconst_traits<Alloc> > const_iterator;
+	typedef const_iterator iterator;
+	typedef NodeReverseIterator<const_iterator> const_reverse_iterator;
+	typedef const_reverse_iterator reverse_iterator;
+	
+	template<class O, class T, class NCT> friend struct NodeIterator;
+
+	// ***** Constructors *****
+	
+	//Node() : buffer_(0), insertMissingLastTimestamp_(0), numSamples_(0), firstSampleIndex_(0) {}	
+	
+	// Recommended constructor: specify the capacity in samples
+	explicit NodeNonInterpolating(capacity_type capacity) : insertMissingLastTimestamp_(0), buffer_(0), numSamples_(0), firstSampleIndex_(0) {
+		buffer_ = new boost::circular_buffer<OutputType>(capacity);
+		timestamps_ = new boost::circular_buffer<timestamp_type>(capacity);
+	}	
+	
+	// Copy constructor
+	NodeNonInterpolating(const NodeNonInterpolating<OutputType>& obj) : numSamples_(obj.numSamples_), firstSampleIndex_(obj.firstSampleIndex_) {
+		if(obj.buffer_ != 0)
+			this->buffer_ = new boost::circular_buffer<OutputType>(*obj.buffer_);
+		else
+			this->buffer_ = 0;
+		if(obj.timestamps_ != 0)
+			timestamps_ = new boost::circular_buffer<timestamp_type>(*obj.timestamps_);
+		else
+			this->timestamps_ = 0;
+	}
+
+	// ***** Destructor *****
+	
+	virtual ~NodeNonInterpolating() {
+		delete buffer_;
+		delete timestamps_;
+	}
+	
+	// ***** Circular Buffer (STL) Methods *****
+	//
+	// In general we support const methods accessing the contents of the buffer, but only in limited cases can the buffer
+	// contents be modified.  Source objects allow inserting objects into the buffer, but Filters require the buffer to contain
+	// either the result of the evaluator function or a "missing" value.
+	
+	// ***** Accessors *****
+	
+	const_iterator begin() { return const_iterator(this, buffer_->begin()); }
+	const_iterator end() { return const_iterator(this, buffer_->end()); }
+	const_reverse_iterator rbegin() { return const_reverse_iterator(end()); }
+	const_reverse_iterator rend() { return const_reverse_iterator(begin()); }
+	
+	const_iterator iteratorAtIndex(size_type index) { return const_iterator(this, buffer_->begin()) + (index - firstSampleIndex_); }
+	const_reverse_iterator riteratorAtIndex(size_type index) { return const_reverse_iterator(iteratorAtIndex(index+1)); }
+	
+	return_value_type operator [] (size_type index) { return (*(this->buffer_))[index-this->firstSampleIndex_]; }
+	return_value_type at(size_type index) { return this->buffer_->at(index-this->firstSampleIndex_); }
+	return_value_type front() { return this->buffer_->front(); }
+	return_value_type back() { return this->buffer_->back(); }
+	
+	// Two more convenience methods to avoid confusion about what front and back mean!
+	return_value_type earliest() { return this->buffer_->front(); }
+	return_value_type latest() { return this->buffer_->back(); }	
+	
+	// In the following methods, check whether the value is missing and calculate it as necessary
+	// These methods return a value_type (i.e. not a reference, can't be used to modify the buffer.)
+	// However, they internally make use of modifying calls in order to update "missing" values.
+	
+	/*return_value_type operator [] (size_type index) {
+		return_value_type val = (*buffer_)[index-firstSampleIndex_];
+		if(!missing_value<OutputType>::isMissing(val))
+			return val;
+		return ((*buffer_)[index-firstSampleIndex_] = evaluate(index));
+	}
+	return_value_type at(size_type index) {
+		return_value_type val = buffer_->at(index-firstSampleIndex_);
+		if(!missing_value<OutputType>::isMissing(val))
+			return val;
+		return (buffer_->at(index-firstSampleIndex_) = evaluate(index));
+	}
+	return_value_type front() {
+		return_value_type val = buffer_->front();
+		if(!missing_value<OutputType>::isMissing(val))
+			return val;
+		return (buffer_->front() = evaluate(firstSampleIndex_));
+	}
+	return_value_type back() {
+		return_value_type val = buffer_->back();
+		if(!missing_value<OutputType>::isMissing(val) && buffer_->size() > 0)
+			return val;
+		return (buffer_->back() = evaluate(buffer_->size() - 1 + firstSampleIndex_));
+	}*/
+	
+	size_type size() { return buffer_->size(); }					// Size: how many elements are currently in the buffer
+	bool empty() { return buffer_->empty(); }
+	bool full() { return buffer_->full(); }
+	size_type reserve() { return buffer_->reserve(); }				// Reserve: how many elements are left before the buffer is full
+	size_type capacity() const { return buffer_->capacity(); }		// Capacity: how many elements could be in the buffer
+	
+	size_type beginIndex() { return firstSampleIndex_; }					// Index of the first sample we still have in the buffer
+	size_type endIndex() { return firstSampleIndex_ + buffer_->size(); }	// Index just past the end of the buffer
+	
+	// ***** Modifiers *****
+	
+	// Clear all stored samples and timestamps
+	void clear() {
+		bufferAccessMutex_.enter();
+		timestamps_->clear();
+		buffer_->clear();
+		numSamples_ = firstSampleIndex_ = 0;
+		bufferAccessMutex_.exit();
+		
+		//notifyListenersOfClear();
+	}
+	
+	// Insert a new item into the buffer
+	void insert(const OutputType& item, timestamp_type timestamp) {
+		this->bufferAccessMutex_.enter();
+		if(this->buffer_->full())
+			this->firstSampleIndex_++;
+		this->timestamps_->push_back(timestamp);
+		this->buffer_->push_back(item);
+		this->numSamples_++;
+		this->bufferAccessMutex_.exit();
+		
+		// Notify anyone who's listening for a trigger
+		this->sendTrigger(timestamp);
+	}
+	
+	// Insert a "missing" item into the buffer.  This is really for the Filter subclasses, but we should provide an implementation
+	/*void insertMissing(timestamp_type timestamp) {		
+		if(timestamp == insertMissingLastTimestamp_)
+			return;
+		this->bufferAccessMutex_.lock();
+		if(this->buffer_->full())
+			this->firstSampleIndex_++;		
+		this->timestamps_->push_back(timestamp);
+		this->buffer_->push_back(missing_value<OutputType>::missing());
+		this->numSamples_++;
+		this->bufferAccessMutex_.unlock();
+		
+		this->insertMissingLastTimestamp_ = timestamp;
+		this->notifyListenersOfInsert(timestamp);
+	}	*/
+	
+protected:
+	// Subclasses are allowed to change the values stored in their buffers.  Give this a different
+	// name to avoid confusion with the behavior of [] and at(), which call evaluate() if the sample
+	// is missing.
+	
+	reference rawValueAt(size_type index) { return (*buffer_)[index-firstSampleIndex_]; }
+	
+public:
+	// ***** Timestamp Methods *****
+	//
+	// Every sample is tagged with a timestamp.  The Filter classes will look up the chain to find the timestamp associated
+	// with the Source of any particular sample.  We also support methods to return an iterator to a piece of data most closely
+	// matching a given timestamp.
+	
+	timestamp_type timestampAt(size_type index) { return timestamps_->at(index-this->firstSampleIndex_); }
+	timestamp_type latestTimestamp() { return timestamps_->back(); }
+	timestamp_type earliestTimestamp() { return timestamps_->front(); }
+	
+	size_type indexNearestBefore(timestamp_type t) {
+		boost::circular_buffer<timestamp_type>::iterator it = std::find_if(timestamps_->begin(), timestamps_->end(), t < boost::lambda::_1);
+		if(it == timestamps_->end())
+			return timestamps_->size()-1+this->firstSampleIndex_;		
+		if(it - timestamps_->begin() == 0)
+			return this->firstSampleIndex_;
+		return (size_type)((--it) - timestamps_->begin()) + this->firstSampleIndex_; 
+	}
+	size_type indexNearestAfter(timestamp_type t) {
+		boost::circular_buffer<timestamp_type>::iterator it = std::find_if(timestamps_->begin(), timestamps_->end(), t < boost::lambda::_1);
+		return std::min<size_type>((it - timestamps_->begin()), timestamps_->size()-1) + this->firstSampleIndex_;
+	}
+	size_type indexNearestTo(timestamp_type t) {
+		boost::circular_buffer<timestamp_type>::iterator it = std::find_if(timestamps_->begin(), timestamps_->end(), t < boost::lambda::_1);
+		if(it == timestamps_->end())
+			return timestamps_->size()-1+this->firstSampleIndex_;
+		if(it - timestamps_->begin() == 0)
+			return this->firstSampleIndex_;
+		timestamp_diff_type after = *it - t;		// Calculate the distance between the desired timestamp and the before/after values,
+		timestamp_diff_type before = t - *(it-1);	// then return whichever index gets closer to the target.
+		if(after < before)
+			return (size_type)(it - timestamps_->begin()) + this->firstSampleIndex_;
+		return (size_type)((--it) - timestamps_->begin()) + this->firstSampleIndex_;
+	}	
+	
+	const_iterator nearestTo(timestamp_type t) { return begin() + (difference_type)indexNearestTo(t); }
+	const_iterator nearestBefore(timestamp_type t) { return begin() + (difference_type)indexNearestBefore(t); }
+	const_iterator nearestAfter(timestamp_type t) { return begin() + (difference_type)indexNearestAfter(t); }
+	
+	const_reverse_iterator rnearestTo(timestamp_type t) { return rend() - (difference_type)indexNearestTo(t); }
+	const_reverse_iterator rnearestBefore(timestamp_type t) { return rend() - (difference_type)indexNearestBefore(t); }
+	const_reverse_iterator rnearestAfter(timestamp_type t) { return rend() - (difference_type)indexNearestAfter(t); }
+	
+private:
+	// Calculate the actual value of one sample.  Behavior of this method will be different for Source and Filter types.
+	// virtual OutputType evaluate(size_type index) = 0;
+	
+	timestamp_type insertMissingLastTimestamp_;	// The last timestamp that came from insertMissing(), so we can avoid duplication	
+	
+protected:
+	boost::circular_buffer<OutputType> *buffer_;			// Internal buffer to store or cache values
+	boost::circular_buffer<timestamp_type> *timestamps_;	// Internal buffer to hold timestamps for each value
+	
+	size_type numSamples_;							// How many samples total we've stored in this buffer
+	size_type firstSampleIndex_;					// Index of the first sample that still remains in the buffer
+};
+
+/*
+ * Node
+ *
+ * This class handles a node of a specific data type, including the ability to interpolate between samples.  This is the recommended
+ * class to use for numeric data types and others for which linear interpolation makes sense.
+ */
+
+template<typename OutputType>
+class Node : public NodeNonInterpolating<OutputType> {
+public:	
+	typedef typename std::allocator<OutputType> Alloc;
+	typedef NodeInterpolatedIterator<OutputType, boost::cb_details::const_traits<Alloc> > interpolated_iterator;
+	
+	typedef typename NodeNonInterpolating<OutputType>::return_value_type return_value_type;
+	typedef typename NodeNonInterpolating<OutputType>::capacity_type capacity_type;
+	typedef typename NodeNonInterpolating<OutputType>::size_type size_type;
+	
+	// ***** Constructors *****
+	//
+	// Use the same constructors as the non-interpolating version.
+	
+	explicit Node(capacity_type capacity) : NodeNonInterpolating<OutputType>(capacity) {}
+	Node(Node<OutputType> const& obj) : NodeNonInterpolating<OutputType>(obj) {}
+	
+	// ***** Interpolating Accessors *****
+	//
+	// These overloaded methods allow querying a location between two samples, using linear
+	// interpolation to generate the value.
+	
+	return_value_type interpolate(double index) {
+		size_type before = floor(index);				// Find the sample before the interpolated location
+		double frac = index - (double)before;			// Find the fractional remainder component
+		OutputType val1 = this->buffer_->at(before-this->firstSampleIndex_);
+		if(before == this->endIndex()-1)
+			return val1;
+		OutputType val2 = this->buffer_->at(before+1-this->firstSampleIndex_);
+		//if(missing_value<OutputType>::isMissing(val1))	// Make sure both values have been calculated
+		//	val1 = (buffer_->at(before-firstSampleIndex_) = evaluate(before));
+		//if(missing_value<OutputType>::isMissing(val2))
+		//	val2 = (buffer_->at(before+1-firstSampleIndex_) = evaluate(before+1));		
+		return val1*(1.0-frac) + val2*frac;				// Return the interpolated value
+	}		
+	
+	interpolated_iterator interpolatedBegin(double step = 1.0) { return interpolated_iterator(this, (double)this->beginIndex(), step); }
+	interpolated_iterator interpolatedEnd(double step = 1.0) { return interpolated_iterator(this, (double)this->endIndex(), step); }
+	interpolated_iterator interpolatedIteratorAtIndex(double index, double step = 1.0) { return interpolated_iterator(this, index, step); }	
+	
+	// ***** Interpolating Timestamp Methods *****
+	//
+	// Using linear interpolation, find the exact relationship between a timestamp and an index in the buffer.
+	// Designed to be used in conjunction with the interpolate() method for buffer access.
+	
+	// Fractional index --> timestamp
+	timestamp_type interpolatedTimestampAt(double index) {
+		size_type before = floor(index);
+		double frac = index - (double)before;
+		timestamp_type ts1 = timestampAt(before);
+		if(before == this->endIndex()-1)
+			return ts1;		
+		timestamp_type ts2 = timestampAt(before+1);
+		return ts1*(1.0-frac)+ts2*frac;
+	}
+	
+	// Timestamp --> fractional index
+	double interpolatedIndexForTimestamp(timestamp_type timestamp) {
+		size_type before = this->indexNearestBefore(timestamp);
+		if(before >= this->timestamps_->size() - 1 + this->firstSampleIndex_)		// If it's at the end of the buffer, return the last available timestamp
+			return (double)before;
+		timestamp_type beforeTimestamp = this->timestampAt(before);			// Get the timestamp immediately before
+		if(beforeTimestamp >= timestamp)								// If it comes after the requested timestamp, we're at the beginning of the buffer
+			return (double)before;
+		timestamp_type afterTimestamp = this->timestampAt(before+1);
+		double frac = (timestamp - beforeTimestamp)/(afterTimestamp-beforeTimestamp);
+		return (double)before + frac;
+	}		
+};
+
+#endif /* KEYCONTROL_NODE_H */
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/Utility/Scheduler.cpp	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,219 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+
+  Scheduler.cpp: allows actions to be scheduled at future times. Runs a
+  thread in which these actions are executed.
+*/
+
+#include "Scheduler.h"
+#undef DEBUG_SCHEDULER
+
+using std::cout;
+
+const timestamp_diff_type Scheduler::kAllowableAdvanceExecutionTime = milliseconds_to_timestamp(1.0);
+
+// Start the thread handling the scheduling.  Pass it an initial timestamp.
+void Scheduler::start(timestamp_type where) {
+	if(isRunning_)
+		return;
+    startingTimestamp_ = where;
+    startThread();
+}
+
+// Stop the scheduler thread if it is currently running.  Events will remain
+// in the queue unless explicitly cleared.
+void Scheduler::stop() {
+	if(!isRunning_)
+		return;
+    
+    // Tell the thread to quit and signal the event it waits on
+    signalThreadShouldExit();
+    waitableEvent_.signal();
+    stopThread(-1);
+    
+	isRunning_ = false;
+}
+
+// Return the current timestamp, relative to this class's start time.
+timestamp_type Scheduler::currentTimestamp() {
+	if(!isRunning_)
+		return 0;
+    return milliseconds_to_timestamp(Time::getMillisecondCounterHiRes() - startTimeMilliseconds_);
+	//return ptime_to_timestamp(microsec_clock::universal_time() - startTime_);
+}
+
+// Schedule a new event
+void Scheduler::schedule(void *who, action func, timestamp_type timestamp) {
+    ScopedLock sl(eventMutex_);
+    
+#ifdef DEBUG_SCHEDULER
+    std::cerr << "Scheduler::schedule: " << who << ", " << timestamp << " (" << timestamp - currentTimestamp() << " from now)\n";
+#endif
+    
+    // Check if this timestamp will become the next thing in the queue
+    bool newActionWillComeFirst = false;
+    if(events_.empty())
+        newActionWillComeFirst = true;
+    else if(timestamp < events_.begin()->first)
+        newActionWillComeFirst = true;
+    events_.insert(std::pair<timestamp_type,std::pair<void*, action> >
+					(timestamp, std::pair<void*, action>(who, func)));
+
+	// Tell the thread to wake up and recheck its status if the
+    // time of the next event has changed
+    if(newActionWillComeFirst)
+        waitableEvent_.signal();
+}
+
+// Remove an existing event
+void Scheduler::unschedule(void *who, timestamp_type timestamp) {
+#ifdef DEBUG_SCHEDULER
+    std::cerr << "Scheduler::unschedule: " << who << ", " << timestamp << std::endl;
+#endif
+    ScopedLock sl(eventMutex_);
+    
+	// Find all events with this timestamp, and remove only the ones matching the given source
+	std::multimap<timestamp_type, std::pair<void*, action> >::iterator it;
+    
+    if(timestamp == 0) {
+        // Remove all events from this source
+        it = events_.begin();
+        while(it != events_.end()) {
+#ifdef DEBUG_SCHEDULER
+            std::cerr << "| (" << it->first << ", " << it->second.first << ")\n";
+#endif
+            if(it->second.first == who) {
+#ifdef DEBUG_SCHEDULER
+                std::cerr << "--> erased " << it->first << ", " << it->second.first << ")\n";
+#endif
+                events_.erase(it++);
+            }
+            else
+                it++;
+        }
+    }
+    else {
+        // Remove only a specific event from this source with the given timestmap
+        it = events_.find(timestamp);
+        while(it != events_.end()) {
+            if(it->second.first == who) {
+#ifdef DEBUG_SCHEDULER
+                std::cerr << "--> erased " << it->first << ", " << it->second.first << ")\n";
+#endif
+                events_.erase(it++);
+            }
+            else
+                it++;
+        }
+    }
+
+#ifdef DEBUG_SCHEDULER
+    std::cerr << "Scheduler::unschedule: done\n";
+#endif
+	// No need to wake up the thread...
+}
+
+// Clear all events from the queue
+void Scheduler::clear() {
+    ScopedLock sl(eventMutex_);
+    
+	events_.clear();
+	
+	// No need to signal the condition variable.  If the thread is waiting, it can keep waiting.
+}
+
+// This function runs in its own thread (from the Juce parent class).  It looks for the next event
+// in the queue.  When its time arrives, the event is executed and removed from the queue.
+// When the queue is empty, or the next event has not arrived yet, the thread sleeps.
+
+void Scheduler::run() {
+    // Start with the mutex locked.  The wait() methods will unlock it.
+    eventMutex_.enter();
+    
+	// Find the start time, against which our offsets will be measured.
+	//startTime_ = microsec_clock::universal_time();
+    startTimeMilliseconds_ = Time::getMillisecondCounterHiRes();
+	isRunning_ = true;
+	
+    // This will run until the thread is interrupted (in the stop() method)
+    // events_ is ordered by increasing timestamp, so the next event to execute is always the first item.
+    while(!threadShouldExit()) {
+        if(events_.empty())	{					// If there are no events in the queue, wait until we're signaled
+            eventMutex_.exit();                 // that a new one comes in.  Unlock the mutex and wait.
+            waitableEvent_.wait();
+            eventMutex_.enter();
+        }
+        else {
+            timestamp_type t = events_.begin()->first;				// Find the timestamp of the first event
+            double targetTimeMilliseconds = startTimeMilliseconds_ + timestamp_to_milliseconds(t);
+            
+            // Wait until that time arrives, provided it hasn't already
+            int timeDifferenceMilliseconds = (int)round(targetTimeMilliseconds - Time::getMillisecondCounterHiRes());
+#ifdef DEBUG_SCHEDULER
+            std::cerr << "Scheduler::run: waiting for " << timeDifferenceMilliseconds << "ms\n";
+#endif
+            if(timeDifferenceMilliseconds > 0) {
+                eventMutex_.exit();                                    
+                waitableEvent_.wait(timeDifferenceMilliseconds);
+                eventMutex_.enter();
+            }
+        }
+        
+        waitableEvent_.reset();             // Clear the signal
+        
+        if(threadShouldExit())
+            break;
+        
+        // At this point, the mutex is locked.  We can change the contents of events_ without worrying about disrupting anything.
+        
+        if(events_.empty())				// Double check that we actually have an event to execute
+            continue;
+        if(currentTimestamp() + kAllowableAdvanceExecutionTime < events_.begin()->first) {
+#ifdef DEBUG_SCHEDULER
+            std::cerr << "Scheduler::run: next event hasn't arrived (currently " << currentTimestamp() << ", waiting for " << events_.begin()->first << "\n";
+#endif
+            continue;
+        }
+        
+        // Run the function that's stored, which takes no arguments and returns a timestamp
+        // of the next time this particular function should run.
+        std::multimap<timestamp_type, std::pair<void*, action> >::iterator it = events_.begin();
+        action actionFunction = (it->second).second;
+        //timestamp_type testingTimestamp = it->first;
+        void *who = it->second.first;
+        
+#ifdef DEBUG_SCHEDULER
+        std::cerr << "Scheduler::run: " << who << ", " << it->first << std::endl;
+#endif
+        
+        timestamp_type timeOfNextEvent = actionFunction();
+        
+        // Remove the last event from the queue
+        events_.erase(it);
+        
+        if(timeOfNextEvent > 0) {
+            // Reschedule the same event for some (hopefully) future time.
+            events_.insert(std::pair<timestamp_type,std::pair<void*, action> >
+                           (timeOfNextEvent,
+                            std::pair<void*, action>(who, actionFunction)));
+        }
+    }
+    
+    eventMutex_.exit();
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/Utility/Scheduler.h	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,99 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+
+  Scheduler.h: allows actions to be scheduled at future times. Runs a
+  thread in which these actions are executed.
+*/
+
+#ifndef KEYCONTROL_SCHEDULER_H
+#define KEYCONTROL_SCHEDULER_H
+
+#include <iostream>
+#include <map>
+#include <boost/bind.hpp>
+#include <boost/function.hpp>
+#include "../JuceLibraryCode/JuceHeader.h"
+#include "Types.h"
+
+/*
+ * Scheduler
+ *
+ * This class allows function calls to be scheduled for arbitrary points in the future.
+ * It maintains a list of future events, ordered by timestamp.  A dedicated thread scans the
+ * list, and when it is time for an event to occur, the thread wakes up, executes it, deletes
+ * it from the list, and goes back to sleep.
+ */
+
+class Scheduler : public Thread {
+public:	
+	typedef boost::function<timestamp_type ()> action;
+    
+private:
+    static const timestamp_diff_type kAllowableAdvanceExecutionTime;
+	
+public:	
+	// ***** Constructor *****
+	//
+	// Note: This class is not copy-constructable.
+	
+	Scheduler(String threadName = "Scheduler") : Thread(threadName), waitableEvent_(true), isRunning_(false) {}
+	
+	// ***** Destructor *****
+	
+	~Scheduler() noexcept { stop(); }
+	
+	// ***** Timer Methods *****
+	//
+	// These start and stop the thread that handles the scheduling of events.
+	
+	void start(timestamp_type where = 0);
+	void stop();
+	
+	bool isRunning() { return isRunning_; }
+	timestamp_type currentTimestamp();
+	
+	// ***** Event Management Methods *****
+	//
+	// This interface provides the ability to schedule and unschedule events for
+	// future times.
+	
+	void schedule(void *who, action func, timestamp_type timestamp);
+	void unschedule(void *who, timestamp_type timestamp = 0);
+	void clear();
+	
+	//static void staticRunLoop(Scheduler* sch, timestamp_type starting_timestamp) { sch->runLoop(starting_timestamp); }
+	
+    // The main Thread run loop
+	void run();
+
+private:    
+	// These variables keep track of the status of the separate thread running the events
+    CriticalSection eventMutex_;
+    WaitableEvent waitableEvent_;
+    timestamp_type startingTimestamp_;
+	bool isRunning_;
+	
+	// Collection of future events to execute
+	//boost::posix_time::ptime startTime_;
+    double startTimeMilliseconds_;
+	std::multimap<timestamp_type, std::pair<void*, action> > events_;
+};
+
+
+#endif /* KEYCONTROL_SCHEDULER_H */
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/Utility/TimerNode.cpp	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,74 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+
+  TimerNode.cpp: creates a Node object which runs its own thread to generate
+  timestamps.
+*/
+
+#include "TimerNode.h"
+
+using std::cout;
+using std::endl;
+
+// Start the timer, if it isn't already running.  The update rate is set elsewhere.
+void TimerNode::start(timestamp_type where) {
+	if(isRunning_)
+		return;
+    startingTimestamp_ = where;
+    startThread();
+	isRunning_ = true;
+}
+
+// Stop the timer if it is currently running.  This kills the associated thread.
+void TimerNode::stop() {
+	if(!isRunning_)
+		return;
+    signalThreadShouldExit();
+    notify();
+    stopThread(-1); // Ask the thread to stop; no timeout
+	isRunning_ = false;
+}
+
+// This function runs in its own thread, as managed by the Juce Thread parent class.  It produces
+// data points approximately separated in time by intervalMicros_.  The resolution of the system
+// timer affects how precise the spacing will be.  Though the ticks may jitter, there shouldn't
+// be any systematic drift unless intervalMicros_ is smaller than the execution time of the loop.
+// (For example, 1000 will be fine; 1 is too short)
+
+void TimerNode::run() {
+	unsigned long long targetMicros = 0;
+	
+	// Find the start time, against which our offsets will be measured.
+    double startTime = Time::getMillisecondCounterHiRes();
+    double nextTime;
+    
+	while(!threadShouldExit()) {
+		// Get the current time relative to when we started, using it as the "official" timestamp 
+		// for the data source.
+		insert(startingTimestamp_ + microseconds_to_timestamp(targetMicros), milliseconds_to_timestamp(Time::getMillisecondCounterHiRes() - startTime));
+		targetMicros += intervalMicros_;
+        
+        // Next millisecond time according to Juce timer
+        nextTime = startTime + (double)targetMicros/1000.0;
+		
+		// Sleep until we get to the next tick.
+		// thread_.sleep(startTime + microseconds(target_micros));
+        wait((int)(nextTime - Time::getMillisecondCounterHiRes()));
+	}
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/Utility/TimerNode.h	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,91 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+
+  TimerNode.h: creates a Node object which runs its own thread to generate
+  timestamps.
+*/
+
+#ifndef KEYCONTROL_TIMER_H
+#define KEYCONTROL_TIMER_H
+
+#include <iostream>
+#include "Types.h"
+#include "Node.h"
+#include "../JuceLibraryCode/JuceHeader.h"
+
+class TimerNode : public Node<timestamp_type>, public Thread {
+#if 0
+    // ***** Class to implement the Juce thread *****
+private:
+    class TimerThread : public Thread {
+    public:
+        TimerThread(Timer *timer, timestamp_type starting_timestamp)
+        : timer_(timer), startingTimestamp_(starting_timestamp) {}
+        
+        ~TimerThread() {}
+        
+        void run() {
+            timer_->runLoop(startingTimestamp_);
+        }
+        
+    private:
+        Timer *timer_;
+        timestamp_type startingTimestamp_;
+    }
+#endif
+public:
+	// ***** Constructor *****
+	
+	TimerNode(capacity_type capacity, unsigned long long interval_micros, String threadName = "Timer")
+		: Node<timestamp_type>(capacity), Thread(threadName), intervalMicros_(interval_micros), isRunning_(false) {}
+	
+	// ***** Destructor *****
+	
+	~TimerNode() {
+		stop();
+	}
+
+	// ***** Timing Methods *****
+	//
+	// These functions start and stop the timer without deleting the data it has generated.
+	
+	void start(timestamp_type where = 0);
+	void stop();
+	
+	// Allow viewing of the interval as a timestamp type.  Allow viewing or setting it as an integer number
+	// of microseconds.  Don't set it directly as a timestamp_type: if timestamp_type is floating point, it
+	// gives a misleading impression of the behavior of the timer when the interval doesn't round to an even
+	// number of microseconds.
+	
+	timestamp_diff_type interval() { return microseconds_to_timestamp(intervalMicros_); }
+	unsigned long long& interval_micros() { return intervalMicros_; }
+	
+	// The loop runs in its own thread and feeds new ticks to the data source at regular intervals.  Give it
+	// the interval length in microseconds and the timestamp of the first tick.
+	// static void staticRunLoop(Timer* timer, timestamp_type starting_timestamp) { timer->runLoop(starting_timestamp); }
+	void run();
+
+private:	
+	//TimerThread *thread_;
+	unsigned long long intervalMicros_;
+	bool isRunning_;
+    timestamp_type startingTimestamp_;
+};
+
+#endif /* KEYCONTROL_TIMER_H */
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/Utility/TimestampSynchronizer.cpp	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,156 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+
+  TimestampSynchronizer.cpp: handles aligning timestamps between multiple
+  asynchronous sources, while reducing the jitter that occurs when using
+  system clock time for every received sample.
+*/
+
+#include "TimestampSynchronizer.h"
+
+// Constructor
+TimestampSynchronizer::TimestampSynchronizer()
+: history_(kTimestampSynchronizerHistoryLength), nominalSampleInterval_(0), currentSampleInterval_(0),
+frameModulus_(0), startingClockTimeMilliseconds_(0), startingTimestamp_(0), 
+bufferLengthCounter_(0)
+{
+}
+
+// Clear the accumulated timestamp history and reset the current
+// value to its nominal "expected" value.  Also (re-)establish
+// the relationship between system clock time and output timestamp.
+// If multiple streams are to be synchronized, they should be
+// initialized with the same values
+
+void TimestampSynchronizer::initialize(double clockTimeMilliseconds,
+									   timestamp_type startingTimestamp) {
+	history_.clear();
+	currentSampleInterval_ = nominalSampleInterval_;
+	startingClockTimeMilliseconds_ = clockTimeMilliseconds;
+	startingTimestamp_ = startingTimestamp;
+	
+	//cout << "initialize(): startingTimestamp = " << startingTimestamp_ << ", interval = " << nominalSampleInterval_ << endl;
+}
+
+// Given a frame number, calculate a current timestamp
+timestamp_type TimestampSynchronizer::synchronizedTimestamp(int rawFrameNumber) {
+	// Calculate the current system clock-related timestamp
+	timestamp_type clockTime = startingTimestamp_ + milliseconds_to_timestamp(Time::getMillisecondCounterHiRes() - startingClockTimeMilliseconds_);
+	timestamp_type frameTime;
+
+	// Retrieve the timestamp of the previous frame
+	// Need at least 2 samples in the buffer for the calculations that follow
+	if(history_.empty()) {
+		frameTime = clockTime;
+	}
+	else if(history_.size() < 2) {
+		// One sample in buffer: make sure the new sample is new before
+		// storing it in the buffer.
+		
+		int lastFrame = history_.latest().first;
+		
+		frameTime = clockTime;
+		
+		if(lastFrame == rawFrameNumber) // Don't reprocess identical frames
+			return frameTime;		
+	}
+	else {
+		int totalHistoryFrames;		
+		int lastFrame = history_.latest().first;
+		frameTime = history_.latest().second;
+		
+		if(lastFrame == rawFrameNumber) // Don't reprocess identical frames
+			return frameTime;
+			
+		if(frameModulus_ == 0) {
+			// No modulus, just compare the raw frame number to the last frame number
+			frameTime += currentSampleInterval_ * (timestamp_type)(rawFrameNumber - lastFrame);
+			
+			totalHistoryFrames = (history_.latest().first - history_.earliest().first);
+			if(totalHistoryFrames <= 0) {
+				//cout << "Warning: TimestampSynchronizer history buffer has a difference of " << totalHistoryFrames << " frames.\n";
+				//cout << "Size = " << history_.size() << " first = " << history_.earliest().first << " last = " << history_.latest().first << endl;
+				totalHistoryFrames = 1;
+			}
+		}
+		else {
+			// Use mod arithmetic to handle wraparounds in the frame number
+			frameTime += currentSampleInterval_ * (timestamp_type)((rawFrameNumber + frameModulus_ - lastFrame) % frameModulus_);
+			
+			totalHistoryFrames = (history_.latest().first - history_.earliest().first + frameModulus_) % frameModulus_;
+			if(totalHistoryFrames <= 0) {
+				//cout << "Warning: TimestampSynchronizer history buffer has a difference of " << totalHistoryFrames << " frames.\n";
+				//cout << "Size = " << history_.size() << " first = " << history_.earliest().first << " last = " << history_.latest().first << endl;
+
+				totalHistoryFrames = 1;
+			}			
+		}
+		
+		// Recalculate the nominal sample interval by examining the difference in times
+		// between first and last frames in the buffer.
+		
+		currentSampleInterval_ = (history_.latestTimestamp() - history_.earliestTimestamp()) / (timestamp_diff_type)totalHistoryFrames;
+		
+		// The frame time was just incremented by the current sample period.  Check whether
+		// this puts the frame time ahead of the clock time.  Don't allow the frame time to get
+		// ahead of the system clock (this will also push future frame timestamps back).
+		
+		if(frameTime > clockTime) {
+			//cout << "CLIP " << 100.0 * (frameTime - clockTime) / currentSampleInterval_ << "%: frame=" << frameTime << " to clock=" << clockTime << endl;
+			frameTime = clockTime;			
+		}
+		
+		bufferLengthCounter_++;
+		
+		if(bufferLengthCounter_ >= kTimestampSynchronizerHistoryLength) {
+			//timestamp_diff_type currentLatency = clockTime - frameTime;
+			timestamp_diff_type maxLatency = 0, minLatency = 1000000.0;
+			
+			Node<pair<int, timestamp_type> >::iterator it;
+			
+			for(it = history_.begin(); it != history_.end(); ++it) {
+				timestamp_diff_type l = (it.timestamp() - it->second);
+				if(l > maxLatency)
+					maxLatency = l;
+				if(l < minLatency)
+					minLatency = l;
+			}
+			
+			//cout << "frame " << rawFrameNumber << ": rate = " << currentSampleInterval_ << " clock = " << clockTime << " frame = " << frameTime << " latency = " 
+			//	<< currentLatency << " max = " << maxLatency << " min = " << minLatency << endl;
+			
+			//timestamp_diff_type targetMinLatency = (maxLatency - minLatency) * 2.0 / sqrt(kTimestampSynchronizerHistoryLength);
+			
+			/*if(minLatency > targetMinLatency) {
+				cout << "ADDING " << 50.0 * (minLatency - targetMinLatency) / (currentSampleInterval_) << "%: (target " << targetMinLatency << ")\n";
+				frameTime += (minLatency - targetMinLatency) / 2.0;
+			}*/
+			//frameTime += minLatency / 4.0;
+			
+			bufferLengthCounter_ = 0;
+		}
+	}
+	
+	// Insert the new frame time and clock times into the buffer
+	history_.insert(pair<int, timestamp_type>(rawFrameNumber, frameTime), clockTime);
+
+	// The timestamp we return is associated with the frame, not the clock (which is potentially much
+	// higher jitter)
+	return frameTime;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/Utility/TimestampSynchronizer.h	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,105 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+
+  TimestampSynchronizer.h: handles aligning timestamps between multiple
+  asynchronous sources, while reducing the jitter that occurs when using
+  system clock time for every received sample.
+*/
+
+#ifndef TIMESTAMP_SYNCHRONIZER_H
+#define TIMESTAMP_SYNCHRONIZER_H
+
+#include <iostream>
+#include "../JuceLibraryCode/JuceHeader.h"
+#include "Types.h"
+#include "Node.h"
+
+const int kTimestampSynchronizerHistoryLength = 100;
+
+/* TimestampSynchronizer
+ *
+ * We often want to deal with multiple independent data streams, roughly
+ * synchronized in time to one another but operating on separate clocks.
+ * Each data stream can be assumed to have a regular, constant frame interval
+ * but the exact duration of the interval might not be known and might drift
+ * with respect to the system clock.
+ *
+ * In this class, the self-reported frame number is compared to the current
+ * system time.  In any multitasking OS, the system time when we receive a
+ * frame may jitter around, but in the long-term average, we want system clock
+ * and frame clock to stay in sync.  Thus we use the low-pass-filtered difference
+ * between system clock and frame clock to adjust the reported frame rate, keeping
+ * the two locked together.
+ */
+
+using namespace std;
+ 
+class TimestampSynchronizer {
+public:
+	// Constructor
+	TimestampSynchronizer();
+	
+	// Clear accumulated timestamps and reinitialize a relationship between clock
+	// time and output timestamp.
+	void initialize(double clockTimeMilliseconds, timestamp_type startingTimestamp);
+	
+	// Return or set the expected interval between frames
+	timestamp_type nominalSampleInterval() { return nominalSampleInterval_; }
+	void setNominalSampleInterval(timestamp_type interval) { 
+		nominalSampleInterval_ = interval; 
+		currentSampleInterval_ = interval;
+	}
+	
+	// Return the current calculated interval between frames
+	timestamp_type currentSampleInterval() { return currentSampleInterval_; }
+	
+	// Return or set the frame modulus (at what number the frame counter wraps
+	// around to 0, since it can't increase forever).
+	int frameModulus() { return frameModulus_; }
+	void setFrameModulus(int modulus) { frameModulus_ = modulus; }
+	
+	// Process a new timestamp value and return the value synchronized to the
+	// system clock
+	timestamp_type synchronizedTimestamp(int rawFrameNumber);
+
+private:
+	// History buffer of clock time vs. frame time.  The Node has a data type
+	// (frame number and frame timestamp, respectively) and a timestamp (clock timestamp);
+	// in other words, two different times are held within the buffer as well as the frame numbers.
+	
+	Node<pair<int, timestamp_type> > history_;
+	
+	// Expected and currently calculated frame intervals
+	
+	timestamp_type nominalSampleInterval_;
+	timestamp_type currentSampleInterval_;
+
+	// Modulus of frame number, i.e. the number at which the frame counter
+	// wraps around back to 0.
+	int frameModulus_;
+	
+	// The time we start from (clock and output timestamp)
+	
+	double startingClockTimeMilliseconds_;
+	timestamp_type startingTimestamp_;
+	
+	int bufferLengthCounter_;
+};
+
+#endif /* TIMESTAMP_SYNCHRONIZER_H */
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/Utility/Trigger.cpp	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,118 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+ 
+  Trigger.cpp: interface for objects to notify one another that new data
+  has arrived. TriggerSource can send triggers to any object inheriting
+  from TriggerDestination. Methods of keeping sources and destinations
+  in sync are included in each class.
+*/
+
+#include "Trigger.h"
+
+#undef DEBUG_TRIGGERS
+
+void TriggerSource::sendTrigger(timestamp_type timestamp) {
+#ifdef DEBUG_TRIGGERS
+    std::cerr << "sendTrigger (" << this << ")\n";
+#endif
+    
+    if(triggerDestinationsModified_) {
+        ScopedLock sl(triggerSourceMutex_);
+        processAddRemoveQueue();
+    }
+    
+    std::set<TriggerDestination*>::iterator it = triggerDestinations_.begin();
+	TriggerDestination* target;
+	while(it != triggerDestinations_.end()) {	// Advance the iterator before sending the trigger
+		target = *it;							// in case the triggerReceived routine causes the object to unregister
+#ifdef DEBUG_TRIGGERS
+        std::cerr << " --> " << target << std::endl;
+#endif
+		target->triggerReceived(this, timestamp);
+        it++;
+	}
+}
+
+void TriggerSource::addTriggerDestination(TriggerDestination* dest) { 
+#ifdef DEBUG_TRIGGERS
+    std::cerr << "addTriggerDestination (" << this << "): " << dest << "\n";
+#endif
+	if(dest == 0 || (void*)dest == (void*)this)
+		return;
+    ScopedLock sl(triggerSourceMutex_);
+    // Make sure this trigger isn't already present
+    if(triggerDestinations_.count(dest) == 0) {
+        triggersToAdd_.insert(dest);
+        triggerDestinationsModified_ = true;
+    }
+    // If the trigger is also slated to be removed, cancel that request
+    if(triggersToRemove_.count(dest) != 0)
+        triggersToRemove_.erase(dest);
+}
+
+void TriggerSource::removeTriggerDestination(TriggerDestination* dest) {
+#ifdef DEBUG_TRIGGERS
+    std::cerr << "removeTriggerDestination (" << this << "): " << dest << "\n";
+#endif
+    ScopedLock sl(triggerSourceMutex_);
+    // Check whether this trigger is actually present
+    if(triggerDestinations_.count(dest) != 0) {
+        triggersToRemove_.insert(dest);
+        triggerDestinationsModified_ = true;
+    }
+    // If the trigger is also slated to be added, cancel that request
+    if(triggersToAdd_.count(dest) != 0)
+        triggersToAdd_.erase(dest);
+}	
+
+void TriggerSource::clearTriggerDestinations() {
+#ifdef DEBUG_TRIGGERS
+    std::cerr << "clearTriggerDestinations (" << this << ")\n";
+#endif
+    ScopedLock sl(triggerSourceMutex_);
+    processAddRemoveQueue();
+	std::set<TriggerDestination*>::iterator it;
+	for(it = triggerDestinations_.begin(); it != triggerDestinations_.end(); ++it)
+		(*it)->triggerSourceDeleted(this);		
+	triggerDestinations_.clear();
+}
+
+// Process everything in the add and remove groups and transfer them
+// into the main set of trigger destinations. Do this with mutex locked.
+void TriggerSource::processAddRemoveQueue() {
+#ifdef DEBUG_TRIGGERS
+    std::cerr << "processAddRemoveQueue (" << this << ")\n";
+#endif
+    std::set<TriggerDestination*>::iterator it;
+    for(it = triggersToAdd_.begin(); it != triggersToAdd_.end(); ++it) {
+        triggerDestinations_.insert(*it);
+#ifdef DEBUG_TRIGGERS
+        std::cerr << " --> added " << *it << std::endl;
+#endif
+    }
+    for(it = triggersToRemove_.begin(); it != triggersToRemove_.end(); ++it) {
+        triggerDestinations_.erase(*it);
+#ifdef DEBUG_TRIGGERS
+        std::cerr << " --> removed " << *it << std::endl;
+#endif
+    }
+    triggersToAdd_.clear();
+    triggersToRemove_.clear();
+    triggerDestinationsModified_ = false;
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/Utility/Trigger.h	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,157 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+ 
+  Trigger.h: interface for objects to notify one another that new data
+  has arrived. TriggerSource can send triggers to any object inheriting
+  from TriggerDestination. Methods of keeping sources and destinations
+  in sync are included in each class.
+*/
+
+#ifndef KEYCONTROL_TRIGGER_H
+#define KEYCONTROL_TRIGGER_H
+
+#include <iostream>
+#include <set>
+#include "../JuceLibraryCode/JuceHeader.h"
+#include "Types.h"
+
+class TriggerDestination;
+
+/*
+ * TriggerSource
+ *
+ * Provides a set of routines for an object that sends triggers with an associated timestamp.  All Node
+ * objects inherit from Trigger, but other objects may use these routines as well.
+ */
+
+class TriggerSource {
+	friend class TriggerDestination;
+protected:
+	// Send a trigger event out to all our registered listeners.  The type of the accompanying
+	// data will be set by the template of the subclass.
+	void sendTrigger(timestamp_type timestamp);
+	
+public:
+	// ***** Constructor *****
+	
+	TriggerSource() {}	// No instantiating this class directly!
+	
+	// ***** Destructor *****
+	
+	~TriggerSource() { clearTriggerDestinations(); }	
+	
+	// ***** Connection Management *****
+	
+	bool hasTriggerDestinations() { return triggerDestinations_.size() > 0; }
+
+private:
+	// For internal use or use by friend class NodeBase only
+	
+	// These methods manage the list of objects to whom the triggers should be sent.  All objects
+	// will inherit from the Triggerable base class.  These shouldn't be called by the user directly;
+	// rather, they're called by Triggerable when it registers itself.
+	
+	void addTriggerDestination(TriggerDestination* dest);
+	void removeTriggerDestination(TriggerDestination* dest);
+	void clearTriggerDestinations();
+    
+    // When sources are added or removed, they are first stored in separate locations to be updated
+    // prior to each new call of sendTrigger(). This way, destinations which are updated from functions
+    // called from sendTrigger() do not render the set inconsistent in the middle.
+    void processAddRemoveQueue();
+	
+private:
+	std::set<TriggerDestination*> triggerDestinations_;
+    std::set<TriggerDestination*> triggersToAdd_;
+    std::set<TriggerDestination*> triggersToRemove_;
+    bool triggerDestinationsModified_;
+	CriticalSection triggerSourceMutex_;
+};
+
+/*
+ * TriggerDestination
+ *
+ * This class accepts a Trigger event.  Designed to be inherited by more complex objects.
+ */
+
+class TriggerDestination {
+	friend class TriggerSource;
+public:
+	// ***** Constructors *****
+	
+	TriggerDestination() {}
+	TriggerDestination(TriggerDestination const& obj) : registeredTriggerSources_(obj.registeredTriggerSources_) {}
+	
+	// This defines what we actually do when a trigger is received.  It should be implemented
+	// by the subclass.
+	virtual void triggerReceived(TriggerSource* who, timestamp_type timestamp) { /*std::cout << "     received this = " << this << " who = " << who << std::endl;*/ }
+	
+	// These methods register and unregister sources of triggers.
+	
+	void registerForTrigger(TriggerSource* src) {
+		//std::cout<<"registerForTrigger: this = " << this << " src = " << src << std::endl;
+		
+		if(src == 0 || (void*)src == (void*)this)
+			return;
+		ScopedLock sl(triggerDestMutex_);
+		src->addTriggerDestination(this);
+		registeredTriggerSources_.insert(src);
+	}
+	
+	void unregisterForTrigger(TriggerSource* src) {
+		if(src == 0 || (void*)src == (void*)this)
+			return;
+        ScopedLock sl(triggerDestMutex_);
+		src->removeTriggerDestination(this);
+		registeredTriggerSources_.erase(src);
+	}
+	
+	void clearTriggers() {
+		ScopedLock sl(triggerDestMutex_);
+		std::set<TriggerSource*>::iterator it;
+		for(it = registeredTriggerSources_.begin(); it != registeredTriggerSources_.end(); it++)
+			(*it)->removeTriggerDestination(this);
+		registeredTriggerSources_.clear();
+	}
+	
+protected:
+	// This method is called by a TriggerBase object when it is deleted, so that we know not
+	// to contact it later when this object is deleted.  This is different than unregisterForTrigger()
+	// because it only removes the reference and does not call the TriggerBase object (which would create
+	// an infinite loop).
+	
+	void triggerSourceDeleted(TriggerSource* src) { registeredTriggerSources_.erase(src); }
+	
+public:
+	// ***** Destructor *****
+	//
+	// Remove all trigger sources before this object goes away
+	
+	virtual ~TriggerDestination() { clearTriggers(); }
+	
+private:
+	// Keep an internal registry of who we've asked to send us triggers.  It's important to keep
+	// a list of these so that when this object is destroyed, all triggers are automatically unregistered.
+	std::set<TriggerSource*> registeredTriggerSources_;
+    CriticalSection triggerDestMutex_;
+};
+
+
+
+#endif /* KEYCONTROL_TRIGGER_H */
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Source/Utility/Types.h	Mon Nov 11 18:19:35 2013 +0000
@@ -0,0 +1,121 @@
+/*
+  TouchKeys: multi-touch musical keyboard control software
+  Copyright (c) 2013 Andrew McPherson
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+ 
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ 
+  =====================================================================
+ 
+  Types.h: basic types used throughout the program
+*/
+
+#ifndef KEYCONTROL_TYPES_H
+#define KEYCONTROL_TYPES_H
+
+#include <limits>
+#include <cstdlib>
+#include <cmath>
+#include <utility>
+
+#undef FIXED_POINT_TIME
+
+// The following template specializations give the "missing" values for each kind of data that can be used in a Node.
+// If an unknown type is added, its "missing" value is whatever comes back from the default constructor.  Generally speaking, new
+// types should be added to this list as they are used
+
+template<typename T>
+struct missing_value {
+	static const T missing() { return T(); }
+	static const bool isMissing(T val) { return val == missing(); }
+};
+
+template<> struct missing_value<short> { 
+	static const short missing() { return std::numeric_limits<short>::max(); } 
+	static const bool isMissing(short val) { return (val == missing()); }
+};
+template<> struct missing_value<unsigned short> { 
+	static const unsigned short missing() { return std::numeric_limits<unsigned short>::max(); } 
+	static const bool isMissing(unsigned short val) { return (val == missing()); }
+};
+template<> struct missing_value<int> {	
+	static const int missing() { return std::numeric_limits<int>::max(); } 
+	static const bool isMissing(int val) { return (val == missing()); }
+};
+template<> struct missing_value<unsigned int> { 
+	static const unsigned int missing() { return std::numeric_limits<unsigned int>::max(); } 
+	static const bool isMissing(unsigned int val) { return (val == missing()); }
+};
+template<> struct missing_value<long> {	
+	static const long missing() { return std::numeric_limits<long>::max(); } 
+	static const bool isMissing(long val) { return (val == missing()); }
+};
+template<> struct missing_value<unsigned long> { 
+	static const unsigned long missing() { return std::numeric_limits<unsigned long>::max(); } 
+	static const bool isMissing(unsigned long val) { return (val == missing()); }
+};
+template<> struct missing_value<long long> { 
+	static const long long missing() { return std::numeric_limits<long long>::max(); } 
+	static const bool isMissing(long long val) { return (val == missing()); }
+};
+template<> struct missing_value<unsigned long long> { 
+	static const unsigned long long missing() { return std::numeric_limits<unsigned long long>::max(); }
+	static const bool isMissing(unsigned long long val) { return (val == missing()); }
+};
+template<> struct missing_value<float> { 
+	static const float missing() { return std::numeric_limits<float>::quiet_NaN(); } 
+	static const bool isMissing(float val) { return std::isnan(val); }
+};
+template<> struct missing_value<double> { 
+	static const double missing() { return std::numeric_limits<double>::quiet_NaN(); } 
+	static const bool isMissing(double val) { return std::isnan(val);  }
+};
+template<typename T1, typename T2>
+struct missing_value<std::pair<T1,T2> > {
+	static const std::pair<T1,T2> missing() { return std::pair<T1,T2>(missing_value<T1>::missing(), missing_value<T2>::missing()); }
+	static const bool isMissing(std::pair<T1,T2> val) {
+		return missing_value<T1>::isMissing(val.first) && missing_value<T2>::isMissing(val.second);
+	}
+};
+
+
+// Globally-defined types: these types must be shared by all active units
+
+#ifdef FIXED_POINT_TIME
+typedef unsigned long long timestamp_type;
+typedef long long timestamp_diff_type;
+
+#define timestamp_abs(x) std::llabs(x)
+#define ptime_to_timestamp(x) (x).total_microseconds()
+#define timestamp_to_ptime(x) microseconds(x)
+#define timestamp_to_milliseconds(x) ((x)/1000ULL)
+#define microseconds_to_timestamp(x) (x)
+#define milliseconds_to_timestamp(x) ((x)*1000ULL)
+#define seconds_to_timestamp(x) ((x)*1000000ULL)
+
+#else /* Floating point time */
+typedef double timestamp_type;
+typedef double timestamp_diff_type;
+
+#define timestamp_abs(x) std::fabs(x)
+#define ptime_to_timestamp(x) ((timestamp_type)(x).total_microseconds()/1000000.0)
+#define timestamp_to_ptime(x) microseconds((x)*1000000.0)
+#define timestamp_to_milliseconds(x) ((x)*1000.0)
+#define microseconds_to_timestamp(x) ((double)(x)/1000000.0)
+#define milliseconds_to_timestamp(x) ((double)(x)/1000.0)
+#define seconds_to_timestamp(x) (x)
+
+#endif /* FIXED_POINT_TIME */
+
+
+#endif /* KEYCONTROL_TYPES_H */
\ No newline at end of file