Mercurial > hg > touchkeys
changeset 0:3580ffe87dc8
First commit of TouchKeys public pre-release.
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>
--- /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: " 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: " 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& 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& 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& 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& 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*)×tamp, 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(¤tTime, 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*)×tamp, 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*)×tamp, 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(¤tTime, 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